Commission 取消、改价与退款 DDD 业务模型

文档状态:目标领域模型基线
基线日期:2026-09-16
需求来源:COMMISSION_REFUND_PRD.md
API 契约:../api-changes/2026-09-09_commission_cancellation_and_refunds.md

本文只描述长期领域模型、聚合边界、不变量和可靠性语义。代码清理步骤放在独立执行计划中,历史审计和旧原型不作为当前模型的一部分。

1. 问题域

Commission 的复杂度来自四类事实同时存在:

  1. 合同事实:当前价格、阶段、是否取消、是否完成;
  2. 支付事实:多笔 Order、多资金来源、渠道原币、锁定汇率和手续费;
  3. 客户债权:平台当前欠客户多少、是否已履行;
  4. 资金收尾:Provider 退款、关联账户回收、钱包恢复、平台费和最终结算。

这些事实的生命周期不同。取消或降价可以已经生效,但退款仍在等待用户选择;客户可以已经收到退款,但画师关联账户回收仍失败。因此模型不能用一个 status 同时承载所有含义。

2. 限界上下文

上下文聚合根负责不负责
CommissionWorkTask合同、阶段、支付入口、最终状态执行退款
Price ChangeWorkTaskPriceChange改价意愿、审批、补款、价格应用判断客户是否已退款
CancellationCommissionCancellation取消协商、取消业务状态、协调退款与取消结算计算渠道拆分
Financial ResolutionCommissionFinancialResolution把已生效业务决定冻结为不可变资金决议并等待依赖直接调用 Provider
RefundCommissionRefund客户退款债权、资金预留、执行计划和履行状态修改合同价格或取消决定
SettlementSettlement Plan 与结算服务画师收入、平台费、减免、钱包与完成结算决定退款去向
PaymentOrder、Payment、钱包扣款事实已支付金额、支付渠道、费用和资金来源解释退款业务原因
Operations后台查询、受控命令、审计日志恢复和核对既有事实绕过领域规则改钱

上下文之间只通过 ID、不可变报价和稳定状态事实协作。任何调用方都不能通过修改另一个聚合的字段来模拟其状态迁移。

3. 通用语言

术语定义
业务币种Commission 合同与双方协商退款使用的币种
渠道原币Provider 实际收款和退款使用的币种
退款债权平台因一次已生效业务决定而对客户承担的一笔退款义务
开放债权obligation_status = open,仍占用资金预留的债权
履行客户退款步骤已经成功,债权变为 fulfilled
作废债权由补偿性业务决议关闭且不再占用资金,变为 voided
资金预留将特定 Order 和资金来源的一部分锁给某笔债权,防止后续重复使用
去向偏好用户在业务决定生效前表达的可选意愿,不保证最终可用
正式去向债权当前冻结的 cash_destination
财务报价对当前支付、退款、预留、费率和结算事实的计算结果
财务决议业务决定生效时记录的不可变资金计划与依赖
客户履约步骤决定客户是否收到退款的步骤
Recovery 步骤回收画师或平台资金的步骤,不改变客户已收到退款的事实
对账确认 Provider 或内部账本已经发生的事实,不重新执行操作
退款池一张 Commission 下开放债权的摘要读模型

Open Call 是 project 的对外展示名称,Commission 是 worktask 的对外展示名称。

4. 值对象

4.1 Money

Money = integer minor amount + Currency

Currency 至少包含 id、code、symbol、is_zero_decimal。不同币种的 Money 不能直接相加。数据库继续保存整数最小单位,API 边界返回自包含 Money。

4.2 CommissionFinancialQuote

财务报价冻结以下事实:

  • 旧合同金额、新合同金额;
  • 已支付金额;
  • 已完成退款和开放预留;
  • 本次新增退款义务;
  • 后续待支付金额;
  • 画师目标净额、已结算额和调整动作;
  • 支付手续费和平台费政策来源;
  • 退款能力和去向偏好应用结果;
  • 计算版本、快照时间和指纹。

Quote 是业务决议的输入,不是可长期编辑的草稿。业务事实变化后必须重新报价。

4.3 RefundCapability

能力是整张 Commission 的计算结果:

can_exceed_standard
can_refund_to_credit
maximum_policy
unavailable_reason

它由所有已付款 Order 的渠道、collection mode、关联账户、钱包混付和数据完整性共同决定,不能只检查本次准备退款的单笔 Order。

5. 聚合模型

5.1 WorkTaskPriceChange

职责:记录一次价格变更请求及其业务审批。

