Commission 取消、改价与退款 PRD

文档状态:目标需求基线
基线日期:2026-09-16
适用范围:Commission(代码中的 WorkTask)取消、改价、退款债权、退款池、财务决议与运营恢复
详细接口契约:../api-changes/2026-09-09_commission_cancellation_and_refunds.md

本文是产品与业务规则的唯一长期说明。它回答“系统必须提供什么能力、用户看到什么结果、哪些规则不能破坏”,不记录历史实现过程、临时兼容方案或代码重构步骤。

1. 背景

Commission 支持多阶段付款,一张 Commission 可能包含多笔 Order,并混合网关现金、普通 Credit 与 Open Call Commission Credit。不同 Order 还可能使用 Stripe、PayPal、Alipay,以及不同的收款币种和结算路径。

取消或降价后,业务决定可以立即生效,但退款可能需要用户选择去向、等待 Provider、处理关联账户资金回收或进入人工复核。系统因此必须把以下事实分开:

  • 双方是否已经同意取消或改价;
  • 平台是否已经对客户形成退款义务;
  • 客户退款是否已经履行;
  • 画师、平台和支付渠道之间的资金是否已经收尾;
  • Commission 是否可以继续支付或完成结算。

2. 产品目标

  1. 用户与画师始终以 Commission 业务币种协商退款额。
  2. 取消、改价和退款各自有明确职责,不用一个状态同时表达业务决定和资金执行。
  3. 每次退款形成独立退款债权;多次降价或取消不会重复占用同一笔资金。
  4. Credit 与现金按支付时冻结的资金价值和来源执行,不让业务方手工决定拆分。
  5. Stripe 关联账户、PayPal、Alipay、内部钱包和 Credit 使用统一的退款债权与步骤模型。
  6. 业务决定落库后,即使退款暂时失败,也不回滚已同意的取消或改价。
  7. 所有金额响应都自带币种,前端不拼装币种、不重算平台费和渠道金额。
  8. 退款执行可幂等重试、可恢复、可对账,并能明确进入人工复核。
  9. 开发阶段只保留最终契约,不保留旧字段、旧接口、旧命令或旧快照读取逻辑。

3. 非目标

  • 不实现“用户若 3~7 天未选择去向则自动退款”。
  • 不允许前端指定 Credit 与现金的拆分比例或具体退款 Order。
  • 不允许画师代替用户决定退款去向。
  • 不在退款执行器中修改合同价格、决定取消是否成立或直接完成 Commission。
  • 不因历史完成后的退款自动重做已经完成的历史结算。
  • 不建立通用财务中台、完整事件溯源体系或跨业务通用退款 DSL。
  • 不为尚无真实业务入口的状态、触发类型或依赖类型预留实现。

3.1 明确延期、不属于本轮的需求

  • 协商取消与退款的消息通知:业务上需要,本轮不实现;后续按独立通知需求补充。不要为了未来通知保留无人使用的通用时间戳字段。
    • 状态更新(2026-09-21):已补齐。由任务 commission-refund-notifications 实现,新增 8 个 SystemNotificationScene 场景与 2 个 WorktaskPageEventListType,模板初始数据由 CommissionRefundNotificationTemplateSeeder 提供(12 行);对外说明见 2026-09-21_commission_refund_notifications.md。
  • 人工转账/手工履约入口:原支付账号不可用后由运营人工转账到客户其他账号属于债权履约(fulfilled),不是作废(voided)。该入口本轮不实现,只固化它与 voided 的领域边界;实现时必须记录人工履约凭证。
  • 3~7 天自动退款 / 自动选择去向:不做。未选择去向的债权继续留在退款池,等待用户明确选择。
    • 状态更新(2026-09-22):已补齐。由任务 refund-auto-destination 实现,新增可配置等待时限(默认 168 小时)与默认去向优先级,到期按优先级在债权可选去向内自动选定并复用既有退款执行链路;对外说明见 2026-09-22_commission_refund_auto_destination.md。
  • Provider 成功判定的调整:Stripe Connected Account 在退款/调整场景允许形成负余额,这是当前业务前提;但 charges.create(source: connected_account) 等具体 Provider 行为与 Alipay 成功响应边界仍需在测试环境用样本验证,未取得证据前不修改判定逻辑。

4. 参与方与权限

