feat(binding): verify 4G relations before Banban unbind
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# Banban 与 4G 小程序流量关联、联合解绑联调说明
|
||||
|
||||
当前契约以 [4G 后端接口文档 (2)](./设备直接绑定接口联调文档(2).md) 为准。本文描述 Banban 后端、Banban 小程序及 `/Users/stu2not/4g-miniprogram` 的本地实现;第 10 节为此前版本的线上操作记录,不表示新版关系查询/关闭流程已部署。
|
||||
|
||||
## 1. 业务规则
|
||||
|
||||
- Banban NFC 绑定成功即可正常使用设备;4G 关联允许失败、取消或稍后处理。
|
||||
- 管理页查询原 4G 关系,只有后端确认关联有效才显示查询、充值入口。
|
||||
- 用户确认联合解绑后,持久化解绑操作,阻止此绑定实例继续申请 handoff。
|
||||
- Banban 必须调用 4G 关系关闭接口,并取得 `canUnbind=true` 才能解除本地设备绑定。
|
||||
- 4G 已在其他入口解绑时,可直接确认后完成 Banban 解绑,不必再次跳转小程序。
|
||||
- `unknown`、403、404、网络失败和不存在记录均不能作为已解绑依据。
|
||||
- 不把客户端回报或当前用户设备列表中存在同一 ICCID 当作原关系有效的证明。
|
||||
|
||||
## 2. 身份与绑定实例
|
||||
|
||||
| 小程序 | AppID |
|
||||
| --- | --- |
|
||||
| Banban | `wx29ce99ce96b56cd9` |
|
||||
| 4G 设备管理 | `wx56e1cfc52aab630f` |
|
||||
|
||||
两个小程序分别登录。服务密钥仅供后端使用,不进入小程序、页面参数或日志。开发版/体验版跳转目标体验版,正式版跳转目标正式版,两侧需协调发布。
|
||||
|
||||
设备每次重新建立 Banban 绑定时生成新的 `device_bindings.binding_instance_id`。加卡、关联宝贝保持原实例。原有 `sourceBindingId=banban:{binding_instance_id}` 保持稳定,查询/关闭路径对整个 ID 做 URL 编码,包括冒号;不能为旧关系换 ID 绕过关闭状态。
|
||||
|
||||
## 3. ICCID 来源与保存
|
||||
|
||||
设备成功的 `006` 回复:
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": "006",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"uuid": "53D22B6DA20001",
|
||||
"iccid": "898604E91123D1334901"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ICCID 为 18~22 位字母或数字,归一化为大写,持久化到 `device_settings.iccid`。准备操作时保存到操作表作为快照。不能从 IMEI 推导;设备换卡或其他设备占用同一 ICCID 时不覆盖原记录。
|
||||
|
||||
保存最多等待 2 秒;缺失、非法或保存失败不影响 NFC 绑定。日志记录 `006 ICCID present=..., valid=...`,保存成功记录 `006 ICCID persisted`,不打印完整 ICCID。当前仅接入 `006`,没有替固件实现上报或为历史设备自动回填。
|
||||
|
||||
## 4. Banban 接口与状态
|
||||
|
||||
所有接口使用 Banban 用户登录态,并校验设备归属账号。家庭成员不能执行关联、确认或解绑。
|
||||
|
||||
| 方法与路径 | 用途 |
|
||||
| --- | --- |
|
||||
| `GET /banban/joint-bindings/devices/{device_id}` | 本地当前绑定实例的操作概况 |
|
||||
| `POST /banban/joint-bindings` | 创建/复用 `{device_id,action:bind/unbind}` 操作 |
|
||||
| `GET /banban/joint-bindings/{operation_id}` | 读取本地操作 |
|
||||
| `POST /banban/joint-bindings/{operation_id}/check` | 向 4G 查询原关系并更新本地确认状态 |
|
||||
| `POST /banban/joint-bindings/{operation_id}/prepare` | 绑定申请 handoff;解绑先关闭关系,必要时返回 4G 跳转信息 |
|
||||
| `POST /banban/joint-bindings/{operation_id}/result` | 接收客户端结果;成功仍需服务端查询确认 |
|
||||
| `POST /banban/joint-bindings/{operation_id}/finish-unbind` | 重新确认关闭结果后,事务性完成 Banban 解绑 |
|
||||
| `GET /banban/joint-bindings/devices/{device_id}/portal` | 查询原关系后返回查询/充值入口 |
|
||||
|
||||
原 `DELETE /banban/bindings/{device_id}` 返回 409,引导联合解绑。旧客户端需要更新才能使用新解绑流程。
|
||||
|
||||
操作的 `status=pending/partial_failed/completed` 不代表 Banban 是否已绑定。新增 `traffic_status=server_confirmed` 表示 4G 服务端关系查询/关闭已确认;旧的 `client_confirmed` 只代表客户端回报,不能授权解绑。字段已为 VARCHAR,无需为本次新增状态再加列。
|
||||
|
||||
绑定确认要求原关系为 `bound + open`。解绑确认要求 4G 明确返回 `canUnbind=true`,同时校验 `closing`、无待处理凭证、不再接受 handoff、状态为 `unbound/never_bound`。响应必须 HTTP 200、业务码 OK、sourceBindingId 匹配,布尔字段必须是布尔值,checkedAt 必须是合法 UTC 时间。
|
||||
|
||||
## 5. 流量关联流程
|
||||
|
||||
1. Banban NFC 绑定独立完成,用户自愿继续关联。
|
||||
2. 后端使用服务凭证向 `POST /internal/v1/handoff-tokens` 申请凭证;sourceBindingId 始终对应同一 ICCID。
|
||||
3. 4G 小程序使用自身登录态调用 `POST /api/v1/program/bindings/handoff`;原消费请求重试必须沿用 token 与业务 requestId。
|
||||
4. 客户端返回经过字段匹配的结果,Banban 再使用 GET 关系接口确认原绑定,确认后显示查询/充值入口。
|
||||
5. 申请返回 `HANDOFF_TOKEN_USED` 时,Banban 直接查询原关系;只有原绑定仍有效才恢复成功,不再跳转后按当前 ICCID 设备列表猜测。
|
||||
6. 4G 小程序即使保存过成功结果,再次提交也让 4G 后端校验原 token,防止旧回执关联到后来的其他绑定。旧 recoveryOnly 入口提示返回 Banban 确认。
|
||||
|
||||
关联失败不回滚 Banban 绑定。管理页刷新和打开查询/充值入口都会检查原关系,4G 侧独立解绑后不会继续把旧缓存当作有效关联。
|
||||
|
||||
## 6. 联合解绑流程
|
||||
|
||||
1. 用户确认后创建持久化 `action=unbind` 操作;此记录阻止同实例再申请 handoff。
|
||||
2. 用户点击继续,调用 `POST /internal/v1/binding-relations/{sourceBindingId}/close`,业务请求号为 `bb-close-{operation_id}`。
|
||||
3. 返回 `canUnbind=true`:无需跳转,直接执行本地解绑。此判断仅依赖原关系,ICCID 缺失也不会挡住已经得到的服务端确认。
|
||||
4. 返回 `bound`:保留 Banban 绑定,携带原 ICCID 跳转 4G 小程序,用户用自己的 4G 登录态调用现有 unbind 接口。
|
||||
5. 用户返回、重新打开页面或在 4G 其他入口完成解绑后,可以主动查询/继续处理;最终提交前再次调用幂等 close,只有确认放行才执行本地事务。
|
||||
6. 本地设备解绑、家庭关联停用、操作完成在同一事务内提交。失败保留已确认状态,重试仍检查原关系;已完成操作可幂等返回。
|
||||
7. 未知关系、鉴权错误、限流、网络异常均保留绑定,不执行自动猜测或无限重试。
|
||||
|
||||
| 4G 状态 | 处理 |
|
||||
| --- | --- |
|
||||
| 原绑定仍有效 | 保留 Banban,前往 4G 解绑 |
|
||||
| 原绑定已解除 | 关闭后确认 canUnbind=true,再解绑 Banban |
|
||||
| 有记录证明从未完成绑定 | 撤销待消费凭证并确认放行 |
|
||||
| 没有关系记录或历史不明确 | unknown,不自动放行,需核查 |
|
||||
|
||||
关闭不会自动解除 4G 用户绑定或删除套餐。它会持续禁止该 sourceBindingId 签发/消费 handoff,文档没有取消关闭接口。页面明确提示继续后不能恢复本次关联;“稍后处理”只是离开页面,不恢复已关闭关系。
|
||||
|
||||
4G 设备详情页对本机留有 Banban 标记的设备引导回 Banban;清缓存/换手机无法依靠该本地标记拦截所有独立解绑,服务端关系查询负责后续确认。
|
||||
|
||||
## 7. 有限等待与并发
|
||||
|
||||
| 环节 | 上限 |
|
||||
| --- | --- |
|
||||
| 006 保存 ICCID | 2 秒,失败继续 NFC 绑定 |
|
||||
| Banban 新业务接口 | 8 秒总期限 |
|
||||
| Banban 调用 4G | 默认 6 秒网络超时,外层受 8 秒期限约束 |
|
||||
| Banban 小程序 HTTP | 10 秒,平台超时及独立计时器 |
|
||||
| 4G 微信登录 | 10 秒 |
|
||||
| 4G 每次 HTTP | 15 秒,超时终止请求 |
|
||||
| 4G 单次操作 | 45 秒 |
|
||||
| 页面及跨小程序跳转 | 15 秒 |
|
||||
|
||||
无自动无限循环或长期轮询,失败后用户主动重试。关闭按 sourceBindingId 幂等;超时不换绑定实例或业务请求号。
|
||||
|
||||
Banban 不跨网络持有数据库行锁:先读取、关闭事务,再请求 4G,返回后重新校验当前设备所有者和绑定实例。最终事务再次校验操作。4G 端关闭与签发/消费的互斥依赖新版后端的关系锁;本地模拟测试不能替代这部分 MySQL 并发验收。
|
||||
|
||||
## 8. 配置与数据库
|
||||
|
||||
```dotenv
|
||||
TRAFFIC_4G_API_BASE_URL=https://role.api.talkingq.com/traffic-4g
|
||||
BANBAN_HANDOFF_SERVICE_TOKEN=<两侧一致的服务端密钥>
|
||||
TRAFFIC_4G_TARGET_APP_ID=wx56e1cfc52aab630f
|
||||
TRAFFIC_4G_TIMEOUT_SECONDS=6
|
||||
```
|
||||
|
||||
查询和关闭沿用现有密钥。必须先部署 4G 新 JAR、V13 迁移及 Nginx 新路由;仅替换 JAR 不保证公网接口可用。历史关系无法准确回填时保持 unknown。
|
||||
|
||||
Banban 首次安装本功能:已有库执行一次 `talkingq-url/mysql/migrations/20260923_joint_binding.sql`;新库使用 `mysql/init/02-init.sql` 与 `03-joint-binding.sql`。已执行迁移的生产库不要重跑 ADD COLUMN。本次查询/关闭接入不需要新增 Banban 数据库迁移。
|
||||
|
||||
## 9. 本地验证与发布
|
||||
|
||||
```bash
|
||||
# talkingq-url(既有告警测试断言此测试地址,不需要真实 .env)
|
||||
SERVER_HOST=192.168.101.78 .venv/bin/python -m pytest tests -q
|
||||
# banban-mini
|
||||
npm run test:ui
|
||||
npm run build:weapp
|
||||
# 4g-miniprogram
|
||||
node --test tests/*.test.cjs
|
||||
```
|
||||
|
||||
覆盖:原关系解绑、从未绑定、未知关系、旧客户端成功回执、原关系与后来 ICCID 绑定区别、关闭失败重试、事务回滚、重绑后的旧响应、格式/鉴权/限流/超时、页面恢复及跳转计时。
|
||||
|
||||
状态事务测试使用 SQLite 适配,HTTP 使用模拟传输;没有访问生产 4G 接口或验证真实 MySQL 锁竞争。微信登录、真机跨小程序返回、V13 历史回填和真实关闭/消费并发需部署到联调环境后验收。伴伴构建可执行,全量 TypeScript 检查有既有类型错误,不将构建通过等同于全量类型检查通过。
|
||||
|
||||
2026-09-23 本次提交内容在独立目录验证:后端 142 项通过,Banban 小程序 30 项通过且微信小程序构建成功,4G 小程序 16 项通过且全部页面脚本/JSON 校验通过。全量 TypeScript 检查的 6 项错误与修改前 HEAD 一致。本次只做本地实施、测试与 Git 交付,不部署新版服务或上传小程序。
|
||||
|
||||
## 10. 2026-09-23 服务器更新记录
|
||||
|
||||
按用户授权,于北京时间 14:21 完成 Banban 后端更新,范围为本功能相关的 8 个 Python 文件和已有库迁移脚本。未上传两个小程序,未配置 4G 服务密钥。
|
||||
|
||||
服务器:`101.35.224.118`,应用目录:`/home/ubuntu/banban-backend`。
|
||||
|
||||
备份目录:`/home/ubuntu/banban-backups/joint-binding-20260923-1411`,仅 root 可读:
|
||||
|
||||
- `code-and-config.tar.gz`:更新前代码与配置,排除虚拟环境、资源、运行数据和日志。
|
||||
- `binding-tables.sql.gz`:`device_settings`、`device_bindings`、`device_family_members`、`device_bind_history` 的一致性快照。
|
||||
- `replaced-files/`、`manifest-before.json`、`manifest-after.json`:被替换文件和更新前后 SHA-256 清单。
|
||||
|
||||
迁移后四表记录数仍为 63、22、33、144;22 条绑定均生成互不重复的绑定实例 UUID。新操作表为空,未为了验证而创建或解除真实绑定。
|
||||
|
||||
更新后验证:进程 `511137` 启动成功,MQTT 重新连接;7 个新接口路径已注册;设备所有者状态查询与原绑定查询均为 HTTP 200;未关联设备的查询/充值跳转接口返回预期 409;未登录访问返回 401;公网 HTTPS OpenAPI 和绑定提示音均为 200。
|
||||
|
||||
截至 14:22,`TalkingQ_ZHWK00003111` 的 ICCID 仍为空,尚无更新后的 `006 ICCID` 检测日志,需要设备重新上报。`BANBAN_HANDOFF_SERVICE_TOKEN` 仍未配置,申请 4G 凭证暂不可用。
|
||||
|
||||
14:25 按用户提供的密钥配置 `BANBAN_HANDOFF_SERVICE_TOKEN`。更新前 `.env` 备份于 `/home/ubuntu/banban-backups/handoff-token-20260923-1424/env.before`;其他配置项保持原值,现有 `.env` 权限收紧为 600。独立进程校验有效配置与提供值一致,重启后进程为 `512483`,HTTPS OpenAPI 为 200,MQTT 连接正常。密钥值不记录到文档或仓库。
|
||||
|
||||
4G 申请接口空请求分别在无密钥、有密钥时均返回 400 `INVALID_REQUEST`,说明该检查无法证明密钥鉴权成功;未提交有效 ICCID 或创建交接凭证,实际关联仍待正常业务请求验证。
|
||||
@@ -0,0 +1,564 @@
|
||||
# 设备直接绑定接口联调文档
|
||||
|
||||
## 1. 适用范围
|
||||
|
||||
本文档用于 Banban 小程序与 4G 管理后端之间的“设备无法获取绑定码时直接绑定”联调。
|
||||
|
||||
2026-09-23 更新:新增按 `sourceBindingId` 查询原绑定关系、关闭 handoff 关联的服务端接口,见第 11、12 节。伴伴解绑必须以 `canUnbind=true` 为放行条件,不能只看 ICCID 当前状态或把查不到当成已经解绑。
|
||||
|
||||
生产环境推荐使用 `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. 联调环境与配置
|
||||
|
||||
当前生产环境通过 Nginx 的 `/traffic-4g` 前缀访问,以下 `BASE_URL` 必须包含此前缀:
|
||||
|
||||
```text
|
||||
BASE_URL=https://role.api.talkingq.com/traffic-4g
|
||||
```
|
||||
|
||||
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 证明设备归属。
|
||||
- 新增关系查询、关闭接口继续使用 `BANBAN_HANDOFF_SERVICE_TOKEN`,不需要新增密钥或修改 `.env`。即使 `HANDOFF_ENABLED=false`,仍可使用正确服务凭证查询、关闭已有关系。
|
||||
|
||||
## 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://role.api.talkingq.com/traffic-4g/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 个字符;同一绑定关系保持不变,且必须始终对应同一 ICCID。新接入使用 UUID 或字母、数字、短横线、下划线,便于作为查询路径参数 |
|
||||
| `requestId` | string | 是 | 本次申请的幂等请求号,最长 64 个字符;网络超时重试必须保持不变 |
|
||||
|
||||
`sourceBindingId` 不要使用每次随机生成的值,否则无法实现申请接口幂等。`requestId` 建议由 Banban 后端生成,例如 `banban-handoff-20260922-0001`。
|
||||
|
||||
即使换了 `requestId`,同一 `sourceBindingId` 也不能切换 ICCID,否则返回 `IDEMPOTENCY_CONFLICT`。伴伴关系进入 `closing` 后不得再申请或消费 handoff;完成解绑并重新建立伴伴绑定时,必须使用新关系的唯一 ID,不能复用旧 ID 绕过关闭状态。
|
||||
|
||||
### 4.4 curl 示例
|
||||
|
||||
以下命令只能在 Banban 后端服务器或安全的服务端环境执行:
|
||||
|
||||
```bash
|
||||
export BASE_URL="https://role.api.talkingq.com/traffic-4g"
|
||||
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://role.api.talkingq.com/traffic-4g/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 绑定状态、记录 handoff 对应的具体绑定记录、消费凭证和审计记录在同一个数据库事务中完成。即使设备原本已绑定当前用户,也会记录复用的绑定记录 ID。接口成功后,可以调用 `GET /api/v1/program/devices` 验证当前用户的设备列表。
|
||||
|
||||
## 6. 幂等与重试规则
|
||||
|
||||
### 6.1 申请凭证重试
|
||||
|
||||
Banban 后端申请接口超时或未收到响应时,使用相同的 `sourceBindingId` 和 `requestId` 重试,不要重新生成请求号。这样可以复用原有有效凭证,避免创建多条 handoff 记录。
|
||||
|
||||
### 6.2 消费凭证重试
|
||||
|
||||
4G 小程序消费接口超时或网络断开时,使用相同的 `handoffToken` 和 `requestId` 重试:
|
||||
|
||||
- 如果事务已经成功提交,原绑定仍有效且伴伴关系未关闭,原消费用户重试返回 `alreadyBound=true`;
|
||||
- 如果凭证已被其他用户消费,或对应的原绑定已解绑,返回 `HANDOFF_TOKEN_USED`;设备后来重新绑定给同一用户,也不能复用旧凭证;
|
||||
- 如果伴伴已关闭此关系,返回 `HANDOFF_RELATION_CLOSED`;
|
||||
- 不要因为超时就重新向 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` | 原凭证已经消费;按当前绑定结果处理或重新发起新业务请求 |
|
||||
| 409 | `HANDOFF_RELATION_CLOSED` | 此伴伴关系已停止新关联;不能重新申请凭证 |
|
||||
| 409 | `HANDOFF_RELATION_UNKNOWN` | 历史记录无法准确关联,需要核查,不能绕过检查重新申请 |
|
||||
| 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` 对应了不同设备或不同绑定操作 |
|
||||
| 409 | `HANDOFF_RELATION_CLOSED` | 伴伴关系已关闭;已有未消费凭证不能再绑定 |
|
||||
| 409 | `HANDOFF_RELATION_UNKNOWN` | 无法确认此凭证对应的原关系,需联系后端核查 |
|
||||
| 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`。
|
||||
|
||||
## 11. 接口三:查询这次伴伴绑定对应的 4G 关系
|
||||
|
||||
```http
|
||||
GET /internal/v1/binding-relations/{sourceBindingId}
|
||||
X-Service-Token: <BANBAN_HANDOFF_SERVICE_TOKEN>
|
||||
```
|
||||
|
||||
查询的是 `sourceBindingId` 实际关联的具体 4G 绑定记录。原绑定解绑后,即使同一 ICCID 后来被其他用户或原用户通过其他流程重新绑定,原关系仍返回 `unbound`。若未关闭的同一伴伴关系又成功消费了新 handoff,则查询追踪这次新关联。
|
||||
|
||||
```bash
|
||||
export BASE_URL="https://role.api.talkingq.com/traffic-4g"
|
||||
export SOURCE_BINDING_ID="banban-binding-100001"
|
||||
|
||||
curl -i -sS "$BASE_URL/internal/v1/binding-relations/$SOURCE_BINDING_ID" \
|
||||
-H "X-Service-Token: $BANBAN_HANDOFF_SERVICE_TOKEN" \
|
||||
-H "X-Request-Id: relation-query-20260923-0001"
|
||||
```
|
||||
|
||||
成功响应示例(四种业务状态均返回 HTTP 200):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"sourceBindingId": "banban-binding-100001",
|
||||
"status": "never_bound",
|
||||
"pendingHandoff": true,
|
||||
"acceptingHandoff": true,
|
||||
"relationState": "open",
|
||||
"canUnbind": false,
|
||||
"reason": "NO_COMPLETED_BINDING",
|
||||
"checkedAt": "2026-09-23T08:00:00Z"
|
||||
},
|
||||
"requestId": "relation-query-20260923-0001",
|
||||
"timestamp": "2026-09-23T08:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
| `status` | 含义 | 伴伴处理 |
|
||||
| --- | --- | --- |
|
||||
| `bound` | 本次伴伴关系关联的 4G 绑定仍有效 | 提示先完成 4G 解绑 |
|
||||
| `unbound` | 关联的原 4G 绑定已由用户或管理员解除 | 完成关闭后,仅 `canUnbind=true` 才能解绑伴伴 |
|
||||
| `never_bound` | 有关系记录,且能证明尚未完成过 handoff 绑定 | 完成关闭后,仅 `canUnbind=true` 才能解绑伴伴 |
|
||||
| `unknown` | 无记录、历史无法准确关联或数据不一致 | 提示未确认,保留伴伴绑定并核查 |
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `pendingHandoff` | 是否存在 `active` 且未过期的凭证;数据库状态尚未改为 `expired` 的过期凭证不计入 |
|
||||
| `acceptingHandoff` | 此关系是否仍允许申请或消费新 handoff;还需满足功能开关、身份、设备状态等原有检查 |
|
||||
| `relationState` | `open` 允许关联;`closing` 已停止关联;`unknown` 表示没有关系记录 |
|
||||
| `canUnbind` | 仅当关系已 `closing`、无待处理凭证,且 `status` 为 `unbound/never_bound` 时为 `true` |
|
||||
| `reason` | 状态原因,例如 `NO_RELATION`、`AMBIGUOUS_HISTORY`、`ORIGINAL_BINDING_ACTIVE`、`ORIGINAL_BINDING_RELEASED`、`NO_COMPLETED_BINDING` |
|
||||
| `checkedAt` | 查询时间,UTC、带 `Z` 时区;只是本次快照时间,不是后续请求的锁或授权凭据 |
|
||||
|
||||
该 GET 不会撤销凭证或关闭关系,响应禁止缓存。即使查到 `never_bound + pendingHandoff=false`,只要仍为 `open`,`canUnbind` 就是 `false`。伴伴不能根据一次普通 GET 直接删除绑定,必须进入下一节关闭流程。
|
||||
|
||||
## 12. 接口四:关闭关联并确认是否可以解绑伴伴
|
||||
|
||||
```http
|
||||
POST /internal/v1/binding-relations/{sourceBindingId}/close
|
||||
X-Service-Token: <BANBAN_HANDOFF_SERVICE_TOKEN>
|
||||
Content-Type: application/json
|
||||
|
||||
{"requestId":"banban-close-20260923-0001"}
|
||||
```
|
||||
|
||||
`requestId` 必填,最长 64 字符,用于审计及重试追踪;关闭操作按 `sourceBindingId` 幂等,重复调用不会恢复关系,也不会重复解绑设备。返回结构与第 11 节一致。
|
||||
|
||||
```bash
|
||||
curl -i -sS -X POST "$BASE_URL/internal/v1/binding-relations/$SOURCE_BINDING_ID/close" \
|
||||
-H "X-Service-Token: $BANBAN_HANDOFF_SERVICE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Request-Id: banban-close-20260923-0001" \
|
||||
--data '{"requestId":"banban-close-20260923-0001"}'
|
||||
```
|
||||
|
||||
此操作会锁定关系,将其设置为 `closing`,撤销所有尚未消费的凭证,再判断原绑定是否有效。不会自动解除 4G 用户绑定,也不会删除 SIM 档案或影响已有流量、充值记录。
|
||||
|
||||
如果 `sourceBindingId` 完全没有记录,关闭操作会创建一条拒绝后续关联的记录,但仍返回 `unknown + canUnbind=false`,不会把“没有记录”变成“证明从未绑定”。
|
||||
|
||||
伴伴后端的解绑顺序:
|
||||
|
||||
1. 用户确认解绑后,先将本地伴伴关系标记为解绑中;该状态必须持久化,并阻止所有入口发起新的 handoff。
|
||||
2. 调用上述 `close`;超时、网络失败时保持解绑中并重试,不删除本地绑定。
|
||||
3. 返回 `bound`:提示用户先到 4G 小程序解绑。此时 handoff 已被关闭,但原 4G 绑定继续有效,流量查询和充值仍按原权限工作。
|
||||
4. 用户完成 4G 解绑后,再调用 GET 或重试 `close`;只有 `canUnbind=true` 时才能删除伴伴绑定。
|
||||
5. 返回 `unknown`,或 HTTP 401/429/5xx:保留伴伴绑定,提示未确认或稍后重试。不能把异常兜底成 `unbound`。
|
||||
|
||||
并发结果:消费事务先获得关系锁并完成绑定时,`close` 等待后返回 `bound`;`close` 先获得锁时,正在途中的签发、消费请求等待后返回 `HANDOFF_RELATION_CLOSED`。`pendingHandoff` 是已提交凭证的快照;正在进行的消费由 `close` 的事务锁处理。
|
||||
|
||||
`closing` 是持续生效的关闭状态,本版本没有恢复/取消关闭接口。即使用户取消了伴伴解绑,也不会自动恢复此 ID 的 handoff 能力;已有 4G 绑定可以继续使用。只有伴伴真正建立新的绑定关系时才使用新的 `sourceBindingId`。
|
||||
|
||||
查询、关闭接口错误码:
|
||||
|
||||
| HTTP 状态 | `code` | 处理 |
|
||||
| --- | --- | --- |
|
||||
| 400 | `INVALID_REQUEST` | 路径 ID 为空/超长,或关闭请求缺少合法 `requestId` |
|
||||
| 401 | `HANDOFF_SERVICE_UNAUTHORIZED` | 缺失或错误的服务凭证 |
|
||||
| 429 | `RATE_LIMITED` | 同一关系查询和关闭合计超过每分钟 60 次;降低频率 |
|
||||
| 500 | `INTERNAL_ERROR` | 数据库、事务或其他内部失败;保留伴伴关系并重试/排查 |
|
||||
|
||||
## 13. 本次升级部署及验收
|
||||
|
||||
本次新增 Flyway `V13__add_handoff_binding_relations.sql`,启动时自动建表、关联凭证并回填可明确识别的历史绑定,不修改已有 V12 迁移文件。历史凭证能通过消费用户、SIM、消费请求号唯一关联原绑定时会回填;同用户复用旧绑定但请求号不同等无法确认的情况保留为 `unknown`,由后端核查。
|
||||
|
||||
发布时同步更新 JAR 与 `nginx/locations.conf`,让新增 `/traffic-4g/internal/v1/binding-relations/` 路径生效;在实际承载域名的 Nginx 进程/容器中检查配置并重载。只替换 JAR 不会使 Nginx 自动开放新路径。迁移期间停止旧后端实例写入,完成迁移后再由新实例提供 handoff 服务。
|
||||
|
||||
新增验收场景:
|
||||
|
||||
1. 不带或带错误 `X-Service-Token` 调用查询、关闭,均返回 HTTP 401。
|
||||
2. 查询未出现过的 `sourceBindingId`,返回 HTTP 200、`unknown`、`canUnbind=false`。
|
||||
3. 申请未消费凭证后,查询返回 `never_bound`、`pendingHandoff=true`、`canUnbind=false`。
|
||||
4. 同一关系申请多张凭证后关闭,全部不能再消费,返回 `never_bound`、`pendingHandoff=false`、`canUnbind=true`。
|
||||
5. 成功消费后关闭,返回 `bound`、`canUnbind=false`;用户或管理员完成 4G 解绑后,查询返回 `unbound`、`canUnbind=true`。
|
||||
6. 原绑定解绑后,同一 ICCID 被其他用户或原用户通过其他流程绑定,旧关系查询仍为 `unbound`;旧已消费凭证不能变成新绑定的重试凭证。
|
||||
7. 同一 `sourceBindingId` 使用不同 `requestId` 换 ICCID,返回 `IDEMPOTENCY_CONFLICT`。
|
||||
8. 并发执行关闭与签发/消费,结果只能是“先绑定成功,关闭返回 bound”或“关闭成功,后续关联失败”。
|
||||
9. 重复关闭结果保持关闭状态;没有历史的关系关闭后仍为 `unknown`。
|
||||
10. 历史不明确的数据保持 `unknown`,不得通过换申请请求号绕过。
|
||||
|
||||
自动化验证:普通单元和接口测试执行 `mvn test`;真实 MySQL 事务测试为 `HandoffBindingRelationIntegrationTest`,需要显式配置 `HANDOFF_TEST_JDBC_URL`、`HANDOFF_TEST_DB_PASSWORD`(可选 `HANDOFF_TEST_DB_USERNAME`,默认 root),指向全新的空测试库后执行 `mvn -Dtest=HandoffBindingRelationIntegrationTest test`。该测试只使用指定测试库,不读取生产 `.env`;数据库非空时会拒绝执行。
|
||||
Reference in New Issue
Block a user