Skip to main content

EIOTCLUB Integration Handoff

接入概览

EIOTCLUB 接入已经从基础设施、数据库扩展、只读同步、履约闭环、退款预览与执行、回调处理到用户侧可见性全部串起来了。现在本地系统可以从 EIOTCLUB 拉取卡片、套餐、余额和订单状态,也可以接收 EIOTCLUB webhook 推进 DataPlanPurchaseSimCard 状态,并在后台和用户侧看到同步后的结果。

环境变量

Variable用途缺失行为
EIOTCLUB_APP_KEYEIOTCLUB API 请求签名所需的 appkey未配置时 postSigned() 返回 null,脚本打印 EIOTCLUB not configured
EIOTCLUB_SECRETEIOTCLUB API 请求签名所需 secret同上
EIOTCLUB_BASE_URLEIOTCLUB API Base URL,默认 https://oapi.eiotclub.com未配置时使用默认值
EIOTCLUB_WEBHOOK_SECRETEIOTCLUB 回调验签 secret未配置时 webhook 验签失败;本地 replay 脚本会提示 EIOTCLUB not configured
CRON_SECRET/api/cron/eiotclub-reconcile 鉴权 secret未配置时 cron 路由返回 cron_not_configured

关键文件地图

  • lib/eiotclub.ts (legacy codebase reference)
  • lib/sim-sync.ts (legacy codebase reference)
  • lib/data-plan-fulfillment.ts (legacy codebase reference)
  • lib/eiotclub-webhook-handlers.ts (legacy codebase reference)
  • lib/eiotclub-webhook-idempotency.ts (legacy codebase reference)
  • lib/eiotclub-events.ts (legacy codebase reference)
  • app/api/webhooks/eiotclub/route.ts (legacy codebase reference)
  • app/api/webhooks/data-plan/route.ts (legacy codebase reference)
  • app/api/cron/eiotclub-reconcile/route.ts (legacy codebase reference)
  • app/admin/sim-cards/ (legacy codebase reference)
  • app/admin/data-plan-purchases/ (legacy codebase reference)

状态机

stateDiagram-v2
[*] --> pending
pending --> pending_assignment: webhook / retry fulfillment
pending --> ordering: Stripe webhook / retry fulfillment / order_detail
pending_assignment --> ordering: assign SIM + fulfill
ordering --> active: package_activated webhook / reconcile / cron
ordering --> failed: webhook error / fulfillment error
active --> expired: usage_exhausted webhook / reconcile
active --> refunded: refund webhook / admin refund
ordering --> refunded: refund webhook / admin refund
expired --> refunded: refund webhook / admin refund
active --> active: sync / reconcile / usage refresh

9 类回调映射表

EIOTCLUB 原始事件名本地 EiotclubEventType业务动作
FlowAlert / CloudESimFlowAlertflow_warning更新 SimCard.remainFlowMb,不发邮件
SubPkgList / CloudESimSubPkgListorder_detaileiotclubOrderId 找购买记录,推进到 ORDERING,落 eiotclubPackageEndDate
PkgEffective / CloudESimPkgActivatepackage_activated购买记录推进 ACTIVE,写 activatedAteiotclubPackageEndDateexpiresAt
Refund / CloudESimRefundrefund购买记录推进 REFUNDED,幂等处理重复投递
PkgQuantityList / CloudESimPkgDeactivateusage_exhausted购买记录推进 EXPIRED,同步 SimCard.planExpiry
CardStopped / CloudESimCardStoppedcard_offlineSimCard.status = "offline"
SwitchProduct / CloudESimSwitchProductproduct_switched更新 SimCard.packageCode / packageName / packageType
CardIMEILockedcard_lockedSimCard.status = "locked"
IMEIUnLockcard_unlockedSimCard.status = "active"

dedupKey 规则

优先使用 EIOTCLUB 回调自带的唯一标识 id;如果没有 id,则使用 sha256(eventType + iccid + orderId/packageCode + timestamp) 作为本地幂等键,并写入 SimEvent.payload.dedupKey

