Files
junhong_cmp_fiber/openspec/specs/phone-asset-association/spec.md
break 5ed6b39deb
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕
- 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败
- 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影
- 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序
- 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列
- 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名
- 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
2026-09-18 15:34:29 +08:00

229 lines
14 KiB
Markdown
Raw 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.
# 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 提供补录或修改关联的入口。
关联查询 SHALL 额外返回两项投影:该手机号当前有效关联资产数量,以及每条关系的最近解绑人与最近解绑时间。数量口径 MUST 与该手机号「最多关联十项」的上限判定完全一致只统计当前有效关系、卡与设备分别计一项MUST 按当页涉及的手机号集合一次批量聚合MUST NOT 逐手机号或逐资产放大查询次数。最近解绑人 MUST 取最近一次解除该关系的后台账号名称与账号标识快照并随既有解绑方式、解绑时间、解绑原因一并返回关系仍有效时该字段为空MUST NOT 以创建人或最后更新人填充。以上字段 MUST 只读MUST NOT 改变既有解绑、换绑、上限判定与审计规则。
#### Scenario: 数据范围外不可见
- **WHEN** 平台用户查询其资产数据范围之外的资产的关联手机号
- **THEN** 系统不返回该资产的关联手机号
#### Scenario: 列表一次聚合
- **WHEN** 请求一页包含多项资产的列表
- **THEN** 系统按该页资产集合一次读取关联手机号并按资产装配,不逐资产查询
#### Scenario: 解绑后仍可查看完整快照
- **WHEN** 一次批量解绑任务执行完成、关系已失效后查看该任务结果
- **THEN** 结果中仍返回该次解除时记录的完整手机号快照
#### Scenario: 两类导出均含关联手机号
- **WHEN** 导出 IoT 卡场景或设备场景
- **THEN** 两类导出均包含关联手机号列,且设备导出列组解析对新增列保持正确
#### Scenario: 数量与上限口径一致
- **GIVEN** 某手机号当前有效关联十项资产
- **WHEN** 查询其中任一关联
- **THEN** 返回的当前有效关联资产数量为十,与上限判定使用的计数相同
#### Scenario: 已解绑关系展示解绑人
- **GIVEN** 一条关系已被后台账号 A 通过单项解绑失效
- **WHEN** 查询该关系
- **THEN** 返回最近解绑人为 A 的名称与标识、解绑时间与解绑原因
#### Scenario: 有效关系无解绑人
- **WHEN** 查询一条仍有效的关联
- **THEN** 最近解绑人与最近解绑时间均为空,且不以创建人或更新时间填充
#### Scenario: 数量批量聚合
- **GIVEN** 一页返回多项资产或手机号的关联
- **WHEN** 查询该页
- **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: 短信验证码校验失败次数限制
系统 SHALL 对短信验证码校验实施失败次数限制,作用域覆盖注册、绑定、换绑与换证的全部短信验证码校验流程:失败计数按窗口续期维护——每次校验失败在窗口内累加计数并重置窗口时长,相邻两次失败间隔不超过窗口时长即持续累加;计数达到上限后短时间内的后续校验 MUST 被拒绝并返回可定位的限流错误码与提示;校验成功 MUST 清零该手机号的失败计数。失败计数与锁定 MUST 以手机号维度按原子操作维护,并发提交 MUST NOT 绕过计数;锁定检查 MUST 先于验证码比对,锁定期内即使提交正确验证码也 MUST NOT 通过;锁定期为自计数达到上限起的一个窗口时长,锁定期间的提交在比对前即被拒绝且不再计数,因此锁定不因再次提交而续期;锁定到期后 MUST 自动恢复可校验状态MUST NOT 需要人工解锁。阈值与窗口 MUST 为固定常量或既有服务端配置MUST NOT 新增对外可配置项或接口。错误提示 MUST NOT 泄露验证码正确性、剩余失败次数或内部键名。
失败计数的写入或读取失败 MUST NOT 阻断校验流程的成功路径:计数不可用时系统 MUST 放行并记录MUST NOT 因限流计数故障阻断注册、绑定、换绑、换证与登录的成功路径。发送侧既有频率限制、验证码有效期与一次性消费语义 MUST 保持不变;校验失败 MUST NOT 消费验证码MUST NOT 影响既有链路的成功结果。
#### Scenario: 连续失败达到上限
- **WHEN** 同一手机号在窗口内连续提交错误验证码达到上限
- **THEN** 后续校验被拒绝并返回限流错误,即使提交的是正确验证码也不通过
#### Scenario: 锁定到期恢复
- **GIVEN** 某手机号因失败次数达到上限被短时锁定
- **WHEN** 锁定窗口结束且提交正确验证码
- **THEN** 系统正常通过校验
#### Scenario: 成功后清零
- **GIVEN** 某手机号已有若干次失败计数但未达上限
- **WHEN** 一次校验成功
- **THEN** 该手机号失败计数被清零,后续失败重新计数
#### Scenario: 并发提交不绕过计数
- **WHEN** 同一手机号并发提交多个错误验证码
- **THEN** 失败计数按实际提交次数累加,不因并发而丢失计数
#### Scenario: 失败不消费验证码
- **WHEN** 一次校验因验证码错误或限流失败
- **THEN** 该验证码保持可再次校验(在有效期与计数限制允许范围内),不产生任何业务事实
#### Scenario: 计数不可用不阻断成功路径
- **GIVEN** 失败计数所依赖的存储不可用
- **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}`(解除单条手机号资产关联)。