Files
huang 2b3a9cb33f
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
feat: 新增套餐使用记录实付金额快照字段 paid_amount
- 新增迁移 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 订单表
2026-04-18 10:19:21 +08:00

101 lines
4.1 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.
# 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
无。时序、字段设计、回填策略均已确认。