关键字段:

  • old_price、new_price、currency_id;
  • initiator_type/id、approver_type/id;
  • status;
  • need_pay_amount、work_task_paid_amount;
  • refund_destination:用户提交的提前偏好;
  • business_refund_amount:生效时冻结的内部金额事实;
  • refund_calculation_snapshot/fingerprint;
  • commission_financial_resolution_id、commission_refund_id。

不变量:

  • 同一 Commission 同时最多一笔 pending 或 wait_pay 改价;
  • 新价格不能等于当前价格;
  • 降价生效和 Financial Resolution 记录在同一事务;
  • paid 只表示新价格已应用;
  • 改价对象不执行退款,也不根据退款结果回滚价格。

5.2 CommissionCancellation

职责:记录双方取消协商,并编排取消后的退款与结算。

关键字段:

  • 发起人、角色和响应人;
  • status、失败阶段和错误事实;
  • 协商退款额、用户去向偏好、计算快照与指纹;
  • commission_financial_resolution_id、commission_refund_id;
  • 接受、拒绝、撤回、完成时间。

不变量:

  • 同一 Commission 同时最多一笔活动取消;
  • 发起方只能撤回,对方只能接受或拒绝;
  • 画师不能设置退款去向;
  • 接受时必须重新验证条款指纹;
  • 接受后不因退款执行失败回滚业务决定;
  • 未完成退款或取消结算时不能把取消标记为完成。

5.3 CommissionFinancialResolution

职责:把已经生效的取消、降价或完成结算决定冻结为不可变资金事实。

关键字段:

  • trigger_type + trigger_id 唯一;
  • 合同、支付、退款、预留和画师调整快照;
  • calculation_snapshot/fingerprint;
  • 冻结的 Settlement Plan 与执行结果;
  • 退款依赖、状态、人工复核原因和时间。

不变量:

  • 同一业务事件只能有一条决议;
  • 决议记录后不修改金额,纠错通过新的补偿性业务决议完成;
  • Settlement Plan 按记录时费率、政策和钱包动作执行;
  • 所有依赖关闭、Recovery 完成且结算完成后才能进入 completed;
  • 失败恢复只能继续原计划,不能重新报价。

当前有效触发类型只有 cancellation、price_change、completion。不为没有业务入口的 Admin Adjustment 预留类型。

5.4 CommissionRefund

职责:表示一笔客户退款债权,并作为退款执行聚合根。

关键字段:

  • work_task_id、financial_resolution_id;
  • trigger_type + trigger_id;
  • obligation_status、execution_status;
  • cash_destination、preference_applied、action_required_at;
  • 业务退款金额与币种;
  • 计算版本、执行快照与指纹;
  • 执行尝试、错误、对账和完成事实。

不变量:

  • 正金额且计算完整才能创建债权;
  • 每笔债权必须有完整 allocation;
  • 开放债权始终占用 allocation;
  • 未选去向不能创建 Provider 执行步骤;
  • 选择去向后不能更换;
  • 同一请求幂等键只能对应相同条款;
  • 客户履约步骤决定 obligation_status;
  • 所有步骤和对账事实决定 Financial Resolution 能否收尾。

CommissionRefund 不包含旧单轴 status 或 destination 别名。

5.5 CommissionRefundAllocation

Allocation 是债权对资金来源的持久化预留:

  • Order;
  • source type/key;
  • Credit 钱包扣款记录;
  • 业务金额和业务币种;
  • 来源原币金额和币种;
  • channel、collection mode、Provider payment/session、关联账户;
  • 锁定汇率来源;
  • reserved、fulfilled、released 状态。

历史退款计算只读取 allocation,不从任意 JSON 字符串 key 反推账本。

5.6 CommissionRefundStep

Step 是最小可幂等执行单元:

  • 稳定 operation_key;
  • 全局唯一 idempotency_key;
  • type、financial_effect、顺序;
  • 金额、币种、Provider payload/reference/response;
  • 状态、尝试次数和租约。

同一债权与 operation key 唯一。已成功步骤永不重建、永不重放。

6. 状态机

6.1 Cancellation

取消 API 可投影 business_status 与 financial_status,帮助前端区分协商事实和资金进度;持久化 CommissionCancellation.status 仍是取消聚合的流程状态,不是退款兼容字段。

6.2 Refund obligation

6.3 Refund execution

execution_status = completed 表示客户履约执行完成;Recovery 与 Financial Resolution 的最终完成仍需单独核对。

execution_status = not_required 是 obligation_status = voided 的专用执行轴:债权在客户资金尚未开始移动前被补偿性业务决议关闭,因此没有需要执行的步骤。它既不是 retryable_failed(没有失败),也不是 completed(isCustomerFulfilled() 仍只对 completed 成立)。两个轴在同一个事务内一起写入,作废后不再接受去向选择或执行,陈旧执行消息是幂等 no-op。

