8.3 KiB
Context
当前订单领域同时承载了三类不同语义:
- 谁操作了订单:应该回答“哪个账号发起了这次下单动作”。
- 订单属于谁/卖给谁:由
buyer_type/buyer_id/seller_shop_id/purchase_role等业务字段表达。 - 这笔订单的差价佣金该给谁:应从
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:calculate;worker 负责最终把订单落成“已完成+有佣金”“已完成+无佣金”或“待人工处理+链路异常”。
原因:
- 这样可以修复“平台代扣代理钱包但未入队”的现有缺口。
- “无佣金”订单也需要被 worker 明确盖章为完成,否则后台永远分不清是漏算还是确实没有佣金。
- Asynq 的幂等和补偿扫描可以继续复用,只需调整判定口径。
备选方案:
- 创建订单时先同步判断是否有佣金再决定是否入队:会把链路计算逻辑分散到订单服务,增加重复实现和口径漂移风险。
Decision 5:链路断裂统一落为待人工处理,并记录断点信息
选择:当父级链缺失、分配配置不存在、成本价无法继续求解等情况发生时,订单流程状态落为“待人工处理”,业务结果落为“链路异常”;同时保留待审佣金记录和断点备注,作为人工补偿入口。
原因:
- 这与现有
CommissionStatusPendingReview及待审记录机制兼容。 - 运营和财务可以明确知道这不是“无佣金”,而是“规则/数据有问题”。
备选方案:
- 仍然写成已完成但 0 佣金:会掩盖真实异常,导致漏补偿。
Risks / Trade-offs
- [历史订单语义混杂] → 先新增权威字段、兼容读取,再做分批回填;旧
operator_*不立即删除。 - [前端依赖旧
operator_name为店铺名] → 在提案中明确 BREAKING,订单响应补充账号名与业务店铺名分离的展示规则。 - [补偿扫描口径变化导致重复入队] → 继续以流程状态做幂等守卫,worker 内部使用条件更新和重复执行跳过。
- [多级链路查询带来性能波动] → 沿用现有逐级查询逻辑,但只在支付后异步执行;列表查询不做链路展开,避免影响接口时延。
- [历史挂起订单无法自动判断应为无佣金还是异常] → 回填脚本只处理可确定样本,其余保持待人工处理,由运营复核。
Migration Plan
- Schema 先行:为订单新增权威操作者字段与佣金结果字段,保留旧字段不删。
- 读路径兼容:订单详情/列表优先读新字段;若新字段为空,则按
creator + 旧 operator_*做兼容回显,并在响应中明确异常/历史场景。 - 写路径切换:后台订单创建、支付回调、自动完单全部改为写入新操作者字段,并统一使用新佣金状态/结果模型。
- 触发链修复:所有适用支付成功路径统一调用佣金入队逻辑;worker 在完成时写回流程状态和业务结果。
- 历史数据回填:优先回填可由
creator明确推断的后台订单;旧operator_id为店铺 ID 的记录不强改旧列,只填新列。 - 补偿与验证:扫描“已支付且待计算”的订单重新入队;手工验证顶级代理无佣金、多级链路有佣金、链路断裂待人工处理三类样本。
- 回滚策略:如新字段展示异常,可回退到兼容读逻辑并暂停新状态写入;旧字段仍保留,数据可读不丢失。
Open Questions
- 本次不保留开放性业务问题,默认采用“状态看流程、结果看业务”的双轴模型推进;如后续需要增加“计算中”状态,可在实现阶段作为非 breaking 扩展处理。