Files
junhong_cmp_fiber/openspec/changes/enforce-single-phone-per-asset/design.md
邹进 9ced14fa1b fix: 第1批 F1、M12 一资产一号与换货按手机号关联校验
- F1 一个资产同时只能有一个有效手机号关联,他号绑定返回 1192(不含号码),同号幂等
- 登录 need_bind_phone 改看资产是否已有有效关联,新增 asset_phone_bound、asset_phone_is_own;H5 绑定按客户/资产两侧状态分别写入
- 客户换号不再迁移资产关联
- M12 换货新资产有有效手机号关联即拒绝,号码只在 data.phone;换货完成清空旧资产客户绑定并失效旧资产关联(exchange_completed),均不迁移
- OpenSpec Change enforce-single-phone-per-asset:11/16,H5 短信与换货端到端验证待测试部署后完成
2026-10-12 00:10:41 +09:00

48 lines
4.6 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.
## Context
承接《第1批-8-9月迭代缺陷修复技术方案》(下称“方案”)第 5、6 节。
- `AssociationWriter.Establish`(`internal/service/phone_asset_association/association_write.go`)只限制一个手机号最多 10 个资产;部分唯一索引 `uq_phone_asset_association_valid (phone, asset_type, asset_id)`(迁移 `000223`)只防同号同资产重复。
- `BindPhone` 用同一号码同时写客户手机号与资产关联;`ChangePhone` 经 `migrateAssociations` 迁移关联;`need_bind_phone` 只看当前客户号码与资产是否关联。
- `ensureNewAssetBindingAvailableWithTx`(`internal/service/exchange/service.go:1071`)查 `tb_personal_customer_device`,有人扫过码即拒绝;`switchCustomerBindingWithTx`(`:1089`)调用 `customerBinding.Migrate` 迁移客户绑定,`Migrate` 只有换货一个调用方。
- 后台解除接口已存在(`internal/routes/phone_asset_association.go`):单条、按资产批量、CSV 导入、关联列表。
- 测试库 2026-10-11:有效关联 0 条;强制绑定开关已在测试环境打开(提交 `f56113ba`)。
## Goals / Non-Goals
**Goals:**
- 一资产一号,资产已绑定时不再出现资产侧绑定入口。
- 换货能被后台解除放行,换货后旧资产与原客户、原号码脱钩。
**Non-Goals:**
- 不加数据库唯一约束(已确认用程序控制)。
- 不做历史关联回填,不调整关联写入与强制绑定开关的耦合关系(方案第 5 节第 7 条,另行决定)。
- 不改“新号已被其他客户持有返回 1184”。
- 不新增后台解除接口。
## Decisions
1. **一资产一号只做“先查后写”,不加资产锁。** `Establish` 写入前查询该资产的有效关联再判断;不加 Redis 锁、不锁资产行、不加应用锁。2026-10-11 确认:两个不同号码在同一时刻为同一资产完成短信验证的概率极低,后果只是该资产多一个号,可在后台解除,不值得为此加锁。手机号侧既有的十项名额串行(`LockPhoneScopes`)保持不变。
2. **不加唯一约束。** 部分唯一索引 `(asset_type, asset_id) WHERE status = 1` 能兜底,但存量可能有多号资产,加约束前要先清理;已确认本批用程序控制,存量只读统计后交业务处理。
3. **绑定入口按两侧状态拆写。** `bindExistingPrimaryPhone` 不再要求提交号码等于客户手机号;客户侧与资产侧分别判断是否写入。错误码在 `pkg/errors` 新增,提示不带号码。
4. **`ChangePhone` 去掉 `migrateAssociations` 及其迁移审计。** 客户手机号与资产手机号独立,换号不应牵动资产。
5. **换货错误号码放 `data`。** 统一错误处理会把错误信息写日志(`pkg/errors/handler.go`),`data` 只随响应返回;用 `errors.NewWithData` 承载 `{"phone": ...}`,错误码沿用现有冲突类。
6. **换货清空旧资产客户绑定。** 新增 `customer_binding.ClearForExchange(ctx, tx, oldAssetType, oldAssetID)`:定位口径同原 `Migrate`;只处理 `status = 1`,置 0(沿用 `pcdOps.UpdateStatus`、`pciOps.GetByICCID` / `UpdateStatus`),审计角色用 `AuditResourceRolePersonalCustomerOldAssetBinding`。删除 `Migrate`、`migrateFromPCD`、`migrateFromPCI`。
7. **换货失效旧资产关联复用存储方法。** 用 `LockValidByAsset` + `InvalidateByIDs`(`phone_asset_association_store.go:215、229`),不复用 `Service.unbindAsset`:它自开事务并做后台数据范围校验,放不进换货事务。新增失效方式常量 `exchange_completed`(该列无约束,无迁移)。
## Risks / Trade-offs
- [历史上绑过号但没有关联记录的资产不会被换货拦截] → 取决于回填决定,本 Change 不处理。
- [新资产已有钱包余额或生效套餐时,迁移会叠加] → 现状如此,不拦截。
- [前端仍按旧 `need_bind_phone` 语义展示] → 测试环境交付时前端接入两个新布尔字段与 `data.phone`。
- [同一资产并发绑定两个号码] → 不加锁,接受极小概率;交付前与日常用只读统计“同一资产多条有效关联”发现,后台解除多余的。
- [原客户手上的旧资产页面在换货后失效] → 属预期,重新扫码即可进入,已在对外行为变化中告知运营与客服。
## Migration Plan
无数据库迁移。测试环境交付前只读统计“同一资产多条有效关联”,有则出清单交业务在后台解除。
## Open Questions
- 历史关联回填及口径、关联写入是否与强制绑定开关解耦:方案定为“发布时定”,不阻塞本 Change 开发。