## 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`,回滚旧代码后新字段被忽略,不影响业务。