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

4.6 KiB
Raw Blame History

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 开发。