feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s

- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史
- H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列
- 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案
- 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容
- 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏
- 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
This commit is contained in:
2026-09-15 11:54:56 +08:00
parent 93e072e1e2
commit 70e680eb0a
67 changed files with 3951 additions and 147 deletions

View File

@@ -0,0 +1,163 @@
# phone-asset-association 当前行为
## Purpose
保存仅由 H5 短信验证建立的手机号—资产当前有效关系,使全局强制绑定能逐资产执行,同时提供受资产数据范围控制的后台查看与解除能力。
## Requirements
### Requirement: H5 验证建立关联与数量上限
系统 SHALL 保留既有全局 H5 强制绑定开关。开关开启时,客户首次登录一项未关联当前手机号的资产必须完成短信验证码校验后才建立关系;已关联资产不重复验证。开关关闭时登录不要求验证,且不查询、不新增、不删除任何关系,已有关系保留。关联只能由 H5 验证建立,卡(`iot_card`)与设备(`device`)分别计为独立资产,单手机号最多关联十项当前有效资产;上限只统计当前有效关系,已失效关系不占用额度。上线不回填历史客户—资产关系。
#### Scenario: 第十一项资产验证
- **WHEN** 已关联十项有效资产的手机号验证第十一项资产
- **THEN** 系统拒绝本次新关联,已有十项关系不变
#### Scenario: 开关关闭后登录未关联资产
- **WHEN** 全局强制绑定开关关闭,客户登录一项未关联其手机号的资产
- **THEN** 系统不要求验证、不新增关系,且既有关系不被删除
#### Scenario: 卡与设备分别计数
- **WHEN** 同一手机号关联了 `iot_card` 类型 ID 为 5 的资产与 `device` 类型 ID 为 5 的资产
- **THEN** 两者计为两项资产,互不覆盖也不合并计数
### Requirement: 登录提示判定
系统 SHALL 在登录响应中以 `need_bind_phone` 表示当前访问资产是否仍需完成手机号验证,判定依据当前访问资产身份与该手机号之间的有效关系。开关开启时:客户无主手机号,或其主手机号未与当前访问资产存在有效关系,返回 `true`;已存在有效关系返回 `false`。开关关闭时恒为 `false`。该字段 MUST 只作为前端提示,系统 MUST NOT 因缺少该关系拒绝任何资产业务入口。
#### Scenario: 已有手机号但资产未关联
- **WHEN** 客户已有主手机号,登录一项该手机号未关联的资产
- **THEN** 响应 `need_bind_phone=true`
#### Scenario: 已关联资产再次登录
- **WHEN** 客户登录一项其主手机号已关联的资产
- **THEN** 响应 `need_bind_phone=false` 且不要求重复验证
#### Scenario: 缺关系不拦截业务接口
- **WHEN** 客户在 `need_bind_phone=true` 状态下调用其有权访问的资产业务接口
- **THEN** 系统按既有资产归属与数据范围规则授权,不因缺少该关系返回拒绝
### Requirement: H5 绑定与换绑的关联写入
系统 SHALL 在 H5 短信验证成功后建立或迁移关联。客户已有主手机号、提交号码与主手机号一致且验证码有效时,系统 MUST 幂等建立当前访问资产与该手机号的关系,不修改账号手机号;提交号码与主手机号不一致时仍拒绝绑定。请求不含当前访问资产身份时,系统只完成账号手机号绑定,不建立关联。同一手机号与同一资产已存在有效关系时,重复验证返回成功且不产生第二条关系。
客户更换手机号时 MUST 同时验证旧、新号码,并在同一事务内将旧手机号全部有效资产关系迁移至新号码。迁移前先比较「新号码现有有效关系数 + 旧号码待迁移有效关系数」,超过十项时整次失败且旧、新关系均保持原状。迁移与「新号码已存在的同资产有效关系」冲突时,整次换绑失败并回滚,旧、新关系均不变。
#### Scenario: 已有主号验证建联
- **WHEN** 客户已有主手机号 P提交 P 与有效验证码,且当前登录会话指向资产 A
- **THEN** 系统建立 P 与 A 的有效关系,账号手机号仍为 P
#### Scenario: 提交号码与主号不一致
- **WHEN** 客户已有主手机号,提交另一号码与有效验证码
- **THEN** 系统拒绝该绑定,不建立任何新关系
#### Scenario: 重复验证同一资产
- **WHEN** 同一手机号对已有关联的同一资产再次完成验证
- **THEN** 系统返回成功且不产生第二条有效关系
#### Scenario: 换绑超过上限
- **WHEN** 客户换绑后新手机号关联总数将超过十项
- **THEN** 系统不迁移任何关系且旧、新手机号关系均保持原状
#### Scenario: 换绑迁移与既有关系冲突
- **WHEN** 新手机号已存在与待迁移资产相同的有效关系
- **THEN** 整次换绑失败并回滚,旧、新手机号关系均保持原状
### Requirement: 后台查看关联
仅超级管理员和平台用户可在其资产数据范围内查看关联手机号。资产详情 MUST 列出该资产全部当前关联手机号;资产列表、卡与设备两类导出以及批量任务结果中,具备资产数据权限的账号可见完整关联手机号,不做脱敏。按资产查询 MUST 一次批量聚合完成。后台 MUST NOT 提供补录或修改关联的入口。
#### Scenario: 数据范围外不可见
- **WHEN** 平台用户查询其资产数据范围之外的资产的关联手机号
- **THEN** 系统不返回该资产的关联手机号
#### Scenario: 列表一次聚合
- **WHEN** 请求一页包含多项资产的列表
- **THEN** 系统按该页资产集合一次读取关联手机号并按资产装配,不逐资产查询
#### Scenario: 解绑后仍可查看完整快照
- **WHEN** 一次批量解绑任务执行完成、关系已失效后查看该任务结果
- **THEN** 结果中仍返回该次解除时记录的完整手机号快照
#### Scenario: 两类导出均含关联手机号
- **WHEN** 导出 IoT 卡场景或设备场景
- **THEN** 两类导出均包含关联手机号列,且设备导出列组解析对新增列保持正确
### Requirement: 后台解除关联
仅超级管理员和平台用户可在资产数据范围内解除关联,必须二次确认并填写原因;代理、企业和个人客户无后台解除能力。单项解除必须指定一条资产—手机号关系;勾选批量解除与 CSV 导入解除按资产解除该项资产全部当前有效关系。逐资产独立执行并返回成功数、失败数与逐项结果。越权、资产不存在与已无有效关系三种情况 MUST 对调用方返回统一失败文案「无权限操作该资源或资源不存在」。
CSV 导入按资产标识定位资产,逐行独立执行:成功行提交,失败行保留原状,不设行数硬上限,任务级失败不产生行明细。解绑原因由任务级必填字段与二次确认提供,行内只填资产标识与可选备注。
#### Scenario: 单项解除未指定关系
- **WHEN** 调用方请求单项解除但未指定具体关系
- **THEN** 系统拒绝该请求
#### Scenario: 批量部分成功
- **WHEN** 批量资产集合中部分资产有权且已关联、部分越权或不存在
- **THEN** 有权项成功解除、其余项失败,返回成功数、失败数与逐项结果,不因失败项回滚成功项
#### Scenario: 三态统一文案
- **WHEN** 调用方对越权资产、不存在资产或已无有效关系的资产发起解除
- **THEN** 三种情况返回同一失败文案「无权限操作该资源或资源不存在」
#### Scenario: 缺二次确认或原因被拒绝
- **WHEN** 解除请求未提交二次确认或未填写原因
- **THEN** 系统拒绝该请求且不解除任何关系
#### Scenario: 导入失败行保留原状
- **WHEN** CSV 中某行资产标识无法定位或该行资产无权解除
- **THEN** 该行记为失败并保留原因,其他行照常提交,任务继续执行直到结束
#### Scenario: 审计与日志只写脱敏值
- **WHEN** 任一解除操作成功或失败
- **THEN** 审计记录与运行日志中的手机号仅为脱敏形式,不出现完整手机号
### Requirement: 关联不影响换货与其他写入路径
换货、资产导入、后台资产编辑与个人客户主手机号变更历史均 MUST NOT 创建、推断、复制或迁移手机号—资产关系。换货完成后的新资产在客户首次 H5 访问时,按当时全局开关重新走验证。
#### Scenario: 换货不迁移关系
- **WHEN** 换货完成并用新资产替换旧资产
- **THEN** 新资产不继承旧资产的关联手机号,关联记录数不变
#### Scenario: 换货新资产首次登录
- **WHEN** 全局开关开启,客户换货完成后首次登录新资产
- **THEN** 响应 `need_bind_phone=true`
#### Scenario: 资产导入与后台资产编辑不产生关系
- **WHEN** 导入资产或修改后台资产属性
- **THEN** 关联记录数不变,不产生任何手机号—资产关系
## 可达操作索引
本节只用于入口导航,不是行为 Requirement业务义务以上述 Requirements 为准。
### 手机号资产关联
`GET /api/admin/phone-asset-associations`(查询手机号资产关联列表);`POST /api/admin/phone-asset-associations/batch-unbind`(按资产批量解除手机号关联);`POST /api/admin/phone-asset-associations/unbind-imports`(创建手机号资产解绑导入任务);`GET /api/admin/phone-asset-associations/unbind-imports`(查询解绑导入任务列表);`GET /api/admin/phone-asset-associations/unbind-imports/{id}`(查询解绑导入任务详情);`DELETE /api/admin/phone-asset-associations/{id}`(解除单条手机号资产关联)。