All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
- 新增迁移 000129:tb_package_usage 添加 paid_amount BIGINT 字段,存量数据通过 JOIN tb_order 回填 - PackageUsage Model 新增 PaidAmount *int64 字段 - 4 个写入点(order service 主套餐/加油包、auto_purchase 主套餐/加油包)赋值 order.ActualPaidAmount - AssetPackageResponse DTO 新增 paid_amount 字段 - GetCurrentPackage / GetPackages 填充 paid_amount,接口直接返回购买价格无需 JOIN 订单表
4.1 KiB
4.1 KiB
Design: add-paid-amount-snapshot-to-package-usage
Context
当前 tb_package_usage 表在套餐激活时快照了 package_name,但未快照购买实付金额。paid_amount 字段需要从 tb_order.actual_paid_amount 获取,目前只能通过 order_id JOIN 订单表实现。
当前 PackageUsage 创建路径有两条,各含主套餐和加油包,共 4 个写入点:
internal/service/order/service.go—activateMainPackage()/activateAddonPackage()(支付后激活)internal/task/auto_purchase.go—activateMainPackage()/activateAddonPackage()(C 端充值自动购包)
时序保证:两条路径创建 PackageUsage 时,order.ActualPaidAmount 均已赋值:
- 钱包支付 / 自动购包:Order 创建时即写入
actual_paid_amount - 微信 / 支付宝:支付回调
HandlePaymentCallback先更新actual_paid_amount,再调用activatePackage() - 线下支付:
actual_paid_amount = nil(无实际收款,字段允许 null)
约束:禁止外键、禁止 GORM 关联(项目规范),不能依赖 JOIN 查询。
Goals / Non-Goals
Goals
- 在
tb_package_usage中存储购买时的实付金额快照 - 使资产套餐两个接口(current-package / packages)直接返回
paid_amount,无需 JOIN - 存量数据通过迁移 SQL 回填(JOIN 仅在一次性迁移中使用)
Non-Goals
- 不修改订单表结构
- 不改变套餐购买业务流程
- 不修改 C 端 / H5 相关接口
- 不修改其他展示场景(如订单列表、分佣等)中的价格来源
Decisions
决策 1:快照字段还是 JOIN 查询
选择:快照字段(在 tb_package_usage 新增 paid_amount)
理由:
- 项目规范禁止 GORM 关联,JOIN 需手动实现,读频繁时性能代价不可忽视
- 快照语义更清晰:记录的是"购买时的价格",而非"当前订单的价格"
package_name已有先例,模式一致
备选方案:JOIN tb_order — 被否决,原因是高频读场景性能差、与快照设计原则不符。
决策 2:字段类型
选择:BIGINT NULLABLE,Go 侧 *int64
理由:
- 与项目所有金额字段统一(单位:分,bigint)
- Nullable 而非
NOT NULL DEFAULT 0:线下支付actual_paid_amount为 null,区分"0元"和"无实付"语义不同 - 存量
order_id = 0的记录(如企业无订单直接分配套餐)回填为 null
备选方案:NOT NULL DEFAULT 0 — 被否决,0 与"无实付"语义歧义。
决策 3:赋值来源
选择:直接读 order.ActualPaidAmount,不需要调用方额外传参
理由:
- 4 个写入点创建
PackageUsage时均已持有完整order对象 - 避免调用链增加参数,改动最小
决策 4:存量数据回填策略
选择:迁移 SQL 中 UPDATE ... FROM tb_order 一次性回填
UPDATE tb_package_usage pu
SET paid_amount = o.actual_paid_amount
FROM tb_order o
WHERE pu.order_id = o.id
AND pu.order_id != 0
AND pu.paid_amount IS NULL;
理由:
- 历史数据量有限,一次性回填可接受
order_id = 0的记录(无订单分配)保持 null,语义正确
Risks / Trade-offs
| 风险 | 缓解措施 |
|---|---|
| 写入点遗漏:未来新增的 PackageUsage 创建路径忘记赋值 | 在 Model 层 PackageUsage 字段注释中标注"创建时必须从 order 赋值" |
| 存量回填失败(迁移中途中断) | 迁移 SQL 使用 IF NOT EXISTS + UPDATE 幂等写法,重跑安全 |
| 线下支付 null 值前端未处理 | DTO 字段使用 *int64(omitempty),前端已有 null 处理惯例 |
Migration Plan
- 执行迁移
000129_add_paid_amount_to_package_usage.up.sql:ALTER TABLE tb_package_usage ADD COLUMN IF NOT EXISTS paid_amount BIGINTUPDATE ... FROM tb_order回填历史数据
- 部署代码(Model / Service / DTO)
- 新的套餐激活记录自动写入
paid_amount
回滚:执行 .down.sql DROP COLUMN,代码回滚到上一版本。
Open Questions
无。时序、字段设计、回填策略均已确认。