存量数据中曾被投影为 retryable_failed 的作废债权由 2026_09_17_000003 规范化为 not_required;该迁移在写入前校验作废债权没有已开始/成功的客户履约步骤且 allocation 全部 released,否则中止并要求人工核查。

6.4 Financial Resolution

没有生产入口的 voided Resolution 状态不属于当前模型。需要撤销决议时,应先定义补偿业务和审计规则,再新增状态。

6.5 Price Change

pending -> wait_pay -> paid
pending -> paid       # 降价批准或画师直接降价
pending -> rejected
pending -> canceled
wait_pay -> paid
wait_pay -> canceled

退款进度不进入改价状态机。

7. 财务不变量

7.1 三层上限

standard = max(paid - actual_payment_fee - completed_refunds - open_reservations, 0)
absolute = max(paid - completed_refunds - open_reservations, 0)
maximum  = capability.can_exceed_standard ? absolute : standard

requested_refund <= maximum 是硬约束。普通场景最大值就是 standard;所有已付款 Order 都是纯 Stripe Destination Charge 时 maximum 才可等于 absolute。

7.2 分配守恒

sum(allocation.business_amount) = refund.business_refund_amount
sum(order allocations) = order business refund amount
sum(source allocations after conversion) = frozen source value

取整差额使用确定性的最大余数法,并以稳定 key 排序,保证重算结果一致。

7.3 结算口径

  • 支付手续费使用已发生 Order 的真实费用快照。
  • 平台费比例来自 Artist 配置和冻结政策,不使用常量。
  • 改价预测按变更后合同总价计算平台费,并纳入已发生与后续支付手续费预估。
  • 取消与完成结算复用正常 WorkTask 结算规则。
  • 画师目标净额允许在纯 Stripe 关联账户场景为负;平台内部钱包不能靠猜测承担负额。

8. 协作与流程编排

8.1 取消接受

8.2 降价生效

改价聚合只负责应用新价格。Financial Resolution 根据累计退款目标减去已完成退款和开放预留,创建差额债权。债权是否等待去向,不影响新价格已经生效的事实。

8.3 完成结算

完成决议记录时,把当时所有开放债权登记为依赖。依赖要求是债权关闭,即 fulfilled 或 voided。依赖满足后仍要检查 Recovery、对账和冻结 Settlement Plan,全部完成才允许 WorkTask 最终结算。

当前依赖模型只使用 customer_refund + closed。没有实际调用方的依赖类型和条件不进入领域枚举。

9. 退款执行

Step typeFinancial effect说明
stripe_refundcustomer fulfillmentStripe 原路退款
paypal_refundcustomer fulfillmentPayPal 原路退款
alipay_refundcustomer fulfillmentAlipay 原路退款
user_credit_refundcustomer fulfillment用户 Credit 入账
user_wallet_refundcustomer fulfillment用户钱包退款
stripe_transfer_reversalartist/platform recoveryStripe Transfer 冲正
stripe_connected_account_debitartist recovery关联账户扣款
stripe_connected_account_creditartist/platform recovery关联账户补足
artist_wallet_recoveryartist recovery回收已入账画师钱包资金

Step 的 financial_effect 决定它是否影响客户债权。不能根据 Step type 字符串在多个服务中重复推断。

执行顺序原则:先建立客户退款和必要的 Provider 资金动作,再根据 Provider 事实完成内部钱包写入与对账。具体步骤顺序由冻结计划决定,恢复只能续跑未成功步骤。

10. 事务、幂等与并发

10.1 事务边界

以下事实必须一起提交:

  • 业务状态变化;
  • Financial Resolution;
  • Refund 与 allocations;
  • Refund steps;
  • Outbox 执行意图。

10.2 锁纪律

  • 业务生效时锁 WorkTask;
  • 选择去向、重试和执行时锁 Refund;
  • 决议推进时锁 Financial Resolution;
  • 恢复扫描在锁内再次检查状态和投递陈旧性;
  • 资金写入同时依靠行锁、唯一 operation key 和 settlement executed fact。

10.3 幂等层次

层次Key
用户请求request_idempotency_key
业务事件trigger_type + trigger_id
债权资金来源refund_id + source_key
执行步骤refund_id + operation_key、全局 idempotency key
Provider稳定 Provider idempotency key/reference
Outboxtopic + aggregate + 未完成投递唯一语义
完成结算frozen plan fingerprint + executed fact + 唯一钱包 operation

11. 快照与指纹

