Files
banban/docs/设备直接绑定接口联调文档(1).md
T

16 KiB
Raw Blame History

设备直接绑定接口联调文档

1. 适用范围

本文档用于 Banban 小程序与 4G 管理后端之间的“设备无法获取绑定码时直接绑定”联调。

生产环境推荐使用 handoffToken 两段式流程:

  1. Banban 后端使用服务凭证,向 4G 管理后端申请一次性 handoffToken。
  2. 4G 小程序用户完成微信登录后,携带程序用户 Token 和 handoffToken 完成设备绑定。

该流程仍然复用普通绑定码绑定的事务逻辑,成功后会:

  • 创建当前 4G 小程序用户与设备的有效绑定关系;
  • 将 sim_card.binding_status 更新为 bound;
  • 作废该设备尚未使用的绑定验证码;
  • 清理绑定验证码缓存;
  • 写入绑定审计记录。

handoff 申请接口会在服务 Token 校验通过后,按 ICCID 幂等创建或复用 4G 后端的 sim_card 档案。新建档案初始状态为 active、unbound、sync_status=pending;消费 handoffToken 后才会创建用户绑定并将绑定状态更新为 bound。因此 Banban 不需要提前调用 4G 后端的设备注册接口。

该自动建档能力只对已认证的 Banban 服务接口开放,不代表可以通过公开接口任意写入 ICCID。ICCID 格式仍必须合法;如果已有档案状态为非 active,申请会被拒绝。

2. 联调环境与配置

将 <traffic-api-domain> 替换为实际的 4G 管理后端域名:

BASE_URL=https://<traffic-api-domain>

4G 后端需要配置并重启后才会生效:

HANDOFF_ENABLED=true
HANDOFF_TOKEN_TTL=5m
HANDOFF_TOKEN_PEPPER=<独立的高强度随机值>
BANBAN_HANDOFF_SERVICE_TOKEN=<独立的高强度随机值>
WECHAT_APP_ID=<4G 小程序 AppID>
DIRECT_ICCID_BINDING_ENABLED=false

说明:

  • BANBAN_HANDOFF_SERVICE_TOKEN 只允许保存在 Banban 后端服务端,不能下发到小程序或前端页面。
  • HANDOFF_TOKEN_PEPPER 只允许保存在 4G 后端服务端,不能与服务 Token 共用。
  • HANDOFF_TOKEN_TTL=5m 表示凭证默认有效 5 分钟,具体有效期以申请接口返回的 expiresIn 为准。
  • 当前环境的 BINDING_ALLOWED_ORIGINS 可以继续保留 https://banban.api.talkingq.com,http://192.168.101.151,但 handoff 流程不依赖 Origin 证明设备归属。

3. 完整调用时序

Banban 后端                         4G 管理后端                 4G 小程序
    |                                    |                         |
    |-- X-Service-Token + ICCID -------->|                         |
    |<--------- handoffToken ------------|                         |
    |                                    |                         |
    |                                    |<-- wx.login code --------|
    |                                    |---- accessToken -------->|
    |                                    |                         |
    |                                    |<-- accessToken + token --|
    |                                    |---- 完成设备绑定 -------->|

Banban 后端申请凭证的接口只能由服务端调用;4G 小程序消费凭证的接口只能由登录后的 PROGRAM_USER 调用。

4. 接口一:Banban 后端申请一次性 handoffToken

4.1 请求地址

POST /internal/v1/handoff-tokens

完整地址:

https://<traffic-api-domain>/internal/v1/handoff-tokens

必须使用 HTTPS。该接口不使用小程序 JWT,而是使用服务间 Token。

4.2 请求头

X-Service-Token: <BANBAN_HANDOFF_SERVICE_TOKEN>
Content-Type: application/json
X-Request-Id: banban-handoff-20260922-0001

X-Service-Token 的值必须与 4G 后端环境变量 BANBAN_HANDOFF_SERVICE_TOKEN 完全一致。

X-Request-Id 仅用于 HTTP 请求追踪,建议使用 8 至 64 位的字母、数字、点、下划线或短横线。真正的业务幂等键是请求体中的 requestId。

4.3 请求体

