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

432 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设备直接绑定接口联调文档
## 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 管理后端域名:
```text
BASE_URL=https://<traffic-api-domain>
```
4G 后端需要配置并重启后才会生效:
```bash
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. 完整调用时序
```text
Banban 后端 4G 管理后端 4G 小程序
| | |
|-- X-Service-Token + ICCID -------->| |
|<--------- handoffToken ------------| |
| | |
| |<-- wx.login code --------|
| |---- accessToken -------->|
| | |
| |<-- accessToken + token --|
| |---- 完成设备绑定 -------->|
```
Banban 后端申请凭证的接口只能由服务端调用;4G 小程序消费凭证的接口只能由登录后的 `PROGRAM_USER` 调用。
## 4. 接口一:Banban 后端申请一次性 handoffToken
### 4.1 请求地址
```http
POST /internal/v1/handoff-tokens
```
完整地址:
```text
https://<traffic-api-domain>/internal/v1/handoff-tokens
```
必须使用 HTTPS。该接口不使用小程序 JWT,而是使用服务间 Token。
### 4.2 请求头
```http
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 请求体
```json
{
"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 后端服务器或安全的服务端环境执行:
```bash
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`:
```json
{
"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 后端:
```http
POST /api/v1/program/auth/login
```
请求:
```http
Content-Type: application/json
X-Request-Id: program-login-20260922-0001
```
```json
{
"code": "<4G 小程序 wx.login 返回的 code>"
}
```
curl 示例:
```bash
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:
```json
{
"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 请求地址
```http
POST /api/v1/program/bindings/handoff
```
完整地址:
```text
https://<traffic-api-domain>/api/v1/program/bindings/handoff
```
### 5.3 请求头
```http
Authorization: Bearer <PROGRAM_USER_ACCESS_TOKEN>
Content-Type: application/json
X-Request-Id: program-handoff-20260922-0001
```
该接口不要求 `Origin` 白名单。权限来自程序用户 JWT、目标小程序 AppID 校验和一次性 `handoffToken`。
### 5.4 请求体
```json
{
"handoffToken": "<接口一返回的一次性凭证>",
"requestId": "program-handoff-20260922-0001"
}
```
| 字段 | 类型 | 必填 | 约束与说明 |
| --- | --- | --- | --- |
| `handoffToken` | string | 是 | 接口一返回的一次性凭证,最长 256 个字符;不要自行修改或解码 |
| `requestId` | string | 是 | 本次绑定的幂等请求号,最长 64 个字符;网络超时重试时保持不变 |
### 5.5 curl 示例
```bash
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`:
```json
{
"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`:
```json
{
"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 直接绑定(逐步下线)
旧接口地址:
```http
POST /api/v1/program/bindings/by-iccid
```
该接口要求:
- 使用 `PROGRAM_USER` 的 `Authorization: Bearer` Token;
- 携带与页面完全一致的 `Origin`;
- `Origin` 必须存在于 `BINDING_ALLOWED_ORIGINS`;
- 后端配置 `DIRECT_ICCID_BINDING_ENABLED=true`。
当前环境已配置:
```bash
DIRECT_ICCID_BINDING_ENABLED=false
```
因此当前调用该接口会返回 HTTP `410` 和 `DIRECT_BINDING_DISABLED`。只有临时兼容旧版本联调时,才允许在服务端显式开启:
```bash
DIRECT_ICCID_BINDING_ENABLED=true
BINDING_ALLOWED_ORIGINS=https://banban.api.talkingq.com,http://192.168.101.151
```
旧接口请求示例:
```http
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
```
```json
{
"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`。