运行时只接受当前格式的完整快照:

  • Calculation Snapshot:支付、费用、能力、资金来源与渠道换算;
  • Financial Quote Snapshot:决议金额和政策来源;
  • Settlement Plan:实际结算动作;
  • Source Snapshot:allocation 对应的 Provider 和锁定价值;
  • Reconciliation Snapshot:Provider 核实结果。

快照生成后使用键排序规范化再计算指纹。执行前核对主体、币种、金额、计划版本和关键 Provider 引用。缺失当前必需字段属于数据完整性错误,不能回退到旧 JSON 结构或按实时配置重新计算。

没有支付事实的加价或未付款改价可以合法没有退款 calculation snapshot;这是业务上的空退款场景,不是旧数据兼容。只要记录声明存在正退款额或已形成退款决议,就必须有完整快照。

12. Outbox 与恢复

当前 Topic:

TopicAggregateConsumer
commission.refund.executeRefundExecuteCommissionRefund
commission.financial_resolution.resumeFinancial ResolutionResumeCommissionFinancialResolution
commission.cancellation_settlement.executeCancellationExecuteCommissionCancellationSettlement

不保留没有消费者的历史 Topic。

Outbox 每分钟投递,财务恢复扫描每五分钟运行。陈旧阈值必须大于队列 retry window、Job timeout 和步骤租约;queue.retry_after 必须大于同一连接上 Job 的最大 timeout。

恢复扫描只发布持久化意图,不直接改钱。Job 在执行前重新经过状态和依赖门禁。

fulfilled 债权的收尾投影(取消 hand-over、依赖 satisfied_at)也由同一条扫描补齐。已经停放 cancellation_handover_failed 的债权会被排除在自动扫描之外,不再消耗每轮预算,也不会饿死后续健康候选:它们只能由人工受控的 reconcile / resume 处理,收尾成功后该 blocker 被清除。CommissionFinancialResolution.status = completed 是不可降级的终态——陈旧 Finalizer 只把失败事实写到 Refund 诊断和尚未完成的关联 Resolution 上,绝不把已完成的 Resolution 改回 manual_review。

取消退款的恢复身份是 Refund 的不可变来源 trigger_type + trigger_id,而不是 hand-over 成功后写入的 commission_refund_id 投影:该投影为空、错指或对应取消行缺失时,扫描必须仍能通过来源身份发现并安全停放,不能用待修复的投影去发现它自身的损坏。停放(cancellation_handover_failed)只适用于已证明的持久领域冲突(领域 BusinessException/ValidationException);数据库死锁、连接与查询错误、TypeError 和未知程序异常必须整体回滚并继续上抛,保留队列与下一轮扫描的重试能力,不得被伪装成人工审核事实。单条瞬时失败的记录记入 deferred,不消耗本轮修复预算,也不阻止更高 ID 的候选。

13. 对账与人工复核

对账资格至少要求:

  • 债权 fulfilled;
  • 执行状态 completed;
  • 执行计划存在;
  • 所有步骤 succeeded;
  • Stripe refund/reversal 配对完整;
  • 必需 Provider reference 存在;
  • 尚未完成相同对账。

对账成功只补写事实并推动 Finalizer,不重新发送退款。存在 pending、processing 或 failed Step 时必须先走恢复流程。

普通 retry 处理自动可恢复状态;manual_review 或客户已履行但 Recovery 未完成的记录,需要显式受控恢复入口。所有运营动作记录管理员、请求 ID、理由、操作前后摘要和结果。

13.1 人工操作审计的边界(commission_financial_operation_logs)

该表是操作审计,不是资金账本,也不是第二套领域状态:

  • 它回答「谁、何时、因何、对哪个 Commission/资源发起了什么、操作前后状态与结果或失败原因」;
  • 它不承载金额、退款结果或 Provider 状态的事实源,修改审计记录不能改变任何退款或结算结果;
  • 覆盖范围只限人为触发的资金恢复入口:四个内部 HTTP 受控操作与四个人工 CLI 命令;
  • 自动 Job、Outbox 投递、恢复扫描、Webhook 以及普通用户/画师操作不写这张表,它们的事实由 Resolution、Refund、Allocation、Step、Outbox 与普通日志表达;
  • request_id(HTTP X-Request-Id / CLI --request-id)是一次人工意图的幂等标识:同条款成功重放返回已保存结果,不重复执行;条款不同或上一次仍 processing/failed 时返回 22020,要求人工先核对领域事实。