参与方能力
用户发起取消、响应画师取消、发起或响应改价、选择退款去向、查看退款池
画师发起取消、响应用户取消、发起或响应改价、查看退款和结算进度
系统计算金额、冻结资金事实、创建退款债权、异步执行、恢复、对账和结算
运营管理员查询财务对象,在服务端资格校验通过后重试、恢复或对账

用户是退款去向的最终决定者。画师发起取消或降价时不能提交退款去向;用户可以在发起自己的请求、接受对方请求或退款池中表达选择。

5. 核心业务对象

对象职责
WorkTaskCommission 合同、履约和支付入口
WorkTaskPriceChange新旧价格、发起方、审批和补款状态
CommissionCancellation取消协商及取消编排状态
CommissionFinancialResolution一次已生效业务决定对应的不可变资金决议
CommissionRefund平台对客户的一笔退款债权
CommissionRefundAllocation债权对原 Order 和资金来源的预留与履行事实
CommissionRefundStepProvider、Credit、钱包与资金回收的幂等执行步骤
OutboxMessage与业务事务同时持久化的异步执行意图

一张 Commission 可以同时存在多笔尚未完成的退款债权。每笔债权只占用自己的资金来源,退款池负责汇总,不把多笔债权合成一条可变记录。

6. 总体流程

业务记录、退款债权和 Outbox 意图必须在同一数据库事务中创建。业务事务提交后,可以用直接派发降低延迟,但恢复能力必须依赖持久化 Outbox,而不是依赖一次队列调用成功。

7. 取消需求

7.1 入口模式

模式条件结果
directCommission 未付款且当前状态允许取消统一通过取消申请接口立即完成,无需对方确认
negotiatedCommission 已付款且当前状态允许协商取消创建申请,等待对方接受、拒绝或发起方撤回
unavailable有支付中 Order、活动改价、活动取消、支付事实不完整或状态不允许禁止发起并返回稳定原因

不再提供独立的旧直接取消接口。两种可用模式都使用 cancellation/preview 与 cancellation/request。

7.2 协商规则

  • 用户发起时可以提交退款额和可选去向偏好。
  • 画师发起时只提交退款额,不能决定去向。
  • 用户接受画师请求时可以提交可选去向偏好。
  • 画师接受用户请求时沿用用户表达的偏好。
  • 未选择或偏好已不可用时,取消仍然生效,债权进入 awaiting_destination。
  • 拒绝和撤回只允许发生在 pending。
  • 接受前必须重新核对支付、历史退款、预留与计算指纹;事实变化返回条款变化错误,要求重新试算。

7.3 生效与收尾

接受取消后,取消业务事实立即成立。随后创建 Financial Resolution,并按需要创建退款债权和取消结算步骤。

退款失败不会把双方已经接受的取消恢复成待协商;取消对象进入相应处理或失败状态,运营人员只能恢复原决议,不能创建第二次相同资金操作。

8. 改价需求

8.1 四种路径

发起方与方向初始状态对方动作价格生效时点退款或补款
用户加价wait_pay无需画师批准补款成功后创建补款 Order
画师加价pending用户批准补款成功后创建补款 Order
用户降价pending画师批准批准事务内按需创建退款债权
画师降价paid无需用户批准创建事务内按需创建退款债权

改价状态 paid 表示新价格已经应用,不表示客户退款已经到账。退款状态只能从关联的 CommissionRefund 查询。

8.2 去向偏好

  • 用户发起降价时可以提前提供 refund_destination,它只是偏好。
  • 画师不能提供该偏好。
  • 降价生效时按最新整张 Commission 的支付能力重新校验。
  • 偏好仍可用时应用到新债权;不可用时忽略偏好并进入退款池等待用户选择。
  • 去向偏好失效不能阻止价格生效,也不能回滚价格。

8.3 连续降价

连续降价按累计目标退款计算,只创建尚未被已完成退款和开放债权预留覆盖的差额:

新增债权 = max(累计目标退款 - 已完成退款 - 开放债权预留, 0)

因此前一笔退款等待选择、执行中或人工复核时,后续降价仍可形成另一笔独立债权,但不能重复使用已被预留的资金。

9. 退款金额规则

所有金额使用整数最小单位。业务币种由 Commission 决定,渠道原币和 Credit 原币分别保存在资金来源和执行步骤中。

设:

  • P:已完成支付的业务金额;
  • F:已实际产生并折算到业务币种的支付手续费;
  • H:已完成退款金额;
  • V:开放退款债权已经预留的金额;
  • R:本次退款金额。

