Files
junhong_cmp_fiber/openspec/changes/archive/2026-03-19-client-exchange-system/design.md
huang b9733c4913
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
fix: 修正零售价架构错误 + 清理旧微信配置 + 归档提案 + 前端接口文档
1. 修正 retail_price 架构:
   - 删除 batch-pricing 接口的 pricing_target 字段和 retail_price 分支
     (上级只能改下级成本价,不能改零售价)
   - 新增 PATCH /api/admin/packages/:id/retail-price 接口
     (代理自己改自己的零售价,校验 retail_price >= cost_price)

2. 清理旧微信 YAML 配置(已全部迁移到数据库 tb_wechat_config):
   - 删除 config.yaml 中 wechat.official_account 配置节
   - 删除 NewOfficialAccountApp() 旧工厂函数
   - 清理 personal_customer service 中的死代码(旧登录/绑定微信方法)
   - 清理 docker-compose.prod.yml 中旧微信环境变量和证书挂载注释

3. 归档四个已完成提案到 openspec/changes/archive/

4. 新增前端接口变更说明文档(docs/前端接口变更说明.md)

5. 修正归档提案和 specs 中关于 pricing_target 的错误描述
2026-03-19 17:39:43 +08:00

150 lines
6.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.
# 设计文档客户端换货系统client-exchange-system
## 背景与上下文
现有换卡能力基于 `CardReplacementRecord`,仅覆盖“老卡→新卡”的窄场景,无法支撑本次目标中的完整换货闭环(后台发起、客户端填收货、后台发货、确认完成、可选全量迁移、旧资产转新再销售)。
当前主要问题:
1. **模型能力不足**`internal/model/card_replacement.go:11-71`
- 只支持卡换卡,不支持设备换设备。
- 缺少客户端收货地址、后台物流信息、迁移结果字段。
- 状态机不匹配本次流程(待填写→待发货→已发货待确认→已完成/已取消)。
2. **历史代码字段不一致风险**`internal/store/postgres/iot_card_store.go:644-655`
- 现有查询使用 `old_iot_card_id` 维度过滤换卡记录,但旧模型字段命名是 `old_card_id`,存在语义/列名不一致隐患。
- `is_replaced` 逻辑依赖旧表,不适配新换货单模型。
3. **旧模型未纳入统一迁移体系**
- `CardReplacementRecord` 没有持续参与当前主线 AutoMigrate 维护,演进风险高。
4. **资产迁移链路涉及多模型联动,旧方案无法表达**
- 钱包与流水:`internal/model/asset_wallet.go:9-35``resource_type + resource_id`
- 套餐使用:`internal/model/package.go:57-87`
- 标签:`internal/model/tag.go:25-41`
- 客户设备绑定:`internal/model/personal_customer_device.go:9-23`
- 设备卡绑定:`internal/model/device_sim_binding.go:9-24`
- 分佣记录:`internal/model/commission.go:9-30`
- 流量明细:`internal/model/data_usage.go:7-23`
- 卡累计字段:`internal/model/iot_card.go:41-44``FirstCommissionPaid``AccumulatedRecharge``AccumulatedRechargeBySeriesJSON``FirstRechargeTriggeredBySeriesJSON`
5. **模块接入需遵循统一 Bootstrap 装配模式**
- 参考 `internal/bootstrap/handlers.go:12-62``internal/bootstrap/types.go:13-60`
## 目标与非目标
### Goals
1. 提供完整换货生命周期能力:
- 后台 7 个接口H1~H7
- 客户端 2 个接口G1~G2
2. 在 H5 确认完成时支持可选“全量迁移”11 张表规则)。
3. 支持旧资产“转新”再销售generation+1、状态重置、历史隔离
4. 替换旧换卡模型引用,统一到 ExchangeOrder。
### Non-Goals
1. 不对接第三方物流轨迹查询(仅记录物流公司/单号)。
2. 不实现主动消息推送(客户端通过 G1 轮询换货通知)。
## 关键设计决策
### 决策 1ExchangeOrder 模型设计
引入新模型 `ExchangeOrder`,作为换货生命周期唯一事实来源,字段覆盖:
- 基础字段:`gorm.Model + BaseModel`
- 单号:`exchange_no`
- 旧资产快照:`old_asset_type``old_asset_id``old_asset_identifier`
- 新资产快照:`new_asset_type``new_asset_id``new_asset_identifier`
- 收货信息:`recipient_name``recipient_phone``recipient_address`
- 物流信息:`express_company``express_no`
- 迁移结果:`migrate_data``migration_completed``migration_balance`
- 业务信息:`exchange_reason``remark``status`
- 多租户:`shop_id`
换货单号生成规则:`EXC` + 日期 + 随机数(示例:`EXC20260319XXXXXX`)。
### 决策 2状态机由 Service 层强校验
`status` 采用 int 常量:
- 1 待填写信息
- 2 待发货
- 3 已发货待确认
- 4 已完成
- 5 已取消
状态流转在 Service 层校验,不使用数据库触发器。理由:
1. 业务规则集中在 Go 代码,便于复用和审计。
2. 避免跨环境数据库触发器差异。
3. 更易与错误码体系、权限体系协同。
### 决策 3发货时执行同类型资产校验
在 H4 发货阶段强制校验:
1. `new_asset_type == old_asset_type`(卡换卡 / 设备换设备)。
2. 新资产必须 `asset_status=1`(在库)。
该校验放在“发货”而非“创建”,因为创建时允许先立单、后备货。
### 决策 4全量迁移使用单一大事务11 张表)
H5 在 `migrate_data=true` 时,使用**一个数据库事务**完成 11 张表相关操作。理由:
1. 迁移一致性优先,必须保证“要么全成功,要么全失败”。
2. 换货属于低频运营操作,非高并发核心交易路径。
3. 单资产迁移涉及行数有限,可接受事务时间。
补充规则:设备换设备时,不迁移 `DeviceSimBinding`(新设备视为自带新卡体系)。
### 决策 5转新采用 generation 隔离历史
H7 转新时:
1. `generation = generation + 1`
2. 不删除旧代际历史数据(订单、充值、分佣、流量等)
3. 创建新空钱包(新 `wallet_id` 天然隔离流水)
4. 清除累计充值/首充触发状态
5. 清除客户绑定关系
通过“新代际 + 新钱包”实现可回收再销售,同时不破坏历史可追溯。
### 决策 6旧模型降级为 legacy不回灌迁移
`CardReplacementRecord` 对应表改名为 `tb_card_replacement_record_legacy`,但**不迁移历史数据到新表**。理由:
1. 旧数据量小,保留查询价值即可。
2. 历史数据结构与新模型语义不完全一致,强行回灌成本高且收益低。
`iot_card_store.go``is_replaced` 过滤逻辑改为查询 `ExchangeOrder`,不再依赖旧表。
### 决策 7依托现有多租户 Callback 自动过滤
`ExchangeOrder` 增加 `shop_id` 字段,直接接入现有 GORM 数据权限 Callback避免重复实现权限 where 条件。
## 风险与权衡
1. **[风险] 全量迁移事务锁表时间增长**
- 权衡:换货低频,且单次仅操作单资产关联记录,影响可接受。
2. **[风险] 转新后旧客户仍持有旧虚拟号认知**
- 权衡:`PersonalCustomerDevice` 绑定会清除,旧客户再次登录会被要求重新绑定,避免继续访问新代际资产。
3. **[风险] 设备换设备不迁移 DeviceSimBinding 造成“看起来少迁移”**
- 权衡:这是显式设计决策;新设备按“新硬件+新卡”交付,旧设备卡绑定保留历史关系。
4. **[风险] 迁移期 CMP 与 Gateway 状态观测不一致**
- 权衡:本次迁移仅操作 CMP 数据库,不调用 Gateway运营商侧状态由新资产实际使用逐步收敛。
## 迁移计划
1. 新建 `tb_exchange_order` 表。
2.`tb_card_replacement_record` 改名为 `tb_card_replacement_record_legacy`
3. 代码层替换:
- Store/Service/Handler 查询改用 ExchangeOrder
- `is_replaced` 等旧逻辑改为新表判定
4. 在 bootstrap、routes、docs 生成器中注册新 Handler`cmd/api/docs.go``cmd/gendocs/main.go`)。