{
  "iccid": "898604E91123D1334901",
  "sourceBindingId": "banban-binding-100001",
  "requestId": "banban-handoff-20260922-0001"
}
字段 类型 必填 约束与说明
iccid string 是 18 至 22 位字母或数字,后端会统一转为大写;已登记的 active 档案会复用,未登记时由 4G 后端自动建档
sourceBindingId string 是 Banban 侧绑定关系的稳定唯一 ID,最长 128 个字符;同一绑定关系后续重试必须保持不变
requestId string 是 本次申请的幂等请求号,最长 64 个字符;网络超时重试必须保持不变

sourceBindingId 不要使用每次随机生成的值,否则无法实现申请接口幂等。requestId 建议由 Banban 后端生成,例如 banban-handoff-20260922-0001。

4.4 curl 示例

以下命令只能在 Banban 后端服务器或安全的服务端环境执行:

export BASE_URL="https://<traffic-api-domain>"
export BANBAN_HANDOFF_SERVICE_TOKEN="<服务端密钥>"

curl -i -sS -X POST "$BASE_URL/internal/v1/handoff-tokens" \
  -H "X-Service-Token: $BANBAN_HANDOFF_SERVICE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-Id: banban-handoff-20260922-0001" \
  --data '{
    "iccid": "898604E91123D1334901",
    "sourceBindingId": "banban-binding-100001",
    "requestId": "banban-handoff-20260922-0001"
  }'

4.5 成功响应

HTTP 状态码为 200:

{
  "code": "OK",
  "message": "success",
  "data": {
    "targetAppId": "<4G 小程序 AppID>",
    "handoffToken": "<一次性凭证>",
    "expiresIn": 300
  },
  "requestId": "banban-handoff-20260922-0001",
  "timestamp": "2026-09-22T03:30:00Z"
}
字段 说明
data.targetAppId 允许消费该凭证的目标小程序 AppID,必须与 4G 小程序登录身份一致
data.handoffToken 一次性绑定凭证,只能传给 4G 小程序的绑定接口,不要写入日志或返回给不可信客户端
data.expiresIn 剩余有效秒数,消费时以服务端校验结果为准

相同的 sourceBindingId + requestId 重试时:

  • 凭证仍为 active 且未过期:返回同一凭证,不会重复创建;
  • 对应 ICCID 发生变化:返回 IDEMPOTENCY_CONFLICT;
  • 凭证已消费:返回 HANDOFF_TOKEN_USED;
  • 凭证已过期:返回 HANDOFF_TOKEN_EXPIRED。

5. 接口二:4G 小程序消费 handoffToken 完成绑定

5.1 先获取 4G 小程序 accessToken

4G 小程序先调用微信 wx.login(),再把返回的临时 code 交给 4G 后端:

POST /api/v1/program/auth/login

请求:

Content-Type: application/json
X-Request-Id: program-login-20260922-0001
{
  "code": "<4G 小程序 wx.login 返回的 code>"
}

curl 示例:

curl -i -sS -X POST "$BASE_URL/api/v1/program/auth/login" \
  -H "Content-Type: application/json" \
  -H "X-Request-Id: program-login-20260922-0001" \
  --data '{"code":"<4G 小程序 wx.login 返回的 code>"}'

成功响应中的 data.accessToken 是后续绑定接口需要的程序用户 Token:

{
  "code": "OK",
  "message": "success",
  "data": {
    "accessToken": "<PROGRAM_USER_ACCESS_TOKEN>",
    "expiresAt": "2026-09-22T15:30:00Z",
    "user": {
      "userId": 10001,
      "status": "active"
    }
  },
  "requestId": "program-login-20260922-0001",
  "timestamp": "2026-09-22T03:31:00Z"
}

必须使用 4G 小程序自己的 wx.login() code,不能使用 Banban 小程序的登录 code,也不能使用 SUPER_ADMIN、FACTORY_PROVISIONER 或设备 Token。

5.2 请求地址

POST /api/v1/program/bindings/handoff

完整地址:

https://<traffic-api-domain>/api/v1/program/bindings/handoff

5.3 请求头

Authorization: Bearer <PROGRAM_USER_ACCESS_TOKEN>
Content-Type: application/json
X-Request-Id: program-handoff-20260922-0001

该接口不要求 Origin 白名单。权限来自程序用户 JWT、目标小程序 AppID 校验和一次性 handoffToken。

5.4 请求体

{
  "handoffToken": "<接口一返回的一次性凭证>",
  "requestId": "program-handoff-20260922-0001"
}
字段 类型 必填 约束与说明
handoffToken string 是 接口一返回的一次性凭证,最长 256 个字符;不要自行修改或解码
requestId string 是 本次绑定的幂等请求号,最长 64 个字符;网络超时重试时保持不变

