26 KiB
设备直接绑定接口联调文档
1. 适用范围
本文档用于 Banban 小程序与 4G 管理后端之间的“设备无法获取绑定码时直接绑定”联调。
2026-09-23 更新:新增按 sourceBindingId 查询原绑定关系、关闭 handoff 关联的服务端接口,见第 11、12 节。伴伴解绑必须以 canUnbind=true 为放行条件,不能只看 ICCID 当前状态或把查不到当成已经解绑。
生产环境推荐使用 handoffToken 两段式流程:
- Banban 后端使用服务凭证,向 4G 管理后端申请一次性
handoffToken。 - 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 必须包含此前缀:
BASE_URL=https://role.api.talkingq.com/traffic-4g
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 证明设备归属。 - 新增关系查询、关闭接口继续使用
BANBAN_HANDOFF_SERVICE_TOKEN,不需要新增密钥或修改.env。即使HANDOFF_ENABLED=false,仍可使用正确服务凭证查询、关闭已有关系。
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://role.api.talkingq.com/traffic-4g/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 个字符;同一绑定关系保持不变,且必须始终对应同一 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 后端服务器或安全的服务端环境执行:
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:
{
"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://role.api.talkingq.com/traffic-4g/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 绑定状态、记录 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:
{
"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 直接绑定(逐步下线)
旧接口地址:
POST /api/v1/program/bindings/by-iccid
该接口要求:
- 使用
PROGRAM_USER的Authorization: BearerToken; - 携带与页面完全一致的
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. 联调验收清单
建议按以下顺序验收:
- 使用尚未登记的测试 ICCID,申请接口返回
code=OK、targetAppId、handoffToken和正数expiresIn。 - 查询数据库,确认该 ICCID 已自动写入
sim_card,初始为status=active、binding_status=unbound、sync_status=pending。 - 使用 4G 小程序的
wx.login()code 登录,确认返回PROGRAM_USER的accessToken。 - 使用该
accessToken消费 handoffToken,返回code=OK、正确 ICCID、alreadyBound=false。 - 查询
GET /api/v1/program/devices,确认设备出现在当前 4G 小程序用户列表中。 - 使用相同
sourceBindingId + requestId重试申请接口,确认不会重复创建 SIM 档案或凭证。 - 使用相同用户、相同 handoffToken 和相同绑定
requestId重试消费接口,确认返回幂等结果或可确认已绑定。 - 绑定成功后调用流量详情接口,确认设备可以进入
pending或同步后的流量状态,而不是DEVICE_NOT_FOUND。 - 使用其他小程序身份消费,确认返回
HANDOFF_TARGET_APP_MISMATCH。 - 使用过期或篡改后的凭证,确认分别返回
HANDOFF_TOKEN_EXPIRED或HANDOFF_TOKEN_INVALID。 - 使用已绑定到其他用户的设备,确认返回
DEVICE_ALREADY_BOUND,不会覆盖原绑定关系。 - 确认数据库中该设备的
sim_card.binding_status为bound,且user_device_binding.status为active。 - 确认旧
/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 关系
GET /internal/v1/binding-relations/{sourceBindingId}
X-Service-Token: <BANBAN_HANDOFF_SERVICE_TOKEN>
查询的是 sourceBindingId 实际关联的具体 4G 绑定记录。原绑定解绑后,即使同一 ICCID 后来被其他用户或原用户通过其他流程重新绑定,原关系仍返回 unbound。若未关闭的同一伴伴关系又成功消费了新 handoff,则查询追踪这次新关联。
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):
{
"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. 接口四:关闭关联并确认是否可以解绑伴伴
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 节一致。
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,不会把“没有记录”变成“证明从未绑定”。
伴伴后端的解绑顺序:
- 用户确认解绑后,先将本地伴伴关系标记为解绑中;该状态必须持久化,并阻止所有入口发起新的 handoff。
- 调用上述
close;超时、网络失败时保持解绑中并重试,不删除本地绑定。 - 返回
bound:提示用户先到 4G 小程序解绑。此时 handoff 已被关闭,但原 4G 绑定继续有效,流量查询和充值仍按原权限工作。 - 用户完成 4G 解绑后,再调用 GET 或重试
close;只有canUnbind=true时才能删除伴伴绑定。 - 返回
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 服务。
新增验收场景:
- 不带或带错误
X-Service-Token调用查询、关闭,均返回 HTTP 401。 - 查询未出现过的
sourceBindingId,返回 HTTP 200、unknown、canUnbind=false。 - 申请未消费凭证后,查询返回
never_bound、pendingHandoff=true、canUnbind=false。 - 同一关系申请多张凭证后关闭,全部不能再消费,返回
never_bound、pendingHandoff=false、canUnbind=true。 - 成功消费后关闭,返回
bound、canUnbind=false;用户或管理员完成 4G 解绑后,查询返回unbound、canUnbind=true。 - 原绑定解绑后,同一 ICCID 被其他用户或原用户通过其他流程绑定,旧关系查询仍为
unbound;旧已消费凭证不能变成新绑定的重试凭证。 - 同一
sourceBindingId使用不同requestId换 ICCID,返回IDEMPOTENCY_CONFLICT。 - 并发执行关闭与签发/消费,结果只能是“先绑定成功,关闭返回 bound”或“关闭成功,后续关联失败”。
- 重复关闭结果保持关闭状态;没有历史的关系关闭后仍为
unknown。 - 历史不明确的数据保持
unknown,不得通过换申请请求号绕过。
自动化验证:普通单元和接口测试执行 mvn test;真实 MySQL 事务测试为 HandoffBindingRelationIntegrationTest,需要显式配置 HANDOFF_TEST_JDBC_URL、HANDOFF_TEST_DB_PASSWORD(可选 HANDOFF_TEST_DB_USERNAME,默认 root),指向全新的空测试库后执行 mvn -Dtest=HandoffBindingRelationIntegrationTest test。该测试只使用指定测试库,不读取生产 .env;数据库非空时会拒绝执行。