剩余标准上限与绝对上限为:

standard = max(P - F - H - V, 0)
absolute = max(P - H - V, 0)

有效上限由整张 Commission 的收款能力决定:

maximum = all_orders_pure_stripe_connected ? absolute : standard

默认建议退款额是标准上限。支付手续费默认由用户承担,因此普通场景不能超过标准上限。只有所有已付款 Order 都是信息完整、没有钱包混付的 Stripe Destination Charge 时,才允许在标准上限与绝对上限之间退款;超出的部分通过 Stripe 关联账户资金调整处理。

平台手续费比例来自画师数据库配置,不写死为 5%。平台费、Open Call 减免、PlatFee Wallet 和画师预计净收入必须沿用 WorkTask 正常结算规则,由服务端统一计算。

10. 资金来源与分配

10.1 Credit

  • Credit 是 Order 的资金来源,不是画师输入项。
  • 按整个 Commission 下单时锁定的 Credit 与现金业务价值比例分摊退款。
  • Credit 的业务价值在下单时锁定,退款时按锁定汇率反算原币,不使用实时汇率。
  • 多个 Credit 来源使用最大余数法分配,保证汇总额与债权金额完全一致。

10.2 网关现金

  • 原路退款按各 Order 可用业务价值和渠道事实分配。
  • PayPal、Alipay 与 Stripe 使用各自 Provider 能力计算渠道原币退款额。
  • 任何 Provider 返回金额、币种、支付对象或关联账户事实不完整时,计算结果不可执行。

10.3 画师与平台资金

客户退款步骤、画师资金回收步骤和平台资金回收步骤必须分开记录。客户已经收到退款后,画师回收或平台对账仍可能失败;此时客户债权是已履行,Financial Resolution 仍可处于处理中或人工复核。

11. 退款去向

去向说明条件
original按原 Order 和原资金来源退款计算完整且 Provider/钱包支持
credit将本次债权转为用户 Credit整张 Commission 不含 Stripe 关联账户 Order,且支付事实完整

只要存在 Stripe 关联账户 Order,就不允许退到 Credit。原因是 Stripe Destination Charge 的资金归属和关联账户回收必须保持原渠道事实,不能由平台用 Credit 替代。

多笔待选择债权可以批量选择同一去向,但服务端必须计算这些债权允许去向的交集:

  • 交集非空:用户可从交集中选择,整批原子提交;
  • 交集为空:不报系统错误,前端改为逐笔选择;
  • 任一债权状态变化或不属于该 Commission:整批失败,不允许部分提交。

12. 退款池

WorkTask 详情返回退款池摘要,用于展示:

  • 开放债权数量;
  • 需要用户操作的数量;
  • 可直接用于批量选择的债权 ID;
  • 开放债权总金额及币种;
  • 待操作债权的共同去向。

退款池只是摘要读模型。逐笔金额、去向、执行状态和步骤必须查询退款列表或详情。

13. 状态与用户展示

13.1 退款债权

退款必须使用双轴状态:

  • obligation_status:open、fulfilled、voided;
  • execution_status:awaiting_destination、ready、processing、partially_completed、retryable_failed、manual_review、completed。

不提供旧的单轴 status。前端判断客户是否收到退款只看 obligation_status;展示执行进度看 execution_status 和 execution.steps。

13.2 金额响应

所有公开金额统一返回:

{
  "amount": 12380,
  "currency": {
    "id": 31,
    "code": "CNY",
    "symbol": "CN",
    "is_zero_decimal": false
  }
}

同一事实只返回一次。退款债权的正式去向只返回 cash_destination,允许去向只返回 cash_destinations;不提供 destination、退款对象上的 destinations 或扁平金额镜像。

14. 支付与完成结算门禁

  • 支付中 Order 会阻止取消、改价和可执行退款计算。
  • 开放退款债权占用资金预留,后续计算必须扣除该预留。
  • processing 或 partially_completed 的客户退款会阻止继续支付。
  • Commission 完成结算必须等待记录时已有的开放债权关闭。
  • 债权 fulfilled 只表示客户退款已完成;若画师或平台回收未完成,Financial Resolution 不能完成。
  • 完成结算必须执行创建决议时冻结的 Settlement Plan,不能按执行时的新费率重新定价。