5.5 curl 示例

export PROGRAM_ACCESS_TOKEN="<4G 小程序登录返回的 accessToken>"
export HANDOFF_TOKEN="<接口一返回的 handoffToken>"

curl -i -sS -X POST "$BASE_URL/api/v1/program/bindings/handoff" \
  -H "Authorization: Bearer $PROGRAM_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-Id: program-handoff-20260922-0001" \
  --data "{\"handoffToken\":\"$HANDOFF_TOKEN\",\"requestId\":\"program-handoff-20260922-0001\"}"

5.6 成功响应

HTTP 状态码为 200:

{
  "code": "OK",
  "message": "success",
  "data": {
    "iccid": "898604E91123D1334901",
    "alreadyBound": false
  },
  "requestId": "program-handoff-20260922-0001",
  "timestamp": "2026-09-22T03:32:00Z"
}
字段 说明
data.iccid 实际绑定成功的设备 ICCID
data.alreadyBound false 表示本次新建绑定;true 表示重复请求返回已有绑定结果

绑定、作废旧验证码、更新 SIM 绑定状态和审计记录在同一个数据库事务中完成。接口成功后,可以调用 GET /api/v1/program/devices 验证当前用户的设备列表。

6. 幂等与重试规则

6.1 申请凭证重试

Banban 后端申请接口超时或未收到响应时,使用相同的 sourceBindingId 和 requestId 重试,不要重新生成请求号。这样可以复用原有有效凭证,避免创建多条 handoff 记录。

6.2 消费凭证重试

4G 小程序消费接口超时或网络断开时,使用相同的 handoffToken 和 requestId 重试:

  • 如果事务已经成功提交,通常返回 alreadyBound=true;
  • 如果凭证已被其他用户消费,返回 DEVICE_ALREADY_BOUND 或 HANDOFF_TOKEN_USED;
  • 不要因为超时就重新向 Banban 申请新的 sourceBindingId,应先使用原请求参数重试或查询绑定结果。

6.3 凭证安全规则

  • handoffToken 只能在服务端申请、在 4G 小程序绑定流程中消费一次;
  • 不要把 BANBAN_HANDOFF_SERVICE_TOKEN 放在小程序、浏览器代码、页面参数或日志中;
  • 不要把完整 handoffToken 写入业务日志、埋点或群聊;
  • handoffToken 过期、篡改或消费后不可恢复,需按原绑定关系重新申请。

7. 错误码与处理建议

统一错误响应结构如下,data 为 null:

{
  "code": "HANDOFF_TOKEN_EXPIRED",
  "message": "跨系统绑定凭证已过期",
  "data": null,
  "requestId": "program-handoff-20260922-0001",
  "timestamp": "2026-09-22T03:40:00Z"
}

7.1 申请凭证接口

HTTP 状态 code 含义与处理
400 INVALID_REQUEST ICCID、sourceBindingId 或 requestId 格式不符合约束
401 HANDOFF_SERVICE_UNAUTHORIZED 缺少或错误的 X-Service-Token;确认服务端密钥配置一致
403 DEVICE_DISABLED 设备状态不是 active;先恢复设备状态
409 IDEMPOTENCY_CONFLICT 相同幂等键对应了不同 ICCID;检查 sourceBindingId 和 requestId
409 HANDOFF_TOKEN_USED 原凭证已经消费;按当前绑定结果处理或重新发起新业务请求
410 HANDOFF_TOKEN_EXPIRED 原凭证已过期;使用相同业务关系重新申请凭证
429 RATE_LIMITED 触发申请频率限制;降低重试频率
503 HANDOFF_DISABLED 后端未启用 handoff 流程;检查 HANDOFF_ENABLED
503 CONFIGURATION_ERROR handoff 密钥、4G AppID 或 TTL 配置缺失/不合法
500 INTERNAL_ERROR 自动建档后无法读取 SIM 档案;需要联系后端排查数据库或事务状态

7.2 消费凭证接口