签名规则要点

  • API 请求签名包含 appkey
  • 回调验签不包含 appkey
  • 过滤 null / undefined / ""
  • 参数按 ASCII 升序排序
  • 每项拼成 key=value&
  • 最后直接追加 secret=xxx
  • 对字符串做 SHA1 后再 toUpperCase()

实现细节见 lib/eiotclub.ts(legacy codebase reference)。

手动运维操作

  • 后台单卡同步:/admin/sim-cards/[iccid]Sync from EIOTCLUB
  • 后台全量同步:/admin/sim-cardsSync all from EIOTCLUB / Dry run
  • 后台履约重试:/admin/data-plan-purchases/[id]Retry fulfillment
  • 后台状态回收:/admin/data-plan-purchases/[id]Reconcile status
  • 后台用量刷新:/admin/data-plan-purchases/[id]Refresh usage
  • 后台 EIOTCLUB 退款:/admin/data-plan-purchases/[id]Refund via EIOTCLUB
  • 后台取消会话:/admin/sim-cards/[iccid]Cancel session / reconnect
  • Stripe 退款仍然在 /admin/payments 里单独处理,EIOTCLUB 退款不自动联动 Stripe
  • cron 建议:/api/cron/eiotclub-reconcile 每 10 到 15 分钟跑一次

联调脚本

scripts/eiotclub-smoke.ts

用途:本地 / staging 只读探测 EIOTCLUB。

示例:

npm run eiotclub:smoke -- account-balance
npm run eiotclub:smoke -- card-count
npm run eiotclub:smoke -- cards --page 1 --size 10
npm run eiotclub:smoke -- card --iccid 8988308650104486856
npm run eiotclub:smoke -- packages --iccid 8988308650104486856
npm run eiotclub:smoke -- package --code PKG-1

scripts/eiotclub-webhook-replay.ts

用途:把本地 JSON 样例重新签名后 POST 到本地 webhook,验证回调链路。

示例:

cat scripts/fixtures/eiotclub-webhook/package_activated.json | npm run eiotclub:webhook-replay
npm run eiotclub:webhook-replay -- --file scripts/fixtures/eiotclub-webhook/refund.json
npm run eiotclub:webhook-replay -- --file scripts/fixtures/eiotclub-webhook/card_locked.json

已知边界 / 留待后续

  • eSIM / cloud eSIM 目前只接了客户端壳,业务侧只保留了兼容入口
  • 运营商切换、IMEI 池、短信、分润未接
  • pending_assignment 的用户侧 assign 流程仍未实现
  • EIOTCLUB 与 Stripe 的退款联动仍需人工决定,建议按订单号 / 支付意图 / EIOTCLUB orderId 做对账
  • CloudESIM 某些回调只带 eid,本地 SimCard 目前没有 eid 字段时只能落 not_local

端到端验证清单

  • 配齐 4 个 EIOTCLUB env,重启服务
  • npm run eiotclub:smoke -- account-balance 返回非空
  • npm run eiotclub:smoke -- cards --page 1 --size 5 返回卡列表
  • 后台 /admin/sim-cardsSync all (Dry run) 看到 scanned / updated / skipped
  • 取一张真卡 ICCID,本地 DB upsert 占位行;点 Sync from EIOTCLUB,字段被填
  • 用 Stripe test card 走一次 /data-plans 购买;webhook 后查 DataPlanPurchase 应有 eiotclubOrderId 且状态为 ORDERING
  • 等待或 cron 触发;状态推进到 ACTIVEeiotclubPackageEndDate 落库
  • webhook-replayPkgEffective 样例打过来,状态保持 ACTIVE 且第二次返回 duplicate
  • 后台执行 Preview refund,金额非空
  • 后台执行 Refund via EIOTCLUB,状态变 REFUNDED
  • webhook-replay 一个 CardIMEILockedSimCard.statuslocked
  • 用错误签名调 webhook,返回 401