15. 可靠性与运营要求

  • 请求幂等键只能代表一组不变条款;同键不同条款必须失败。
  • Provider 操作使用稳定的步骤幂等键和 Provider reference。
  • 执行步骤使用租约,过期后才能恢复,不能并发重复执行。
  • 退款能力、资金来源、费率、汇率来源和结算计划在业务决定生效时冻结并带指纹。
  • Outbox 定时投递,恢复扫描定时检查陈旧记录。
  • 自动重试耗尽或事实无法安全核实时进入 manual_review,禁止猜测结果或重新发起另一笔退款。
  • 运营接口必须要求内部 Token、管理员身份、请求 UUID 和操作原因,并把「谁、何时、因何、对哪个资源、操作前后状态、结果或失败原因」持久化到数据库操作审计表;审计记录不是资金账本,不能通过修改它改变退款或结算结果。
  • 人工操作审计只覆盖人为触发的资金恢复入口(四个内部 HTTP 受控操作与四个人工 CLI 命令)。自动 Job、Outbox 投递、恢复扫描、Webhook 与普通用户/画师操作不写该表,它们的事实由 Resolution、Refund、Allocation、Step、Outbox 与普通日志表达。
  • 同一个 request_id 不会重复动钱:同条款成功重放返回已保存结果,条款不同或上一次仍未收尾时返回冲突错误码 22020,要求人工核对该资源。
  • Operator reconcile 只确认已经发生的 Provider/账本事实,不能跳过失败步骤或重新发起退款。
  • 队列时序不变量:retry_after 是连接级参数,必须大于同一连接上所有 Job 的最大 timeout;恢复扫描的陈旧阈值必须高于 retry_after 与步骤租约。

16. 接口边界

  • 用户端:取消全流程、改价全流程、退款查询、单笔和批量选择去向。
  • 画师端:取消全流程、改价全流程、退款只读查询。
  • 内部接口:退款试算、取消恢复、退款恢复、退款对账、Financial Resolution 恢复。
  • API 业务失败使用 HTTP 400 与稳定 ErrorCode;字段格式错误使用 422;不存在或无权访问使用 404。
  • 异步业务成功提交仍返回 200,以资源状态表达后续进度,不使用 202。

17. 验收标准

17.1 业务

  • 未付款直接取消与已付款协商取消都只走统一取消入口。
  • 用户和画师四种改价路径均符合审批、生效和补款规则。
  • 多次降价只新增未覆盖差额,不重复占用资金。
  • 多笔债权去向交集为空时可逐笔处理。
  • 业务决定生效后,Provider 失败不会回滚合同或协商事实。

17.2 金额

  • CNY、JPY 等零位和非零位币种都按最小单位计算和展示。
  • Credit 与现金混付、多 Credit、多 Order 分配汇总严格等于债权金额。
  • 普通场景不超过标准上限;纯 Stripe 关联账户场景可以达到绝对上限。
  • 任一 Stripe 关联账户 Order 存在时不可退到 Credit。
  • 平台费比例来自画师配置,并与正常 WorkTask 结算口径一致。

17.3 状态与执行

  • 客户退款与画师/平台回收的结果可独立表达。
  • 已成功步骤不会因重试再次执行。
  • 已对账事实不会回退。
  • Outbox 或队列中断后可以恢复原决议。
  • 人工操作无法绕过服务端资格校验。
  • 「退款已履约、但依赖或取消投影尚未收尾」的缝隙可以由恢复扫描/人工 resume 自动补齐,且不重放客户退款;归属不一致时保留客户 fulfilled 并进入 manual_review。
  • 作废债权稳定为 obligation_status = voided + execution_status = not_required,不显示为可重试失败。
  • 取消陈旧请求不能把已推进的取消覆写回 withdrawn;同一幂等键的合法重放不再因 accept 后去向变化而被判定为不同条款。
  • 改价的状态、价格与财务决议只会写入一次,并发终态不能互相覆盖。

17.4 契约

  • API、管理后台和文档均不再引用 CommissionRefund.status、CommissionRefund.destination。
  • 不存在旧 /work_tasks/cancel 路由和旧 refund:retry-commission 命令。
  • 不存在历史 Outbox Topic 和旧快照 fallback。
  • 所有公开金额都是 Money 结构,退款债权只暴露唯一状态和去向字段。
  • 对外不再出现 next_retry_at / last_notified_at 的任何契约描述。
  • 新增正向 migration 可在已有测试数据的数据库上直接 php artisan migrate 升级,无需 migrate:fresh;无法证明的数据会失败关闭并输出数量与样例 ID。