## 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:calculate`;worker 负责最终把订单落成“已完成+有佣金”“已完成+无佣金”或“待人工处理+链路异常”。 **原因**: - 这样可以修复“平台代扣代理钱包但未入队”的现有缺口。 - “无佣金”订单也需要被 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 扩展处理。