HTTP 状态 code 含义与处理
400 INVALID_REQUEST 请求体字段缺失或超过长度限制
400 HANDOFF_TOKEN_INVALID 凭证不存在、被篡改或已失效
401 UNAUTHORIZED 未登录、Token 过期或 Token 不是程序用户 Token
403 HANDOFF_TARGET_APP_MISMATCH 当前登录的小程序不是接口一返回的 targetAppId
403 DEVICE_DISABLED 设备当前被禁用
409 HANDOFF_TOKEN_USED 凭证已使用且无法匹配当前用户的重试
409 DEVICE_ALREADY_BOUND 设备已经绑定其他程序用户
409 IDEMPOTENCY_CONFLICT 相同 requestId 对应了不同设备或不同绑定操作
410 HANDOFF_TOKEN_EXPIRED 凭证已经过期
429 RATE_LIMITED 用户、IP 或凭证消费频率过高
503 HANDOFF_DISABLED 后端未启用 handoff 流程

8. 兼容接口:裸 ICCID 直接绑定(逐步下线)

旧接口地址:

POST /api/v1/program/bindings/by-iccid

该接口要求:

  • 使用 PROGRAM_USER 的 Authorization: Bearer Token;
  • 携带与页面完全一致的 Origin;
  • Origin 必须存在于 BINDING_ALLOWED_ORIGINS;
  • 后端配置 DIRECT_ICCID_BINDING_ENABLED=true。

当前环境已配置:

DIRECT_ICCID_BINDING_ENABLED=false

因此当前调用该接口会返回 HTTP 410 和 DIRECT_BINDING_DISABLED。只有临时兼容旧版本联调时,才允许在服务端显式开启:

DIRECT_ICCID_BINDING_ENABLED=true
BINDING_ALLOWED_ORIGINS=https://banban.api.talkingq.com,http://192.168.101.151

旧接口请求示例:

POST /api/v1/program/bindings/by-iccid
Origin: https://banban.api.talkingq.com
Authorization: Bearer <PROGRAM_USER_ACCESS_TOKEN>
Content-Type: application/json
X-Request-Id: direct-bind-20260922-0001
{
  "iccid": "898604E91123D1334901",
  "requestId": "direct-bind-20260922-0001"
}

该接口无法证明 Banban 后端实际拥有该 ICCID,不应作为新的跨系统正式方案。新接入方必须使用本文档第 4、5 节的 handoffToken 流程。

9. 联调验收清单

建议按以下顺序验收:

  1. 使用尚未登记的测试 ICCID,申请接口返回 code=OK、targetAppId、handoffToken 和正数 expiresIn。
  2. 查询数据库,确认该 ICCID 已自动写入 sim_card,初始为 status=active、binding_status=unbound、sync_status=pending。
  3. 使用 4G 小程序的 wx.login() code 登录,确认返回 PROGRAM_USER 的 accessToken。
  4. 使用该 accessToken 消费 handoffToken,返回 code=OK、正确 ICCID、alreadyBound=false。
  5. 查询 GET /api/v1/program/devices,确认设备出现在当前 4G 小程序用户列表中。
  6. 使用相同 sourceBindingId + requestId 重试申请接口,确认不会重复创建 SIM 档案或凭证。
  7. 使用相同用户、相同 handoffToken 和相同绑定 requestId 重试消费接口,确认返回幂等结果或可确认已绑定。
  8. 绑定成功后调用流量详情接口,确认设备可以进入 pending 或同步后的流量状态,而不是 DEVICE_NOT_FOUND。
  9. 使用其他小程序身份消费,确认返回 HANDOFF_TARGET_APP_MISMATCH。
  10. 使用过期或篡改后的凭证,确认分别返回 HANDOFF_TOKEN_EXPIRED 或 HANDOFF_TOKEN_INVALID。
  11. 使用已绑定到其他用户的设备,确认返回 DEVICE_ALREADY_BOUND,不会覆盖原绑定关系。
  12. 确认数据库中该设备的 sim_card.binding_status 为 bound,且 user_device_binding.status 为 active。
  13. 确认旧 /bindings/by-iccid 接口在 DIRECT_ICCID_BINDING_ENABLED=false 时返回 DIRECT_BINDING_DISABLED。

10. 联调排查信息

请保留以下信息用于后端排查,但不要提交 Token 或完整 ICCID:

  • 请求路径和 HTTP 状态码;
  • 响应中的 code、message、requestId;
  • 请求头中的 X-Request-Id;
  • Banban 的 sourceBindingId 和脱敏后的 ICCID;
  • 调用时间(含时区);
  • 是否使用了相同幂等参数重试。

不要在工单、群聊、代码仓库或日志中粘贴 X-Service-Token、PROGRAM_USER_ACCESS_TOKEN 或完整 handoffToken。