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 订单表
101 lines
4.1 KiB
Markdown
101 lines
4.1 KiB
Markdown
# 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 个写入点**:
|
||
1. `internal/service/order/service.go` — `activateMainPackage()` / `activateAddonPackage()`(支付后激活)
|
||
2. `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` 一次性回填
|
||
|
||
```sql
|
||
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
|
||
|
||
1. 执行迁移 `000129_add_paid_amount_to_package_usage.up.sql`:
|
||
- `ALTER TABLE tb_package_usage ADD COLUMN IF NOT EXISTS paid_amount BIGINT`
|
||
- `UPDATE ... FROM tb_order` 回填历史数据
|
||
2. 部署代码(Model / Service / DTO)
|
||
3. 新的套餐激活记录自动写入 `paid_amount`
|
||
|
||
**回滚**:执行 `.down.sql` DROP COLUMN,代码回滚到上一版本。
|
||
|
||
## Open Questions
|
||
|
||
无。时序、字段设计、回填策略均已确认。
|