文档状态:目标领域模型基线
基线日期:2026-09-16
需求来源:COMMISSION_REFUND_PRD.md
API 契约:../api-changes/2026-09-09_commission_cancellation_and_refunds.md
本文只描述长期领域模型、聚合边界、不变量和可靠性语义。代码清理步骤放在独立执行计划中,历史审计和旧原型不作为当前模型的一部分。
Commission 的复杂度来自四类事实同时存在:
这些事实的生命周期不同。取消或降价可以已经生效,但退款仍在等待用户选择;客户可以已经收到退款,但画师关联账户回收仍失败。因此模型不能用一个 status 同时承载所有含义。
| 上下文 | 聚合根 | 负责 | 不负责 |
|---|---|---|---|
| Commission | WorkTask | 合同、阶段、支付入口、最终状态 | 执行退款 |
| Price Change | WorkTaskPriceChange | 改价意愿、审批、补款、价格应用 | 判断客户是否已退款 |
| Cancellation | CommissionCancellation | 取消协商、取消业务状态、协调退款与取消结算 | 计算渠道拆分 |
| Financial Resolution | CommissionFinancialResolution | 把已生效业务决定冻结为不可变资金决议并等待依赖 | 直接调用 Provider |
| Refund | CommissionRefund | 客户退款债权、资金预留、执行计划和履行状态 | 修改合同价格或取消决定 |
| Settlement | Settlement Plan 与结算服务 | 画师收入、平台费、减免、钱包与完成结算 | 决定退款去向 |
| Payment | Order、Payment、钱包扣款事实 | 已支付金额、支付渠道、费用和资金来源 | 解释退款业务原因 |
| Operations | 后台查询、受控命令、审计日志 | 恢复和核对既有事实 | 绕过领域规则改钱 |
上下文之间只通过 ID、不可变报价和稳定状态事实协作。任何调用方都不能通过修改另一个聚合的字段来模拟其状态迁移。
| 术语 | 定义 |
|---|---|
| 业务币种 | Commission 合同与双方协商退款使用的币种 |
| 渠道原币 | Provider 实际收款和退款使用的币种 |
| 退款债权 | 平台因一次已生效业务决定而对客户承担的一笔退款义务 |
| 开放债权 | obligation_status = open,仍占用资金预留的债权 |
| 履行 | 客户退款步骤已经成功,债权变为 fulfilled |
| 作废 | 债权由补偿性业务决议关闭且不再占用资金,变为 voided |
| 资金预留 | 将特定 Order 和资金来源的一部分锁给某笔债权,防止后续重复使用 |
| 去向偏好 | 用户在业务决定生效前表达的可选意愿,不保证最终可用 |
| 正式去向 | 债权当前冻结的 cash_destination |
| 财务报价 | 对当前支付、退款、预留、费率和结算事实的计算结果 |
| 财务决议 | 业务决定生效时记录的不可变资金计划与依赖 |
| 客户履约步骤 | 决定客户是否收到退款的步骤 |
| Recovery 步骤 | 回收画师或平台资金的步骤,不改变客户已收到退款的事实 |
| 对账 | 确认 Provider 或内部账本已经发生的事实,不重新执行操作 |
| 退款池 | 一张 Commission 下开放债权的摘要读模型 |
Open Call 是 project 的对外展示名称,Commission 是 worktask 的对外展示名称。
Currency 至少包含 id、code、symbol、is_zero_decimal。不同币种的 Money 不能直接相加。数据库继续保存整数最小单位,API 边界返回自包含 Money。
财务报价冻结以下事实:
Quote 是业务决议的输入,不是可长期编辑的草稿。业务事实变化后必须重新报价。
能力是整张 Commission 的计算结果:
它由所有已付款 Order 的渠道、collection mode、关联账户、钱包混付和数据完整性共同决定,不能只检查本次准备退款的单笔 Order。
职责:记录一次价格变更请求及其业务审批。
关键字段:
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。不变量:
pending 或 wait_pay 改价;paid 只表示新价格已应用;职责:记录双方取消协商,并编排取消后的退款与结算。
关键字段:
status、失败阶段和错误事实;commission_financial_resolution_id、commission_refund_id;不变量:
职责:把已经生效的取消、降价或完成结算决定冻结为不可变资金事实。
关键字段:
trigger_type + trigger_id 唯一;calculation_snapshot/fingerprint;不变量:
completed;当前有效触发类型只有 cancellation、price_change、completion。不为没有业务入口的 Admin Adjustment 预留类型。
职责:表示一笔客户退款债权,并作为退款执行聚合根。
关键字段:
work_task_id、financial_resolution_id;trigger_type + trigger_id;obligation_status、execution_status;cash_destination、preference_applied、action_required_at;不变量:
obligation_status;CommissionRefund 不包含旧单轴 status 或 destination 别名。
Allocation 是债权对资金来源的持久化预留:
reserved、fulfilled、released 状态。历史退款计算只读取 allocation,不从任意 JSON 字符串 key 反推账本。
Step 是最小可幂等执行单元:
operation_key;idempotency_key;type、financial_effect、顺序;同一债权与 operation key 唯一。已成功步骤永不重建、永不重放。
取消 API 可投影 business_status 与 financial_status,帮助前端区分协商事实和资金进度;持久化 CommissionCancellation.status 仍是取消聚合的流程状态,不是退款兼容字段。
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,否则中止并要求人工核查。
没有生产入口的 voided Resolution 状态不属于当前模型。需要撤销决议时,应先定义补偿业务和审计规则,再新增状态。
退款进度不进入改价状态机。
requested_refund <= maximum 是硬约束。普通场景最大值就是 standard;所有已付款 Order 都是纯 Stripe Destination Charge 时 maximum 才可等于 absolute。
取整差额使用确定性的最大余数法,并以稳定 key 排序,保证重算结果一致。
改价聚合只负责应用新价格。Financial Resolution 根据累计退款目标减去已完成退款和开放预留,创建差额债权。债权是否等待去向,不影响新价格已经生效的事实。
完成决议记录时,把当时所有开放债权登记为依赖。依赖要求是债权关闭,即 fulfilled 或 voided。依赖满足后仍要检查 Recovery、对账和冻结 Settlement Plan,全部完成才允许 WorkTask 最终结算。
当前依赖模型只使用 customer_refund + closed。没有实际调用方的依赖类型和条件不进入领域枚举。
| Step type | Financial effect | 说明 |
|---|---|---|
stripe_refund | customer fulfillment | Stripe 原路退款 |
paypal_refund | customer fulfillment | PayPal 原路退款 |
alipay_refund | customer fulfillment | Alipay 原路退款 |
user_credit_refund | customer fulfillment | 用户 Credit 入账 |
user_wallet_refund | customer fulfillment | 用户钱包退款 |
stripe_transfer_reversal | artist/platform recovery | Stripe Transfer 冲正 |
stripe_connected_account_debit | artist recovery | 关联账户扣款 |
stripe_connected_account_credit | artist/platform recovery | 关联账户补足 |
artist_wallet_recovery | artist recovery | 回收已入账画师钱包资金 |
Step 的 financial_effect 决定它是否影响客户债权。不能根据 Step type 字符串在多个服务中重复推断。
执行顺序原则:先建立客户退款和必要的 Provider 资金动作,再根据 Provider 事实完成内部钱包写入与对账。具体步骤顺序由冻结计划决定,恢复只能续跑未成功步骤。
以下事实必须一起提交:
| 层次 | Key |
|---|---|
| 用户请求 | request_idempotency_key |
| 业务事件 | trigger_type + trigger_id |
| 债权资金来源 | refund_id + source_key |
| 执行步骤 | refund_id + operation_key、全局 idempotency key |
| Provider | 稳定 Provider idempotency key/reference |
| Outbox | topic + aggregate + 未完成投递唯一语义 |
| 完成结算 | frozen plan fingerprint + executed fact + 唯一钱包 operation |
运行时只接受当前格式的完整快照:
快照生成后使用键排序规范化再计算指纹。执行前核对主体、币种、金额、计划版本和关键 Provider 引用。缺失当前必需字段属于数据完整性错误,不能回退到旧 JSON 结构或按实时配置重新计算。
没有支付事实的加价或未付款改价可以合法没有退款 calculation snapshot;这是业务上的空退款场景,不是旧数据兼容。只要记录声明存在正退款额或已形成退款决议,就必须有完整快照。
当前 Topic:
| Topic | Aggregate | Consumer |
|---|---|---|
commission.refund.execute | Refund | ExecuteCommissionRefund |
commission.financial_resolution.resume | Financial Resolution | ResumeCommissionFinancialResolution |
commission.cancellation_settlement.execute | Cancellation | ExecuteCommissionCancellationSettlement |
不保留没有消费者的历史 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 的候选。
对账资格至少要求:
fulfilled;completed;succeeded;对账成功只补写事实并推动 Finalizer,不重新发送退款。存在 pending、processing 或 failed Step 时必须先走恢复流程。
普通 retry 处理自动可恢复状态;manual_review 或客户已履行但 Recovery 未完成的记录,需要显式受控恢复入口。所有运营动作记录管理员、请求 ID、理由、操作前后摘要和结果。
commission_financial_operation_logs)该表是操作审计,不是资金账本,也不是第二套领域状态:
request_id(HTTP X-Request-Id / CLI --request-id)是一次人工意图的幂等标识:同条款成功重放返回已保存结果,不重复执行;条款不同或上一次仍 processing/failed 时返回 22020,要求人工先核对领域事实。voided 与人工履约的领域边界voided + not_required:未开始客户履约的退款债权被补偿性业务决议关闭并释放预留。它不代表「退款失败」,也不代表「人工已付款」。fulfilled,绝不能用 voided 代替。该能力本轮不实现,作为明确的未来扩展。fulfilled 与 voided 都关闭债权,但二者不可互换;fulfilled 表示客户债权已被履约,Recovery 是否结束仍由步骤、对账与 Resolution 表达。领域模型和 API DTO 分离:
obligation_status、execution_status;cash_destination;cash_destinations;status 是执行步骤状态,继续保留;status 是取消聚合流程状态,继续保留;status 是决议状态,继续保留。禁止返回以下开发原型字段:
status;destination;destinations;old_price_money/new_price_money;execution_status = null 的历史兜底语义。| 表 | 所属聚合 | 关键约束 |
|---|---|---|
work_task_price_changes | Price Change | 活动状态由服务和锁约束,关联 Resolution/Refund |
commission_cancellations | Cancellation | 一张 Commission 最多一笔活动取消 |
commission_financial_resolutions | Financial Resolution | trigger type/id 唯一,请求幂等键唯一 |
commission_financial_resolution_refund_dependencies | Financial Resolution | resolution/refund/type 唯一 |
commission_refunds | Refund | 请求幂等键唯一,双轴状态必填 |
commission_refund_allocations | Refund | refund/source key 唯一 |
commission_refund_steps | Refund | refund/operation key 唯一,idempotency key 唯一 |
commission_settlement_steps | Cancellation | cancellation/operation key 唯一 |
outbox_messages | Reliability | topic、aggregate、可用时间、租约和投递状态 |
commission_financial_operation_logs | Operations | 管理员命令审计与请求 ID |
数据库结构演进必须遵守以下规则:
migrate:fresh。down() 明确 no-op 并说明原因。voided 债权的 execution_status 规范化、取消申请的不可变 requested_refund_destination 回填,以及无读者列 next_retry_at / last_notified_at 的删除,都是按上述规则以 2026-09-17 新增 migration 完成的。
| 职责 | 服务 |
|---|---|
| 支付事实与退款计算 | 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 |