- 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 短信与换货端到端验证待测试部署后完成
48 lines
4.6 KiB
Markdown
48 lines
4.6 KiB
Markdown
## 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 开发。
|