432 lines
16 KiB
Markdown
432 lines
16 KiB
Markdown
# 设备直接绑定接口联调文档
|
||
|
||
## 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`。
|