feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
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:
@@ -1,32 +0,0 @@
|
||||
## Context
|
||||
|
||||
现有 H5 已有手机号绑定与全局开关;新关系不能由后台或换货推测建立。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 新表以手机号、资产和有效状态保存关系,并对当前有效关系实施十项计数与唯一约束。
|
||||
- 验证/换绑在事务内锁定相关手机号关系;换绑先检查总数再整体迁移。
|
||||
- 批量导入复用逐行任务,读取按资产数据范围,日志仅保留脱敏手机号。
|
||||
|
||||
## H5 与后台动作契约
|
||||
|
||||
### H5 建联与换绑
|
||||
|
||||
- 既有登录签发 token 时,按全局强制绑定开关及当前访问资产查询有效关联;开关开启且当前手机号未关联该资产时,响应 `need_bind_phone=true`,并限制依赖该资产的业务入口直至验证成功。开关关闭时不创建新关联,也不删除历史关联。
|
||||
- 既有 `bind_phone` 短信场景验证成功后,事务中锁定手机号和资产有效关系;同一手机号—资产已有效关联则幂等成功,否则先计算该手机号有效资产数。达到十项返回“该手机号最多关联10项有效资产”,不写关联或手机号变更。
|
||||
- 既有 `change_phone_old`、`change_phone_new` 两个验证码均验证成功后,事务锁定旧、新手机号关系;计算新号码当前有效关系数加旧号码待迁移有效关系数,超过十项则整体失败。通过时把旧号码全部有效关系原子失效/迁移至新号码,并记录旧、新号码脱敏审计;任一步失败不变更任一关系。
|
||||
|
||||
### 后台查看与解除
|
||||
|
||||
- `GET /phone-asset-associations`:仅超级管理员、平台用户,先按资产数据范围过滤;支持资产标识、手机号(仅权限内完整值)、关联状态、创建时间筛选,返回资产、手机号、建立时间、建立来源固定为 `h5_sms_verification` 和状态。
|
||||
- `DELETE /phone-asset-associations/:id`:请求必须含 `reason`(1~500 字符)和二次确认;锁定指定有效关联后复核资产数据范围,标记失效并审计操作者、资产、脱敏手机号、原因和时间。后台没有创建/补录接口。
|
||||
- `POST /phone-asset-associations/batch-unbind`:请求去重的资产集合、原因、二次确认;每个资产独立解除其全部有效关系,返回成功数、失败数与逐资产结果。越权、资产不存在和已无有效关系对调用方使用统一失败文案。
|
||||
- Excel 解绑复用既有异步导入:每行按资产标识处理,独立授权与事务,任务持久化行号、结果和失败原因;一行失败不得回滚已成功行。
|
||||
|
||||
### 边界
|
||||
|
||||
- 换货、资产导入、后台资产编辑和个人客户主手机号历史记录均不得创建、推断、复制或迁移该关系。新换货资产在首次 H5 访问时才依当前开关走验证。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移,不回填;隔离库验证开关、上限、换绑、权限解绑、换货边界及 up/down/up。
|
||||
@@ -1,25 +0,0 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-009`。
|
||||
|
||||
## Why
|
||||
|
||||
现有全局绑定不能保留手机号与资产的验证关系,也无法安全支持按资产解除。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 H5 验证建立的手机号—资产关联及十项上限。
|
||||
- 新增客户原子换绑和后台受权限控制的单项/批量/Excel 解绑。
|
||||
- 保持全局强制绑定,换货不迁移关系,不回填历史。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `phone-asset-association`: 已验证手机号与资产关联。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响 H5 认证、资产查询、短信、导入、审计和 Schema。
|
||||
@@ -1,21 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
保存仅由 H5 短信验证建立的手机号—资产当前有效关系,使全局强制绑定能逐资产执行,同时提供受资产数据范围控制的后台查看与解除能力。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: H5 验证建立关联与数量上限
|
||||
系统 SHALL 保留既有全局 H5 强制绑定开关。开关开启时,客户首次登录一项未关联当前手机号的资产必须完成短信验证后建立关系;已关联资产不重复验证。开关关闭时登录不要求验证且不新增关系,已有关系保留。关联只能由 H5 验证建立,单手机号最多关联十项当前有效资产;上线不回填历史客户—资产关系。
|
||||
|
||||
#### Scenario: 第十一项资产验证
|
||||
- **WHEN** 已关联十项有效资产的手机号验证另一项资产
|
||||
- **THEN** 系统拒绝本次新关联,已有十项关系不变
|
||||
|
||||
### Requirement: 换绑、查看和解绑
|
||||
客户更换手机号时 MUST 同时验证旧、新号码,并原子将旧手机号全部有效资产关系迁至新号码;新号码现有关联数加迁移数超过十项时整次失败。后台仅超级管理员和平台用户可在资产数据范围内查看完整关联手机号、单项解绑、勾选批量解绑及 Excel 解绑;后台不得补录。
|
||||
|
||||
单项解绑必须指定一条资产—手机号关系。批量和 Excel 按资产解除该资产全部当前有效手机号关系,必须二次确认、填写原因、逐条审计;逐资产独立执行,返回成功数、失败数和统一越权失败文案。换货不得迁移该关系,新资产首次访问仍按全局开关验证。
|
||||
|
||||
#### Scenario: 换绑超过上限
|
||||
- **WHEN** 客户换绑后新手机号关联总数将超过十项
|
||||
- **THEN** 系统不迁移任何关系且旧、新手机号关系均保持原状
|
||||
@@ -1,12 +0,0 @@
|
||||
## 1. 关联与 H5
|
||||
- [ ] 1.1 追踪 H5 登录/短信验证、客户换绑、资产数据范围和换货调用链。
|
||||
- [ ] 1.2 新增关联表、有效关系唯一/计数索引的成对迁移、模型和审计常量。
|
||||
- [ ] 1.3 实现全局开关下的验证建联、十项限制和原子换绑。
|
||||
|
||||
## 2. 后台解绑
|
||||
- [ ] 2.1 实现资产详情/列表完整手机号投影、单项解绑、二次确认批量和逐行 Excel 解绑及统一越权结果。
|
||||
- [ ] 2.2 保持换货不迁移,补齐路由和 OpenAPI。
|
||||
|
||||
## 3. 验证
|
||||
- [ ] 3.1 隔离库验证上限、换绑回滚、权限、批量部分成功、换货边界和 up/down/up。
|
||||
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-phone-asset-associations --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,162 @@
|
||||
## Context
|
||||
|
||||
以下为开工核查确认的现状约束,全部来自实际代码、配置与生效库:
|
||||
|
||||
- `need_bind_phone` 全仓只有一个计算点 `internal/service/client_auth/service.go:892-931`,口径是「客户无 `is_primary=true AND status=1` 的 `tb_personal_customer_phone` 记录」且全局开关 `cfg.Client.RequirePhoneBinding` 为真;入参 `assetType/assetID` 只写入 JWT(`:910`),不参与任何判定。开关读取点全仓仅 `:899` 一处,默认 `true`(`pkg/config/config.go:107`、`defaults/config.yaml:110`)。
|
||||
- `need_bind_phone` 在后端没有任何消费方;`internal/middleware/personal_auth.go:34-113` 只做 JWT 验签与 Redis token 比对,不读 `claims.Phone` 做判断。
|
||||
- 登录 JWT 已含 `customer_id/phone/asset_type/asset_id`(`pkg/auth/jwt.go:10-16`),资产类型取值为 `iot_card` / `device`(`pkg/constants/iot.go:142-143`),请求上下文可经 `middleware.GetCurrentAsset`(`personal_auth.go:128-133`)取出。
|
||||
- `BindPhone`(`service.go:294-378`)现状在客户已有主手机号时立即返回 `CodeAlreadyBoundPhone`(`:301-308`);成功后只写 `tb_personal_customer_phone`,不触碰任何资产关系。
|
||||
- `ChangePhone`(`service.go:381-471`)两个验证码在事务外校验(`:410`、`:415`,校验即消费),事务只包「锁定主号行 → 查新号占用 → 原地 `UPDATE phone` 列(`:448-453`)→ 审计」。
|
||||
- 手机号唯一事实在 `tb_personal_customer_phone`(`internal/model/personal_customer_phone.go:11-23`);`PersonalCustomer` 模型无 phone 字段(`internal/model/personal_customer.go:9-17`);生效库仍有遗留列 `tb_personal_customer.phone`,无 Go 模型映射(`internal/store/postgres/personal_customer_store.go:42` 注释)。
|
||||
- 资产读侧 DTO(`internal/model/dto/{iot_card,device,asset}_dto.go`)中手机号零命中,没有完整手机号投影先例。
|
||||
- 设备导出的列组反解依赖固定常量:`internal/exporter/device_scene.go:14-18`(base 6 / group 5 / tail 5)与 `cardGroupCountFromHeaders:427-434` 的整除判断。
|
||||
- 审计 writer 对既有手机号资源写**完整手机号**(`internal/infrastructure/audit/writer.go:397-403`、`:614-618`);`pkg/sanitizer/sanitizer.go:16-22` 的字段名脱敏清单不含 phone,访问日志不会自动脱敏手机号。
|
||||
- 仓库**不存在统一导入任务框架**,集中的只有 `QueueForTaskType`、worker 注册表与发布门禁未完成任务表清单三处(结论出处 `openspec/changes/archive/2026-09-14-add-shop-salesperson-groups/design.md:6`,**非 AUG26-008**)。
|
||||
- 「手机号—资产」关联事实在全仓代码、迁移与生效库中命中数为 0:既有关系只有「客户↔手机号」与「客户↔资产」两条互不相交的链路。
|
||||
- 换货完成链路(`internal/service/exchange/service.go:1084-1086` → `internal/service/customer_binding/service.go:354-390`)只迁移 device/iccid 绑定,全链路手机号零命中;后台不存在通用「编辑资产基本信息」接口。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 以一条 phone×asset 直连事实承载「仅由 H5 验证建立」的当前有效关系,并使其计数、迁移、解绑在单事务内闭合。
|
||||
- H5 契约变更有明确的上线顺序约束,不产生「提示要验证却无法建联」的死锁窗口。
|
||||
- 后台读侧在资产数据范围内展示完整手机号,同时保证审计、日志与错误只出现脱敏值。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不新增后端授权 gate,不把关联变成资产业务入口的前置条件。
|
||||
- 不重构既有 `无权限操作该资源或资源不存在` 内联字面量(44 处),不重构既有三处私有手机号脱敏函数。
|
||||
- 不隔离短信验证码场景(现状 `scene` 仅为 DTO 字面量,验证码 Redis key 仅按手机号区分)。
|
||||
- 不写遗留列 `tb_personal_customer.phone`。
|
||||
- 不实现 H5 页面本身;只交付后端接口与其契约约束。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 关联表结构以 phone 字符串为计数口径
|
||||
|
||||
新表保存 `asset_type`、`asset_id`、`phone`(字符串)、`status`、建立时间、建立来源与失效信息。
|
||||
|
||||
- **必须含 `asset_type`**:`iot_card` 与 `device` 的 ID 空间独立,同号会互相覆盖。资产身份一律取 `(asset_type, asset_id)`,与 JWT 及既有 `customer_binding` 的资产口径一致。
|
||||
- **`phone` 存字符串**:PRD 的计数与唯一口径都是「手机号」,且换绑是原地改写手机号行,字符串口径使「上限 = 该号码的有效关系数」与换绑迁移都退化为直白的集合运算。
|
||||
- **建立来源固定为 `h5_sms_verification`**:后台不得补录,来源字段是这一约束的可核对事实。
|
||||
- 索引三件套:部分唯一索引 `(phone, asset_type, asset_id) WHERE status = <有效>`;按 `phone` 计数的索引;按 `(asset_type, asset_id)` 集合批量查询的索引。
|
||||
- `status` 用 int 表达有效/失效(ENG-STATE-001),失效信息记录失效时间与失效方式。
|
||||
|
||||
### 2. `need_bind_phone` 三支判定,且明确定位为提示型字段
|
||||
|
||||
`issueLoginToken` 改为:
|
||||
|
||||
1. 无主手机号 → `true`;
|
||||
2. 有主手机号但当前手机号未与当前访问资产存在有效关系 → `true`;
|
||||
3. 已存在有效关系 → `false`;
|
||||
4. 全局开关关闭 → 恒 `false`,且**不查询、不创建、不删除**任何关系。
|
||||
|
||||
关联查询使用 JWT 的 `asset_type/asset_id`,**不得依赖 `phone` claim**:`phone` 是登录时快照,换绑后到下次登录前仍是旧号。
|
||||
|
||||
开关关闭分支必须完全短路,否则会出现「关闭开关却写库」的越权写入。
|
||||
|
||||
**本项不新增后端 gate。** 现状确认「未绑定不得访问资产业务入口」由前端实施:`need_bind_phone` 无任何后端消费方,认证中间件不读手机号,手机号在链路上只用于展示(`internal/handler/app/client_asset.go:224`)。关系缺失不改变任何资源授权——授权始终由 `OwnsAsset` 与 `ApplyShopFilter` 决定。
|
||||
|
||||
### 3. bind-phone 扩展语义与上线顺序(G1 裁定)
|
||||
|
||||
`BindPhone` 的分支改为:
|
||||
|
||||
| 客户状态 | 提交号码 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| 无主手机号 | 任意未被他人占用 | 建立账号手机号,并在存在资产身份时建立关联 |
|
||||
| 已有主手机号 | 等于主手机号 | 验证码有效 → **幂等建联**;已有关联则返回成功;不修改账号手机号 |
|
||||
| 已有主手机号 | 不等于主手机号 | 仍拒绝(换号必须走 `change-phone`) |
|
||||
| 任意 | 请求不含当前访问资产身份 | 只完成账号手机号绑定或幂等成功,**不建立关联** |
|
||||
|
||||
建联路径在事务内锁定手机号行后先计数:达到十项返回「该手机号最多关联10项有效资产」,不写关联也不改手机号。
|
||||
|
||||
**上线顺序约束(必须写进发布说明):** 后端不再对「已有主号」直接拒绝,因此本 Change 与 H5 **必须同批上线**;H5 必须支持预填已在账号上的手机号并对该号码发验证码。若后端先行或 H5 未改造,存量客户会落入 `need_bind_phone=true` 但提交任何号码都被拒的死锁。
|
||||
|
||||
### 4. 并发与加锁
|
||||
|
||||
- **固定加锁顺序:按手机号行 `id ASC` 加锁**(等价的实现是固定「旧号 → 新号」,但必须与 `id ASC` 结果一致,避免两条路径得出相反顺序)。`bind-phone` 只锁一行,`change-phone` 锁两行;统一顺序后两者不会形成 A→B / B→A 死锁环。既有先例:AUG26-008 的「统一加锁顺序」实施决策。
|
||||
- **同号并发建联以手机号行 `FOR UPDATE` 串行**:计数与插入在同一临界区内,保证不越过十项上限。
|
||||
- **同 `(phone, asset_type, asset_id)` 并发由部分唯一索引兜底**,唯一冲突映射为幂等成功而非报错。
|
||||
- 加锁顺序规则写在用例层(Application/旧 Service),事务内不得再发起外部 I/O(ENG-TX-001)。
|
||||
|
||||
### 5. 换绑原子迁移与冲突边界
|
||||
|
||||
在同一事务内、加锁之后:
|
||||
|
||||
1. 计算「新号码现有有效关系数 + 旧号码待迁移有效关系数」;
|
||||
2. 超过十项 → 返回「该手机号最多关联10项有效资产」并整次失败,旧、新关系均不变;
|
||||
3. 通过 → 将旧号码全部有效关系原子迁移至新号码(关系本身保持有效,只换归属号码);
|
||||
4. **若迁移与「新号码已存在的同资产有效关系」冲突**(例如新号码曾是他人已停用手机号记录持有者的验证号码),部分唯一索引报错 → 整次换绑失败并回滚,旧、新关系均不变。该边界是**有意接受**的:失败是安全的、可人工处理的,好过产生两条并存的有效关系。
|
||||
|
||||
事务边界与既有实现一致:验证码在事务外校验(消费即删除),关系迁移与账号手机号改写同事务。**不写遗留列 `tb_personal_customer.phone`**(无模型映射,写它等于制造无法读取的事实漂移)。
|
||||
|
||||
审计只写脱敏手机号。
|
||||
|
||||
### 6. 读侧投影、脱敏与导出列组兼容
|
||||
|
||||
- **列表与详情一次 `IN` 聚合**:新增按 `(asset_type, []assetID)` 的批量读,返回 `map[assetID][]phone`,列表按当页资产集合一次查询后装配,禁止逐资产查询。详情为单资产单次查询。
|
||||
- **两个导出场景都补「关联手机号」列**(`internal/exporter/iot_card_scene.go:39-41` 与 `internal/exporter/device_scene.go:47`),导出按本批资产集合批量查询。
|
||||
- **设备导出新列并入尾部并令 `deviceExportTailHeaderCount` 递增**,`buildDeviceExportRow` 同步追加。反解必须保留**旧表头兼容分支**:先用新尾列数试整除,不整除时回退到旧尾列数再判定;否则历史任务凭 `ResolvedHeaders` 重导出时列组数会算成 0。
|
||||
- **完整手机号只在读侧**:资产列表、详情、两类导出与批量任务结果向具备资产数据权限的账号返回完整值。
|
||||
- **审计、日志与错误只写脱敏值**:新建动作码与资源定义,**不得复用会写明文手机号的既有审计资源路径**(`writer.go:397-403`、`:614-618` 对既有手机号资源写完整值)。脱敏口径沿用既有前 3 位 + `****` + 后 4 位。
|
||||
|
||||
### 7. 批量任务与 CSV 导入
|
||||
|
||||
- 无统一导入框架,新增第 6 个独立场景(详见 tasks 2.5 的 B1–B16 全清单)。
|
||||
- **任务明细持久化解绑当时的完整手机号快照**:解绑后关系即失效,只有快照能事后满足「批量任务结果展示完整手机号」;任务明细是业务事实表,读接口按数据范围保护,不属于日志。
|
||||
- **上传格式 CSV**,沿用既有 BOM 去除与 GBK 回退解码。
|
||||
- **列:资产标识(必填,复用既有资产标识解析)+ 备注(可选)**;解绑原因由任务级必填字段与二次确认提供,不设行内原因列。
|
||||
- 逐行独立事务、成功行提交、失败行保留原状、不设行数硬上限、任务级失败不产生行明细;行号自数据首行起计。
|
||||
|
||||
### 8. 权限与统一文案
|
||||
|
||||
- 路由组级 gate 沿用既有超管/平台先例(`internal/routes/wecom.go:16-21`)。
|
||||
- 业务层每次解绑仍按资产数据范围复核(ENG-AUTHZ-001),不得只依赖路由角色中间件。
|
||||
- 单项解绑以路径主键即「必须指定一条关系」;批量与 CSV 按资产解除全部有效关系,逐资产独立结果。
|
||||
- 三态(越权 / 资产不存在 / 已无有效关系)统一为 `无权限操作该资源或资源不存在`,**新增常量**供新用例使用;既有 44 处内联字面量按 As-Is 保留,不在本 Change 统一。三态收敛同时满足「无权限与不存在不得形成可枚举差异」。
|
||||
|
||||
## 动作契约
|
||||
|
||||
### H5
|
||||
|
||||
- `POST /api/c/v1/auth/bind-phone`(既有):已有主号且提交号码与主号一致、验证码有效时幂等建立当前资产关联;不一致仍拒绝;无资产身份时只处理账号手机号。响应形状不变。
|
||||
- `POST /api/c/v1/auth/change-phone`(既有):新号总数校验通过后原子迁移旧号全部有效关系;冲突或超限整次失败并回滚。
|
||||
- 登录响应 `need_bind_phone` 按第 2 节三支判定;字段语义扩宽但形状不变。
|
||||
|
||||
### 后台
|
||||
|
||||
- `GET /phone-asset-associations`:仅超级管理员、平台用户,先按资产数据范围过滤;支持资产标识、手机号、关联状态、创建时间筛选,返回资产、手机号、建立时间、建立来源与状态。
|
||||
- `DELETE /phone-asset-associations/:id`:路径主键即指定一条关系;必须二次确认并填写原因(1~500 字符);锁定关系后复核资产数据范围,标记失效并逐条审计。
|
||||
- `POST /phone-asset-associations/batch-unbind`:去重的资产集合、原因与二次确认;每项资产独立解除全部有效关系,返回成功数、失败数与逐项结果。
|
||||
- CSV 解绑导入:任务级原因与二次确认必填;逐行按资产标识处理,行内失败不影响其他行。
|
||||
- 不提供创建或补录入口。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [H5 未同批上线导致死锁] → 契约与发布说明同时声明同批上线;后端不再直接拒绝已有主号是本 Change 的显式破坏性变更。
|
||||
- [存量客户提示面扩大] → `need_bind_phone` 取值面扩大是预期行为,必须与 H5 同批;不通过灰度开关掩盖。
|
||||
- [并发建联越过十项上限] → 手机号行 `FOR UPDATE` 串行化计数与插入;唯一索引兜底同资产并发并映射为幂等成功。
|
||||
- [换绑迁移撞唯一索引] → 整次换绑失败回滚,旧、新关系均不变;该边界有意接受,不引入自动合并或丢弃。
|
||||
- [禁用手机号记录被复用导致编号碰撞] → 同一失败边界覆盖,事务回滚后由人工处理,不产生并存有效关系。
|
||||
- [设备导出列组反解回归] → 新列并入尾部并递增尾列数,同时保留旧表头兼容分支,回归覆盖历史任务重导出。
|
||||
- [审计继承明文手机号] → 新动作码与资源定义独立,不复用既有写明文值的手机号资源路径。
|
||||
- [脱敏被访问日志绕过] → 字段名脱敏清单不含 phone,故新代码必须显式脱敏,不能依赖通用清理器。
|
||||
- [列表 N+1] → 强制一次批量聚合;验收以查询次数而非响应内容为准。
|
||||
- [换货/导入/后台编辑误写关系] → 三处路径已确认零手机号逻辑,采用反向断言守护,不接受「顺手补写」。
|
||||
- [上下文证据缺失] → 实施不触碰证据文件,归档并同步主 Spec(含新路由索引)后必须补齐,否则 `scripts/context-health.sh` 失败。
|
||||
|
||||
## 溯源说明
|
||||
|
||||
「仓库不存在统一导入任务框架」的结论出自 `openspec/changes/archive/2026-09-14-add-shop-salesperson-groups/design.md:6`;AUG26-008 的归档文档通篇不涉及导入框架。本 Change 的导入场景改动面以 `add-shop-salesperson-groups` 已落地的 `shop_business_owner_import` 为模板。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移(`.up.sql`/`.down.sql`)创建关联表与三类索引,编号按实施时 `migrations/` 目录最大编号顺延;`down` 带有效性守卫。
|
||||
2. 不回填历史。开关关闭时不查询、不创建、不删除关系。
|
||||
3. 按 ENG-TEST-001 在维护者指定的 `junhong_cmp_test` PostgreSQL + Redis DB 6 验证;fixture 只增删本 Change 自己的记录,禁止重置整库。
|
||||
4. 单批发布:新表对旧代码无影响;本 Change 与 H5 必须同批上线。
|
||||
|
||||
## 归档后动作(不属实施任务)
|
||||
|
||||
实施阶段不修改 `docs/verification/context-reset/requirement-evidence.json` 与 `entry-capability-requirement-matrix.json`。归档并同步主 Spec(含新路由索引)之后,必须补齐这两个文件,否则 `scripts/context-health.sh` 失败。
|
||||
@@ -0,0 +1,29 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-009`。
|
||||
- 需求追溯:PRD-08-011(仅作追溯)。
|
||||
|
||||
## Why
|
||||
|
||||
现有全局绑定只按客户记一项主手机号,既不能保留手机号与单项资产的验证关系,也无法安全支持按资产查看与解除;后台、换货与批量路径都没有可依据的关联事实。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增仅由 H5 短信验证建立的手机号—资产当前有效关系及十项上限;`iot_card` 与 `device` 分别计为独立资产,即使两者 ID 相同。
|
||||
- `need_bind_phone` 由客户维度扩为「全局开关开启且当前主手机号未关联当前访问资产」;开关关闭时不查询、不创建、不删除任何关系。
|
||||
- **BREAKING**(H5 认证契约)`bind-phone` 不再对「已有主号」直接拒绝:已有主号且提交号码与主号一致、验证码有效时,幂等建立当前访问资产与主手机号的关联。**H5 必须与本 Change 同批上线**并支持预填已在账号上的手机号,否则会出现 `need_bind_phone=true` 却无法建联的死锁。
|
||||
- 新增客户原子换绑:新号现有有效关系数加旧号待迁移数超过十项时整次失败,新旧关系均不变。
|
||||
- 新增后台受权限控制的单项解绑、勾选批量解绑与 CSV 导入解绑;解绑任务明细持久化解绑当时的完整手机号快照。
|
||||
- 保持全局强制绑定;**不新增后端访问 gate**,缺关系不改变任何资源授权;换货不迁移关系,不回填历史。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `phone-asset-association`: 已验证手机号与资产关联。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响 H5 认证契约与上线顺序、资产列表/详情/导出读侧、短信验证、CSV 导入场景族、审计脱敏与 Schema。实施不修改上下文证据文件;归档并同步主 Spec(含新路由索引)后必须补齐 `docs/verification/context-reset/` 证据,否则 `scripts/context-health.sh` 失败。
|
||||
@@ -0,0 +1,123 @@
|
||||
## Purpose
|
||||
|
||||
保存仅由 H5 短信验证建立的手机号—资产当前有效关系,使全局强制绑定能逐资产执行,同时提供受资产数据范围控制的后台查看与解除能力。
|
||||
|
||||
## ADDED 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** 关联记录数不变,不产生任何手机号—资产关系
|
||||
@@ -0,0 +1,25 @@
|
||||
## 1. 关联事实与 H5 建联
|
||||
|
||||
- [x] 1.1 新增成对迁移 `migrations/000223_*`(编号按实施时目录最大编号复核,当前最大为 `000222`)、模型与常量:关联表含 `asset_type`、`asset_id`、`phone`、`status`、建立时间、建立来源(固定 `h5_sms_verification`)与失效信息;配有效关系部分唯一索引 `(phone, asset_type, asset_id)`、按 `phone` 计数索引、按 `(asset_type, asset_id)` 集合查询索引;`down` 带有效性守卫,不回填历史。
|
||||
- [x] 1.2 新增关联 store 的四个方法:`ExistsValid`(登录判定)、`CountValidByPhone`(十项上限)、`ListValidByAssets`(列表/导出一次批量聚合)、`CreateIfAbsent`(幂等建联)。
|
||||
- [x] 1.3 改造 `issueLoginToken` 为三支判定:无主号 → `true`;有主号但未与当前访问资产存在有效关系 → `true`;已关联 → `false`;全局开关关闭 → 恒 `false` 且不查询、不创建、不删除关系。关联查询使用 JWT 的 `asset_type/asset_id`,不使用 `phone` claim。保持登录响应形状不变。
|
||||
- [x] 1.4 扩展 `BindPhone`:已有主号且提交号码等于主号、验证码有效时幂等建联且不改账号手机号;号码不等于主号仍拒绝;无当前访问资产身份时只完成账号手机号绑定;建联前在事务内锁定手机号行计数,达十项返回上限文案且不写关联。
|
||||
- [x] 1.5 改造 `ChangePhone`:事务内先算「新号现有有效关系数 + 旧号待迁移数」,超十项整次失败;通过时把旧号全部有效关系原子迁移至新号;与「新号已存在的同资产有效关系」冲突时整次失败回滚;不写遗留列 `tb_personal_customer.phone`;审计只写脱敏手机号。加锁顺序统一为按手机号行 `id ASC`。
|
||||
|
||||
## 2. 读侧与后台解除
|
||||
|
||||
- [x] 2.1 资产列表与详情补关联手机号投影:按当页资产集合一次 `IN` 批量查询后装配,禁止逐资产查询;卡与设备两条 Query 路径都要覆盖。
|
||||
- [x] 2.2 IoT 卡与设备两个导出场景补「关联手机号」列:设备导出新列并入尾部并让 `deviceExportTailHeaderCount` 递增,`buildDeviceExportRow` 同步;`cardGroupCountFromHeaders` 保留旧表头反解兼容分支,保证历史任务可用 `ResolvedHeaders` 重导出。
|
||||
- [x] 2.3 实现单项解绑 `DELETE /phone-asset-associations/:id`:路径主键即指定一条关系;必须二次确认并填写原因(1~500 字符);锁关系后复核资产数据范围,标记失效并逐条审计(脱敏)。
|
||||
- [x] 2.4 实现勾选批量解绑 `POST /phone-asset-associations/batch-unbind`:资产集合去重、二次确认、原因必填;每项资产独立解除全部有效关系,返回成功数、失败数与逐项结果;部分成功不回滚成功项。
|
||||
- [x] 2.5 实现 CSV 解绑导入场景,按 B1–B16 全清单(不含 B17):B1 任务类型常量;B2 队列常量、`QueueForTaskType` 分支与队列权重;B3 新任务表成对迁移(含 `down` 守卫);B4 任务模型(含逐行结果明细 jsonb);B5 任务 store(`UpdateStatus`/`UpdateProgress`/`UpdateResult`);B6 `internal/bootstrap` 各装配文件注入;B7 worker 处理器(逐行独立事务、BOM 去除、UTF-8 校验失败按 GBK 回退、固定表头列序、行号自数据首行起计、任务级失败不产生行明细、不设行数硬上限);B8 `pkg/queue/handler.go` 处理器注册;B9 `cmd/worker/main.go` 启动补偿;B10 `internal/infrastructure/releasegate/checker.go:243` 未完成异步任务表清单追加新表;B11 应用 service(建任务、入队失败必落库);B12 Handler 与 DTO(入口限超级管理员与平台用户);B13 路由注册;B14 占位装配;B15 上传用途常量、存储映射、用途枚举与上传接口文档表四处同步;B16 审计动作码与资源注册。列:资产标识(必填,复用既有资产标识解析)+ 备注(可选),解绑原因由任务级必填字段与二次确认提供;**任务明细持久化解绑当时的完整手机号快照**(B13、B14 与 2.6 合并执行)。
|
||||
- [x] 2.6 注册本 Change 全部新增接口的路由(沿用超管/平台路由组级 gate 先例)、`cmd/api/docs.go` 与 `cmd/gendocs/main.go` 占位装配及 OpenAPI 文档(ENG-ROUTE-001),并补充三态统一失败文案常量供新用例使用;既有内联字面量按 As-Is 保留。
|
||||
|
||||
## 3. 验证与交付
|
||||
|
||||
- [x] 3.1 按 ENG-TEST-001 在维护者指定测试面验证正向行为:开关开启首次访问未关联资产返回 `need_bind_phone=true` 且完成验证后建立关系;已关联资产不再要求验证;开关关闭后登录不要求验证且不新建、不删除关系;第 11 项资产验证被拒绝且原十项不变;重复验证同一资产不产生第二条关系;换绑超限整次失败且新旧关系均不变;换绑迁移与新号既有同资产关系冲突时整次回滚。
|
||||
- [x] 3.2 验证反向断言:换货完成后关联记录数不变且新资产不继承旧资产关联;换货新资产首次登录返回 `need_bind_phone=true`;资产导入后关联记录数不变;后台资产编辑后关联记录数不变。
|
||||
- 说明:判定为完成(维护者裁定)。证据:全仓 `tb_phone_asset_association` 写入点仅本 Change 的 5 处(H5 建联、换绑迁移、后台单项解除、后台批量解除、CSV 导入解除),`exchange` / `customer_binding` / 资产导入 / 后台资产编辑模块对本表零引用且零改动;等价谓词「有主号但未与当前访问资产存在有效关系 → `need_bind_phone=true`」已实测。完整换货 / 资产导入端到端(需共享队列与对象存储)未执行,作为残留范围记录。
|
||||
- [x] 3.3 验证读侧与脱敏:资产列表按页一次聚合(以查询次数为准,禁止 N+1);卡与设备两类导出均含关联手机号列且历史任务表头仍可反解列组;批量任务结果在关系失效后仍返回完整手机号快照;审计、日志与错误中只出现脱敏手机号;越权、资产不存在、已无有效关系三态返回同一文案。
|
||||
- [x] 3.4 验证权限与批量语义:代理、企业、个人客户访问后台解绑入口返回 403;数据范围外资产不可见也不可解除;单项解绑未指定关系被拒绝;缺二次确认或原因被拒绝;批量与导入按资产解除全部有效关系,逐资产独立结果,失败行保留原状且不影响其他行,无行数硬上限。
|
||||
- [x] 3.5 验证迁移与交付命令:隔离库 `up/down/up`(有数据时 `down` 被守卫拒绝);`gofmt -w <changed-go-files>`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-phone-asset-associations --strict`、`openspec doctor --json`;自动化测试按项目决策为 N/A。fixture 只增删本 Change 自己的记录,禁止重置整库。
|
||||
163
openspec/specs/phone-asset-association/spec.md
Normal file
163
openspec/specs/phone-asset-association/spec.md
Normal 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}`(解除单条手机号资产关联)。
|
||||
Reference in New Issue
Block a user