Files
Break 944526d9ef
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m17s
修复订单金额落库不对的问题
2026-06-01 11:21:29 +08:00

78 lines
5.4 KiB
Markdown
Raw Permalink 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.
## Context
当前 `tb_order.total_amount` 在后台代购/钱包支付场景被覆盖为成本价,导致:
1. C 端订单列表/详情展示成本价(甚至 0 元)
2. `tb_package_usage.paid_amount` 快照的是成本价,资产套餐接口对所有后台账号一视同仁地暴露成本价
字段语义混乱的根源:`total_amount` 在 admin order service 中被 `buyerTotalCost` 覆盖,`retailTotalAmount` 计算出来后被丢弃。`actual_paid_amount` 本应是成本价,但 `total_amount` 也是成本价,两者语义重叠。
涉及模块:`internal/service/order``internal/service/client_order``internal/service/asset``internal/task``internal/model``internal/handler/admin`
## Goals / Non-Goals
**Goals:**
- `Order.TotalAmount` 始终 = 零售价客户面值C 端和后台均可安全展示
- `Order.ActualPaidAmount` = 实际支付成本价,钱包扣款以此为准
- `OrderItem.UnitPrice` = 零售单价,新增 `CostPrice` 字段存成本单价
- `PackageUsage.RetailAmount` 快照零售价,`PaidAmount` 继续快照成本价
- 资产套餐接口按调用方账号类型过滤:非平台账号不返回 `paid_amount`
**Non-Goals:**
- 不修改 C 端下单流程C 端本来就用零售价,无需改动)
- 不修改佣金计算逻辑(佣金基于 `SellerCostPrice`,不受此次影响)
- 不修改退款逻辑(退款金额基于 `ActualPaidAmount`,语义已正确)
- 不重命名 `PaidAmount` 字段(保持向后兼容)
## Decisions
### 决策 1`TotalAmount` 始终存零售价,`ActualPaidAmount` 存成本价
**选择**admin order service 中 `totalAmount` 始终赋值 `retailTotalAmount`,不再被 `buyerTotalCost` 覆盖;成本价只写入 `ActualPaidAmount`
**理由**`TotalAmount` 是订单的"面值",对客户可见;`ActualPaidAmount` 是"实付",是内部财务字段。两者语义本来就应该分离,当前代码是历史遗留的语义混用。
**钱包扣款影响**`createOrderWithWalletPayment` 目前用 `order.TotalAmount` 扣款,改为用 `order.ActualPaidAmount`。这是正确的——代理扣的是成本价,不是零售价。
**替代方案**:新增独立的 `RetailAmount` 字段存零售价,`TotalAmount` 保持现状。被否决,因为 `TotalAmount` 的字段名本身就暗示"订单总金额",应该是客户看到的面值,改语义比加字段更干净。
### 决策 2`OrderItem` 新增 `CostPrice` 字段
**选择**`tb_order_item` 新增 `cost_price BIGINT NOT NULL DEFAULT 0``UnitPrice` 改为存零售单价。
**理由**详情页需要展示每个套餐的单价C 端看零售价,后台平台账号可能需要看成本价做对账。两个字段各司其职,不互相覆盖。
**替代方案**:只在 order 级别存成本价(`SellerCostPrice` 已有item 级别不存。被否决,因为多套餐订单时无法还原每个套餐的成本单价。
### 决策 3`PackageUsage` 新增 `RetailAmount` 字段,`PaidAmount` 保持不变
**选择**:新增 `retail_amount BIGINT NULLABLE`,来源为 `order.TotalAmount`(零售价);`PaidAmount` 继续来源于 `order.ActualPaidAmount`(成本价)。
**理由**`PaidAmount` 已有存量数据和下游依赖,重命名风险高。新增字段向后兼容,存量数据通过迁移 SQL 回填。
### 决策 4资产套餐接口按账号类型过滤在 Service 层实现
**选择**`GetPackages` / `GetCurrentPackage` 新增 `callerAccountType string` 参数Service 层根据此参数决定是否填充 `PaidAmount`Handler 层从 middleware 读取账号类型后传入。
**理由**:过滤逻辑是业务规则,属于 Service 层职责。Handler 层只负责提取调用方身份并传递,不做业务判断。
**账号类型判断规则**`callerAccountType == "platform"` 时返回 `PaidAmount`,其他类型(`agent``enterprise``personal_customer`)不返回。
## Risks / Trade-offs
- **[风险] 钱包扣款金额变化**:改用 `ActualPaidAmount` 扣款后,代理自购场景扣款金额不变(成本价),但需确认所有扣款路径都已切换,避免遗漏。→ 迁移时逐一检查 `createOrderWithWalletPayment` 的所有调用点。
- **[风险] 存量订单数据不一致**:历史订单的 `TotalAmount` 已经是成本价,无法回填零售价(零售价快照未存储)。→ 接受此历史数据问题仅对新订单生效C 端展示历史订单时可能仍显示成本价,属于已知限制。
- **[风险] `PackageUsage.RetailAmount` 存量数据为 null**:历史套餐使用记录无零售价快照。→ 迁移 SQL 尝试从关联订单回填,但历史订单 `TotalAmount` 已是成本价,回填值不准确。接受此限制,存量数据 `RetailAmount` 保持 null接口返回时 omitempty 处理。
- **[Trade-off] 赠送套餐0 元)场景**`TotalAmount` = 零售价,`ActualPaidAmount` = 0。C 端看到零售价,后台看到实付 0 元。这是期望行为(已与业务确认)。
## Migration Plan
1. 执行数据库迁移:`tb_order_item``cost_price``tb_package_usage``retail_amount`
2. 部署新代码(新订单开始写入正确的字段语义)
3. 存量数据回填(`retail_amount` 从关联订单 `total_amount` 回填,已知不准确,仅作参考)
4. 无需回滚策略:新增字段有默认值,旧代码不读新字段,可安全回滚
**回滚**:新字段有 `DEFAULT 0` / `NULLABLE`,回滚旧代码后新字段被忽略,不影响业务。