5.4 KiB
Context
当前 tb_order.total_amount 在后台代购/钱包支付场景被覆盖为成本价,导致:
- C 端订单列表/详情展示成本价(甚至 0 元)
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
- 执行数据库迁移:
tb_order_item加cost_price,tb_package_usage加retail_amount - 部署新代码(新订单开始写入正确的字段语义)
- 存量数据回填(
retail_amount从关联订单total_amount回填,已知不准确,仅作参考) - 无需回滚策略:新增字段有默认值,旧代码不读新字段,可安全回滚
回滚:新字段有 DEFAULT 0 / NULLABLE,回滚旧代码后新字段被忽略,不影响业务。