Files
junhong_cmp_fiber/openspec/changes/fix-order-operator-and-chain-commission-semantics/design.md
huang 66e12e9629
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
订单相关以及佣金相关修复
2026-04-30 09:52:39 +08:00

8.3 KiB
Raw Blame History

Context

当前订单领域同时承载了三类不同语义:

  1. 谁操作了订单:应该回答“哪个账号发起了这次下单动作”。
  2. 订单属于谁/卖给谁:由 buyer_type/buyer_id/seller_shop_id/purchase_role 等业务字段表达。
  3. 这笔订单的差价佣金该给谁:应从 seller_shop_id 出发,沿完整父级链逐级计算。

现状问题在于:operator_id/operator_type 被混成了“真实账号 + 店铺 ID + 平台空值”三种含义,导致平台订单无法稳定展示操作者;commission_status 又被混成“流程状态 + 业务结果”,导致“未触发计算”“已算完但没有佣金”“链路异常待处理”无法区分。历史提案已经明确:支付成功后要自动入队佣金计算,代购订单只跳过一次性佣金,不跳过差价佣金。因此本次设计不是新增分佣能力,而是把已有能力的语义纠偏为可长期维护的模型。

Goals / Non-Goals

Goals:

  • 将订单操作者统一收敛为“真实账号语义”,平台与代理都能准确落库、展示和追溯。
  • 将订单佣金模型拆成“流程状态 + 业务结果”两层,明确区分无佣金与异常待人工处理。
  • 将差价佣金统一定义为基于 seller_shop_id 的完整上级链分配,不再受 operator_id/operator_type 影响。
  • 修正平台代扣、代理自购、代理为下级代购、代购自动完单等触发链路,保证所有适用订单都能进入佣金计算。
  • 设计兼容迁移方案,避免直接改变旧字段含义造成历史数据误读。

Non-Goals:

  • 不重构一次性佣金规则,也不改变“代购订单不触发一次性佣金、不更新累计充值”的既有结论。
  • 不重写整套钱包模型,不额外引入外键、关联关系或新的异步基础设施。
  • 不在本次设计中解决所有历史脏数据,只定义兼容读取、渐进回填和后续修复路径。

Decisions

Decision 1新增“权威操作者快照”不直接复用被污染的旧 operator_* 字段