13.2voided 与人工履约的领域边界

  • voided + not_required:未开始客户履约的退款债权被补偿性业务决议关闭并释放预留。它不代表「退款失败」,也不代表「人工已付款」。
  • 原支付账号不可用后由运营人工转账到客户其他账号,属于债权履约:正确做法是记录人工履约凭证并把 obligation 置为 fulfilled,绝不能用 voided 代替。该能力本轮不实现,作为明确的未来扩展。
  • fulfilled 与 voided 都关闭债权,但二者不可互换;fulfilled 表示客户债权已被履约,Recovery 是否结束仍由步骤、对账与 Resolution 表达。

14. API 投影

领域模型和 API DTO 分离:

  • 金额统一投影为 Money;
  • Refund 只返回 obligation_status、execution_status;
  • 正式去向只返回 cash_destination;
  • 允许去向只返回 cash_destinations;
  • Step 的 status 是执行步骤状态,继续保留;
  • Cancellation 的 status 是取消聚合流程状态,继续保留;
  • Financial Resolution 的 status 是决议状态,继续保留。

禁止返回以下开发原型字段:

  • Refund 单轴 status;
  • Refund destination;
  • Refund 对象上的重复 destinations;
  • 扁平金额镜像和 old_price_money/new_price_money;
  • execution_status = null 的历史兜底语义。

15. 数据模型

表所属聚合关键约束
work_task_price_changesPrice Change活动状态由服务和锁约束,关联 Resolution/Refund
commission_cancellationsCancellation一张 Commission 最多一笔活动取消
commission_financial_resolutionsFinancial Resolutiontrigger type/id 唯一,请求幂等键唯一
commission_financial_resolution_refund_dependenciesFinancial Resolutionresolution/refund/type 唯一
commission_refundsRefund请求幂等键唯一,双轴状态必填
commission_refund_allocationsRefundrefund/source key 唯一
commission_refund_stepsRefundrefund/operation key 唯一,idempotency key 唯一
commission_settlement_stepsCancellationcancellation/operation key 唯一
outbox_messagesReliabilitytopic、aggregate、可用时间、租约和投递状态
commission_financial_operation_logsOperations管理员命令审计与请求 ID

数据库结构演进必须遵守以下规则:

  1. 共享测试环境和后续环境可能已有数据,数据库结构变化必须新增正向 migration。
  2. 已部署的 migration 不可改写;API 不兼容不等于数据库可以 migrate:fresh。
  3. 涉及既有数据时使用 expand → backfill → validate → contract:先加字段/表并回填,再让代码依赖,最后删旧字段。
  4. 金融数据无法可靠回填时失败关闭:中止迁移并输出异常行数量与样例 ID,交人工核查,不猜测资金事实。
  5. 数据 migration 的回滚不得删除已经形成的财务历史;必要时 down() 明确 no-op 并说明原因。

voided 债权的 execution_status 规范化、取消申请的不可变 requested_refund_destination 回填,以及无读者列 next_retry_at / last_notified_at 的删除,都是按上述规则以 2026-09-17 新增 migration 完成的。

16. 代码职责映射

职责服务
支付事实与退款计算RefundSnapshotService、RefundCalculationService
Commission 能力CommissionRefundCapabilityService
对外 Money 与政策投影CommissionRefundPresentationService
统一财务报价CommissionFinancialQuoteService
决议记录和依赖CommissionFinancialResolutionService
决议推进CommissionFinancialResolutionOrchestrator
债权创建和去向选择CommissionRefundService
资金预留CommissionRefundReservationService
步骤规划CommissionRefundExecutionPlanner
步骤执行CommissionRefundExecutor、Gateway adapters
状态派生CommissionRefundStateService
对账和收尾CommissionRefundReconciliationService、CommissionRefundFinalizer
取消编排CommissionCancellationService
降价退款CommissionPriceChangeRefundService
完成结算计划CompletionSettlementPlanBuilder/Executor
可靠投递OutboxService、Outbox dispatch/recovery commands

17. 设计守则

  1. 先区分业务决定、客户债权和资金执行,再讨论状态。
  2. 新业务只通过 Financial Resolution 和 Refund 进入资金流程。
  3. 不读取旧列、旧快照或旧 Topic;数据不完整时失败关闭。
  4. 不把 Provider 特例扩散到 Controller 或前端。
  5. 不用实时费率重算已接受的财务决议。
  6. 不为了减少类数量而合并业务聚合,也不为假想扩展增加空枚举和状态。
  7. 不让管理后台直接修改业务库资金字段。
  8. 不用测试 workaround 掩盖 Laravel worktree 或 package discovery 问题。
  9. 每个状态必须有生产写入路径、读取方和可验证的迁移;否则删除。
  10. 文档只描述当前业务和最终契约,历史执行记录放在临时报告中。