EIOTCLUB Integration Handoff
接入概览
EIOTCLUB 接入已经从基础设施、数据库扩展、只读同步、履约闭环、退款预览与执行、回调处理到用户侧可见性全部串起来了。现在本地系统可以从 EIOTCLUB 拉取卡片、套餐、余额和订单状态,也可以接收 EIOTCLUB webhook 推进 DataPlanPurchase 与 SimCard 状态,并在后台和用户侧看到同步后的结果。
环境变量
| Variable | 用途 | 缺失行为 |
|---|---|---|
EIOTCLUB_APP_KEY | EIOTCLUB API 请求签名所需的 appkey | 未配置时 postSigned() 返回 null,脚本打印 EIOTCLUB not configured |
EIOTCLUB_SECRET | EIOTCLUB API 请求签名所需 secret | 同上 |
EIOTCLUB_BASE_URL | EIOTCLUB API Base URL,默认 https://oapi.eiotclub.com | 未配置时使用默认值 |
EIOTCLUB_WEBHOOK_SECRET | EIOTCLUB 回调验签 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 / CloudESimFlowAlert | flow_warning | 更新 SimCard.remainFlowMb,不发邮件 |
SubPkgList / CloudESimSubPkgList | order_detail | 按 eiotclubOrderId 找购买记录,推进到 ORDERING,落 eiotclubPackageEndDate |
PkgEffective / CloudESimPkgActivate | package_activated | 购买记录推进 ACTIVE,写 activatedAt、eiotclubPackageEndDate、expiresAt |
Refund / CloudESimRefund | refund | 购买记录推进 REFUNDED,幂等处理重复投递 |
PkgQuantityList / CloudESimPkgDeactivate | usage_exhausted | 购买记录推进 EXPIRED,同步 SimCard.planExpiry |
CardStopped / CloudESimCardStopped | card_offline | SimCard.status = "offline" |
SwitchProduct / CloudESimSwitchProduct | product_switched | 更新 SimCard.packageCode / packageName / packageType |
CardIMEILocked | card_locked | SimCard.status = "locked" |
IMEIUnLock | card_unlocked | SimCard.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-cards的Sync 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-cards点Sync all (Dry run)看到scanned / updated / skipped - 取一张真卡 ICCID,本地 DB upsert 占位行;点
Sync from EIOTCLUB,字段被填 - 用 Stripe test card 走一次
/data-plans购买;webhook 后查DataPlanPurchase应有eiotclubOrderId且状态为ORDERING - 等待或 cron 触发;状态推进到
ACTIVE,eiotclubPackageEndDate落库 -
webhook-replay把PkgEffective样例打过来,状态保持ACTIVE且第二次返回duplicate - 后台执行
Preview refund,金额非空 - 后台执行
Refund via EIOTCLUB,状态变REFUNDED -
webhook-replay一个CardIMEILocked,SimCard.status变locked - 用错误签名调 webhook,返回
401