选择:订单模型新增一组权威操作者字段(账号 ID、账号类型、名称快照作为“谁操作了这笔订单”的唯一来源现有 operator_id/operator_type 视为历史兼容字段,在迁移期通过回填/映射兼容读取。

原因

  • 直接把旧 operator_id 改成账号 ID 会让历史上保存店铺 ID 的订单立即失真。
  • 单独依赖 creator 虽然能覆盖部分后台订单,但缺少名称快照,且无法优雅覆盖未来其他入口。
  • 新增权威字段可以让写路径和读路径先稳定下来,再逐步清理历史值。

备选方案

  • 直接复用 operator_id/operator_type:实现快,但会让历史数据语义混乱,风险最高。
  • 只依赖 creator/updater:不需要新字段,但响应层仍需额外查询/推导,且无法表达多入口场景下的操作者快照。

Decision 2佣金采用“双轴模型”——流程状态与业务结果分离

选择:保留 commission_status 作为流程状态字段,但重新定义为“待计算 / 已完成 / 待人工处理”;新增 commission_result 作为业务结果字段,至少表达“有佣金 / 无佣金 / 链路异常”。

原因

  • “无佣金”是计算结果,不是待处理状态。
  • “链路断裂”需要触发人工排障,不应与普通完成混淆。
  • 双轴模型最适合补偿扫描:补偿只看流程状态,运营展示同时看流程状态和业务结果。

备选方案

  • 仅扩展 commission_status 单字段:字段值会迅速膨胀,前后端更容易再次把流程与结果混用。
  • 仅依赖佣金记录条数推导结果0 条记录既可能是无佣金,也可能是任务未执行或链路异常,无法可靠区分。

Decision 3差价佣金只从 seller_shop_id 出发,沿完整父级链逐级计算

选择:佣金归属只依赖 seller_shop_id 的完整上级链,以及每一层对应的成本价/分配配置;operator_id/operator_type/purchase_role 不参与佣金归属判断,只参与审计和展示。

原因

  • 这样可以统一覆盖平台代扣、代理自购、代理为下级代购、多级代理链等所有场景。
  • 这与现有 commission_calculation 服务的主体算法一致,只是把现有“例子式理解”提升为正式规则。
  • 顶级代理“无佣金”只是父链终止后的自然结果,不需要单独特判。

备选方案

  • 按操作者类型分支:会错误跳过平台代扣二级/三级代理的上级差价佣金。
  • 按买家类型硬编码一级/二级场景:无法覆盖灵活多级代理链,且后续每加场景都要加特判。

Decision 4所有适用订单都要先进入佣金计算再由计算结果决定“有/无佣金”

选择:只要订单属于差价佣金适用范围,并在支付成功或自动完单后仍处于“待计算”,就必须入队 commission:calculateworker 负责最终把订单落成“已完成+有佣金”“已完成+无佣金”或“待人工处理+链路异常”。

原因

  • 这样可以修复“平台代扣代理钱包但未入队”的现有缺口。
  • “无佣金”订单也需要被 worker 明确盖章为完成,否则后台永远分不清是漏算还是确实没有佣金。
  • Asynq 的幂等和补偿扫描可以继续复用,只需调整判定口径。

备选方案

  • 创建订单时先同步判断是否有佣金再决定是否入队:会把链路计算逻辑分散到订单服务,增加重复实现和口径漂移风险。

Decision 5链路断裂统一落为待人工处理并记录断点信息

选择:当父级链缺失、分配配置不存在、成本价无法继续求解等情况发生时,订单流程状态落为“待人工处理”,业务结果落为“链路异常”;同时保留待审佣金记录和断点备注,作为人工补偿入口。

原因

  • 这与现有 CommissionStatusPendingReview 及待审记录机制兼容。
  • 运营和财务可以明确知道这不是“无佣金”,而是“规则/数据有问题”。

备选方案

  • 仍然写成已完成但 0 佣金:会掩盖真实异常,导致漏补偿。

Risks / Trade-offs

  • [历史订单语义混杂] → 先新增权威字段、兼容读取,再做分批回填;旧 operator_* 不立即删除。
  • [前端依赖旧 operator_name 为店铺名] → 在提案中明确 BREAKING订单响应补充账号名与业务店铺名分离的展示规则。
  • [补偿扫描口径变化导致重复入队] → 继续以流程状态做幂等守卫worker 内部使用条件更新和重复执行跳过。
  • [多级链路查询带来性能波动] → 沿用现有逐级查询逻辑,但只在支付后异步执行;列表查询不做链路展开,避免影响接口时延。
  • [历史挂起订单无法自动判断应为无佣金还是异常] → 回填脚本只处理可确定样本,其余保持待人工处理,由运营复核。

Migration Plan

  1. Schema 先行:为订单新增权威操作者字段与佣金结果字段,保留旧字段不删。
  2. 读路径兼容:订单详情/列表优先读新字段;若新字段为空,则按 creator + 旧 operator_* 做兼容回显,并在响应中明确异常/历史场景。
  3. 写路径切换:后台订单创建、支付回调、自动完单全部改为写入新操作者字段,并统一使用新佣金状态/结果模型。
  4. 触发链修复所有适用支付成功路径统一调用佣金入队逻辑worker 在完成时写回流程状态和业务结果。
  5. 历史数据回填:优先回填可由 creator 明确推断的后台订单;旧 operator_id 为店铺 ID 的记录不强改旧列,只填新列。
  6. 补偿与验证:扫描“已支付且待计算”的订单重新入队;手工验证顶级代理无佣金、多级链路有佣金、链路断裂待人工处理三类样本。
  7. 回滚策略:如新字段展示异常,可回退到兼容读逻辑并暂停新状态写入;旧字段仍保留,数据可读不丢失。

Open Questions

  • 本次不保留开放性业务问题,默认采用“状态看流程、结果看业务”的双轴模型推进;如后续需要增加“计算中”状态,可在实现阶段作为非 breaking 扩展处理。