适用分支:
feature/refund-preview-calculation;计算版本14面向读者:前端(用户端 + 画师端)、联调、测试 状态:开发阶段契约。退款相关载荷已按「每个事实只出现一次 + 金额一律自包含」重构,不再保留旧扁平镜像字段,也不再保留单轴退款状态等旧兼容字段。
勘误说明(2026-09-28):
can_submit的原描述「等价于试算是否完整」已过时。改价试算(work_task_price_changes/preview)现除计算完整性外,还会因退款政策拒绝而返回can_submit = false(如below_minimum_amount、final_refund_must_be_remaining、write_off_cap_exceeded、exchange_rate_unavailable、below_payment_fee),disabled_reason也新增了这些政策取值;取消试算在计算完整且有可退额度时可用,整笔剩余低于最低退款金额时仍可选择全退(R = M,不受门槛限制)。完整政策字段、取值与示例见 2026-09-26_small_refund_threshold_write_off.md。更新说明(2026-09-16,第三轮):补充部署与领域不变量。数据库通过正向 migration
2026_09_16_000001增量收敛到最终 schema:已有测试数据库执行普通php artisan migrate即可保留数据并删除旧列,不需要migrate:fresh;「不兼容旧 API」与「数据库必须可增量部署」是两件事。voided退款债权是不可执行终态,陈旧执行消息只做 no-op。客户已fulfilled时若出现 allocation 完整性异常,客户双轴状态保持不变,异常记录在 Refund 诊断与 Financial Resolution 人工复核(原因码allocation_integrity_invalid)。详见 §10。更新说明(2026-09-17,第四轮):修复取消接受与改价终态的并发覆写、补齐终态退款的自动收尾,并把人工财务操作持久化审计。数据库新增
2026_09_17_000001~2026_09_17_000004四个正向 migration(含删除两个无读者的历史列),已有测试数据库同样只需要普通php artisan migrate。voided债权新增专用execution_status = not_required,不再投影成retryable_failed;/api/internal四个受控恢复接口开始把「谁、因何、对哪个资源、结果如何」写入commission_financial_operation_logs,并新增内部请求冲突错误码22020。详见 §4.8、§9、§13.2、§13.3。
更新说明(2026-09-16):Commission 取消/改价/退款完成开发阶段的破坏性清理,本文档已按当前代码逐项复核。退款债权不再返回单轴
status、destination与重复的destinations:进度只读obligation_status+execution_status双轴,去向能力只读cash_destinations.allowed/cash_destinations.unavailable。同时移除旧CommissionRefundStatus枚举、已废弃且不再保留定义的旧退款冲突错误码、POST /work_tasks/cancel与POST /api/artist_center/work_tasks/cancel旧直接取消端点、refund:retry-commission与commission-refund:backfill-allocations命令、以及 Outbox 主题commission.financial_resolution.recorded。本版不承诺兼容旧 API 字段;数据库结构由正向 migration 增量收敛(见 §10)。更新说明(2026-09-14):本文已合并退款池、多笔退款债权、财务决议及执行可靠性约定,作为 Commission 取消、改价和退款对接的唯一文档。同日按当前代码复核修正了
refund_destination必填性、改价 Preview 字段范围、role_view角色互斥、60001、结算步骤类型与execution.readiness/blocked_reason取值等事实。更新说明(2026-09-15):改价领域补齐专用错误码。重复发起待处理改价改为返回
60003 WorkTaskPriceChangeAlreadyPending;approve/reject/cancel以及补款完成时的状态冲突改为返回60004 WorkTaskPriceChangeStateInvalid。改价入口不再返回不含code的 HTTP400,前端按code分支即可(见 §9)。同日补充:
refund_pool(WorkTask 详情)与退款列表summary新增action_required_refund_ids,与bulk_allowed_destinations是同一集合,可直接作为批量选择去向的refund_ids;退款池仍是摘要读模型,逐笔明细读commission_refunds/list(见 §4、§6.5)。
建议阅读顺序:第 1 章(上手)→ 第 4 章(字段字典)→ 第 6 章(你要对接的端点)→ 第 7 章(流程)→ 第 8 章(字段绑定)。第 3 章是金额口径,金额相关问题都源自它。遇到「为什么这个字段是这样」先看第 12 章 FAQ。
| 章 | 内容 | 什么时候看 |
|---|---|---|
| 1. 一分钟上手 | 三条流程 + 八条铁律 | 刚拿到文档 |
| 2. 领域模型与基础约定 | 领域、名词、金额、币种与技术术语 | 需要对齐概念 |
| 3. 退款业务规则 | 三层上限、收款资格矩阵、去向、平台费 | 金额算不明白时 |
| 4. 统一字段字典 | 每个字段的含义与前端用途 | 写绑定前 |
| 5. 端点总览 | 端点清单与通用约定 | 找接口 |
| 6. 端点详解 | 每个端点的参数与要点 | 对接具体接口 |
| 7. 对接流程 | 流程图、轮询与失败处理 | 串流程 |
| 8. UI 字段绑定对照表 | UI 项 → 字段 | 写页面 |
| 9. 错误处理 | 错误码与处置 | 联调排错 |
| 10. 开发阶段破坏性变更 | 不承诺兼容 + 正向增量 migration | 从旧开发环境升级 |
| 11. 前端现有绑定迁移对照表 | 逐条旧→新绑定 | 改前端 |
| 12. 常见误解(FAQ) | 高频疑问 | 卡住时 |
| 13. 执行可靠性与发布 | 异步恢复、人工审核、部署要求 | 联调与发布 |
| 附录 A / B | 完整示例 / 参数速查 | 抄示例 |
financials.refund_limits.maximum。不要用 standard,不要判断 Stripe 场景。destinations.available,退款债权读 cash_destinations.allowed(被禁用的去向不展示、不置灰,直接不出现在选项里);不要根据支付渠道推断能不能退 Credit。{ amount, currency }:包括 role_view 中的估算额与网关/Credit 投影。币种对象自带 is_zero_decimal,前端按它决定小数位,不需要站点币种表。code(HTTP 400),字段格式错误是 HTTP 422,权限/不存在是 HTTP 404;不要依赖 message 文案。改价领域同样遵循该约定:重复发起待处理改价是 60003,approve/reject/cancel 等动作的状态冲突是 60004(见 §5.4、§9)。idempotency_key;改金额或改去向后必须换新键。financials.payment_fee 是已付款 Order 的实际手续费;financials.future_payment_fee_estimate 是变更后剩余全部未付款金额的手续费预估,二者不能复用同一字段。price_change.status 只表示改价协商、待补款或价格已应用;退款是否创建、选好去向或到账,要通过 commission_refund_id 查询退款领域对象。本章作用:统一术语与金额表示法,避免同一个数字在两端被理解成不同东西——尤其是「所有金额都是最小单位整数」与「每笔金额自带币种」。
| 称呼 | 指 | 出现在 |
|---|---|---|
| 取消 Preview | POST …/cancellation/preview 的响应 | 用户端/画师端取消试算 |
| 取消对象 | CommissionCancellation:active_cancellation(摘要)与 cancellation/info(完整) | 取消流程全程 |
| 退款对象 | CommissionRefund:commission_refunds/list、/info、select_destination 的元素 | 退款执行与查询 |
| 改价对象 | WorkTaskPriceChange:work_task_price_changes/* 的响应元素、WorkTask 详情的 price_change | 改价流程 |
| 业务币种 | Commission 的结算币种(画师币种) | 所有业务金额 |
| 网关币种 | 实际支付的渠道币种(可能不同,需汇率) | role_view.gateway_refunds |
| 名词 | 说明 |
|---|---|
| Commission | 对外的“委托/约稿”,后端模型是 WorkTask。 |
| Order | 一次支付单。一个 Commission 可以有多笔已付 Order(分阶段付款、加价补款)。 |
| Cancellation | 取消申请,资源字段见 cancellation/info。 |
| WorkTaskPriceChange | 改价领域的业务对象,记录加价/降价、审批、待补款和价格应用;其状态不表示退款进度。 |
| Financial Resolution | 财务决议领域的不可变业务财务事实,衔接业务决定与资金义务;它不是退款执行记录。 |
| CommissionRefund | 退款领域的独立退款债权,取消与降价退款共用;负责去向、预留、执行和履约状态。 |
| Credit | 用户站内余额(钱包)。原订单可能由 Credit + 网关共同支付。 |
| Connected Account | 画师的 Stripe 关联账户(destination charge 时收款方)。 |
| 业务币种 | Commission 的结算币种(画师币种),退款额一律用它的最小单位。 |
| 支付币种 | 实际支付的网关币种,可能与业务币种不同,需要汇率换算。 |
12380 = CN123.80;USD 1883 = US$18.83。{ "amount": 12380, "currency": { "id": 31, "code": "CNY", "symbol": "CN", "is_zero_decimal": false } },不存在“裸整数 + 同级币种字段”的写法。is_zero_decimal 决定小数位,前端不得按 code 猜):is_zero_decimal | 展示 | 例 |
|---|---|---|
false | 金额 ÷ 100,固定两位小数 | 12380 + CN → CN123.80 |
true | 直接显示整数 | 12380 + JP¥ → JP¥12380 |
-200 CNY 显示为 -CN2.00。可能出现负数的字段只有两个(且仅纯 Stripe 关联账户场景):financials.artist_settlement_basis.amount、financials.estimated_artist_net.amount。role_view.gateway_refunds = [{ "amount": 1883, "currency": { "code": "USD", ... } }],币种信息在元素内,前端不需要额外映射。role_view.estimated_display_amount 是用户偏好币种的估算展示值(Money),不参与任何计算;缺少汇率时整体为 null。Cancellation(取消申请) 有两种返回形态:
| 形态 | 出现在 | 包含 |
|---|---|---|
| 摘要 | WorkTask 详情 active_cancellation | financials、refund_policy、destinations、role_view、can_withdraw/accept/reject、失败信息、created_at/updated_at |
| 完整 | cancellation/info、request、withdraw、reject、accept 的 data | 摘要的全部字段 + currency_id、refund(退款对象或 null)、settlement(结算步骤)、accepted_at、completed_at |
CommissionRefund(退款债权):id、work_task_id、trigger_type(cancellation|price_change)、trigger_id、obligation_status、execution_status、cash_destination、cash_destinations、preference_applied、currency_id、financials、refund_policy、execution、action_required_at、created_at、completed_at。
financials.refund(资源不再重复返回一份标量金额)。cash_destination 是已经应用的退款去向(尚未选择时为 null);cash_destinations.allowed 是债权创建时冻结的业务允许去向,cash_destinations.unavailable 给出被禁用去向的原因码。退款债权不返回单轴 status、destination,也不返回重复的 destinations。execution 结构:Refund Pool(退款池摘要) 出现在 WorkTask 详情和退款列表响应中:
open_count 用于退款池角标,action_required_count 表示需要用户选择去向的债权数。action_required_refund_ids 是与 bulk_allowed_destinations 同一集合的债权 id,按升序返回:把这些 id 原样作为 refund_ids 提交 commission_refunds/select_destinations,就是用该去向交集做批量选择。它覆盖全部待选择债权,与 commission_refunds/list 的分页无关;空数组表示当前没有需要选择去向的债权。bulk_allowed_destinations 是全部待选择债权的去向交集;空数组是合法值,此时禁用批量入口。cash_destinations.allowed、execution.readiness 必须读 commission_refunds/list 的 data[](其 summary 与本对象同构)。Financial Resolution(财务决议摘要) 出现在 WorkTask 详情:id、status、trigger_type、refund_obligations_open、action_required。它表示业务决定产生的资金义务是否已经全部收尾,不等同于客户退款状态。
WorkTaskPriceChange(改价):id、work_task_id、currency_id、initiator_type/initiator_id、approver_type/approver_id、old_price、new_price、need_pay_amount、work_task_paid_amount(以上四个价格/金额都是 Money)、status、approved_at、paid_at、commission_refund_id、refund_destination、financials、refund_policy、destinations、created_at、updated_at。
financials.refund(加价时为 0)。financials 比取消/退款对象多一个 future_payment_fee_estimate(Money),表示变更后尚未支付部分的手续费预估。financials.refund、refund_policy 与 destinations 在改价对象中属于改价试算投影:用于说明该改价生效后预计产生多少退款及可填写哪些去向偏好;它们不是退款执行状态。refund_destination 在用户发起降价时是批准前的去向偏好。降价生效后,后端仅在该偏好仍可用时把它应用到退款债权;否则债权进入 awaiting_destination。它不是退款债权 cash_destination 的兼容别名或副本,也不表示退款已经执行。commission_refund_id 是改价领域指向退款领域的关联键。只有降价已生效且实际产生正数退款债权时才有值;前端据此调用退款接口。commission_financial_resolution_id 是改价领域指向财务决议的关联键。它表示该次降价的财务事实已经记录,不表示退款已经完成。内部字段(refund_calculation_snapshot、refund_calculation_fingerprint、calculation_snapshot、request_idempotency_key、last_error、Provider 明细)永不返回。
先区分“领域”和“技术设施”:领域表示一类业务事实由谁负责,例如改价领域负责价格是否已经生效,退款领域负责客户是否收到退款;技术设施用于保证这些业务在断网、重复请求或进程重启时仍能可靠执行,本身不是一种新的业务状态。
| 术语 | 所属领域 | 白话解释 | 前端需要做什么 |
|---|---|---|---|
| 业务决定 | 取消 / 改价领域 | 双方已经确认取消,或者新价格已经应用。这件事一旦成功落库,不会因为后续退款暂时失败而撤销。 | 根据取消或改价对象自己的状态更新业务 UI。 |
| Financial Resolution(财务决议) | 财务决议领域 | 一张“这次业务决定最终应该怎样动钱”的总任务单。它把退款、画师资金回收、平台对账和结算串在一起。 | 只用摘要状态判断整件财务工作是否收尾;不要把它当作客户退款状态。 |
| 退款债权 / CommissionRefund | 退款领域 | 平台已经确认“欠客户这一笔退款”。每次取消或降价可能产生一笔独立债权。 | 查询金额、选择去向,并通过双轴状态判断客户退款进度。 |
| 退款池 / Refund Pool | 退款领域的查询模型 | 当前 Commission 所有未完成退款债权的汇总视图,不是一笔新的退款。 | 展示数量、待选择数量、合计金额;批量操作只用后端返回的去向交集。 |
| 资金来源 / Funding Source | 支付与退款领域 | 原订单的钱来自哪里,例如 Credit、PayPal、支付宝或 Stripe。一次付款也可能由多个来源组成。 | 不自行拆分;展示后端给出的网关和 Credit 退款明细。 |
| 资金预留 / Reservation Ledger | 退款领域的内部账本 | 债权创建后,先把对应 Order/Credit 的可退额度“占住”,避免下一次降价或取消重复使用同一笔钱。它不是冻结用户银行卡。 | 无直接字段;看到未完成债权仍占用可退额度属于正常结果。 |
| 执行计划与步骤 | 退款 / 结算领域 | 后端把一笔财务工作拆成原路退款、退 Credit、回收画师资金等可独立核对的步骤。 | 如需展示进度,读取 execution.steps;不要按支付渠道自行生成步骤。 |
| Provider | 外部支付领域 | 实际处理收款或退款的第三方,例如 Stripe、PayPal、支付宝。 | Provider 暂未确认时保持轮询,不要让用户重复发起退款。 |
| 对账 / Reconciliation | 外部支付与财务决议领域 | 后端向 Provider 或内部钱包账本核实“钱是否真的动了、金额是否一致”。接口调用成功不一定等于 Provider 最终入账。 | processing 可以持续一段时间;以接口状态为准。 |
| 术语 | 所属层次 | 白话解释 | 前端需要做什么 |
|---|---|---|---|
| Outbox | 基础设施 / 可靠性 | 和业务记录一起写入数据库的“待办消息”。例如降价已经生效时,同时记下“需要创建并执行退款”的待办,避免业务成功但队列消息因服务重启而丢失。 | 不调用、不展示;只需知道 HTTP 200 后资金流程可能继续异步执行。 |
| Queue Job / Worker | 基础设施 / 可靠性 | 后台任务和执行它的工作进程。Outbox 会唤醒这些任务去调用 Provider、更新步骤并继续财务决议。 | 按返回状态轮询,不因页面超时重复提交业务请求。 |
| 幂等 / Idempotency | API 与执行可靠性 | 同一个业务动作即使因网络重试提交多次,也只产生一份业务结果或一笔资金操作。 | 同一次提交重试复用 idempotency_key;业务内容改变后生成新键。 |
| 快照 / Snapshot | 财务计算可靠性 | 在业务决定当时保存参与计算的订单、费率、币种和金额,后续执行按当时事实核对,避免配置变化悄悄改变结果。 | 不读取内部快照;重新 Preview 后只使用最新公开响应。 |
| 指纹 / Fingerprint | 财务计算可靠性 | 对快照关键内容计算的校验值,类似封条。执行前发现订单、金额或预留发生变化时,后端会停止自动动钱。 | 无直接处理;发生冲突时刷新 Preview 或按错误码提示。 |
| 依赖 Gate | 财务决议领域的执行控制 | 一道统一检查:前置退款、对账或资金回收没有满足条件时,后续结算暂不执行。 | 等待状态不是失败;继续读取退款和财务决议状态。 |
| 租约 / Lease | 基础设施 / 并发控制 | Worker 对一条消息或步骤取得的短时独占处理权,避免两个进程同时执行;进程退出后租约到期,可由其他 Worker 接手。 | 完全内部机制,无需展示。 |
| 恢复扫描 / Recovery Sweep | 基础设施 / 可靠性 | 定时查找长时间没有推进的 Outbox、退款或财务决议,并重新唤醒执行。 | 无需触发;保持正常轮询即可。 |
| Finalizer(收尾器) | 退款与财务决议的内部服务 | 在所有必要步骤完成后,统一确认债权履约、释放或核销预留,并推进财务决议终态。 | 不直接感知;最终读取 obligation_status、execution_status 和 financial_resolution.status。 |
manual_review | 退款 / 财务决议状态 | 自动流程无法安全证明下一步该怎样动钱,系统停止自动尝试并交给人工核对。它不一定表示客户退款失败。 | 停止自动操作,展示处理中或联系支持;客户是否已退款仍看 obligation_status。 |
可以把整套关系理解为:取消或改价先产生业务决定;Financial Resolution 把决定翻译成资金义务;CommissionRefund 表示其中对客户的退款债权;Outbox、队列、租约和恢复扫描只负责让这些义务可靠地执行下去。
本章作用:把退款金额是怎么算出来的讲清楚(上限从哪来、什么时候能超上限、能退到哪里、平台费与画师收入如何受影响)。金额相关的疑问基本都能在本章找到答案。
所属领域:退款领域的金额与执行规则。 改价领域只在试算和改价对象中返回这些规则的投影;降价生效并创建 CommissionRefund 后,正式债权金额、去向与执行状态以退款领域对象为准。
为什么需要这一章:退款额不是“已付金额减新价格”。它受 三层上限、收款资格 和 历史退款 共同约束。
| 字段 | 含义 | 前端用途 |
|---|---|---|
financials.refund_limits.standard | 标准上限 | 默认建议额;向双方解释“标准退款范围” |
financials.refund_limits.maximum | 有效上限 | 退款额输入框唯一上限 |
financials.refund_limits.absolute | 绝对上限 | 解释纯 Stripe 场景为什么能多退 |
数值示例(P = CN135.00,F = CN11.20,R = 0):
| 场景 | standard | maximum | absolute | 说明 |
|---|---|---|---|---|
| 平台收款(支付宝/PayPal/Stripe 平台收款) | 12380 | 12380 | 13500 | 只能退到 standard;要求更高金额 → HTTP 400 / 22004 |
| 纯 Stripe 关联账户收款 | 12380 | 13500 | 13500 | 可退到 absolute,超出 standard 的部分 CN11.20 由画师承担 |
maximum 一律拒绝(HTTP 400,错误码 22004),前端直接用 maximum 限制输入。refund_entry.presets 与上限的关系:standard = refund_limits.standard、full = refund_limits.maximum、unfinished_stages = max(0, maximum − 已完成节点金额合计)。参与判断的是 Commission 全部有效的已付资金 Order:正业务金额、status = paid;已部分或全部退款的历史 Order 仍然参与;零金额改价审计 Order、未支付/已取消 Order 不参与。
一个 Order 属于「Stripe 对 Stripe(关联账户收款)」需同时满足:
| 有效 Order 构成 | 可超过 standard | 可选择 credit |
|---|---|---|
| 全部为 Stripe 关联账户收款 | 是 | 否 |
| 完全不含 Stripe 关联账户收款 | 否 | 是 |
| 两类收款混合 | 否 | 否 |
| 存在未知支付或收款信息 | 否 | 否 |
判断逻辑(结果随 refund_policy 返回,前端不要自己算):
设计原因:Stripe 关联账户允许结算为负,所以超退部分可以让画师承担;平台内部钱包记为负等于平台先垫付手续费,因此只允许在关联账户场景发生。同理,一旦存在关联账户收款,退款就不能转成 Credit。
| 目的地 | 行为 |
|---|---|
original | 按原资金来源退回:原 Credit 部分回到 Credit,原网关部分退回原支付渠道 |
credit | 原 Credit 部分回到 Credit,原网关部分转换为 Credit;仅当 Commission 中不存在 Stripe 对 Stripe Order 时可用 |
destinations.available 作为唯一依据(只渲染其中的选项);destinations.unavailable.credit 给出稳定原因码,用于说明文案与排查,见 §4.4。commission_refunds/*)不返回 destinations,同一能力用 cash_destinations.allowed(可用去向)/ cash_destinations.unavailable(原因码)表达,语义与 destinations 一致。0 时 available / allowed 为空数组,前端不展示去向选择。credit。提前去向偏好:refund_destination 在业务决定生效前只是可选偏好,不是正式去向。正式去向只有退款债权的 cash_destination。
| 场景 | 是否接收 refund_destination | 语义 |
|---|---|---|
| 用户发起取消 | 可选 | 提前偏好 |
| 画师发起取消 | 否 | 画师不能决定客户去向 |
| 用户接受画师取消 | 可选 | 接受时提前偏好 |
| 画师接受用户取消 | 不新增选择 | 沿用用户偏好 |
| 用户发起降价 | 可选 | 提前偏好 |
| 画师发起降价 | 否 | 退款进入池后由用户选择 |
refund_capabilities 重新校验偏好;偏好缺失或已不可用时,取消/改价仍然生效,新债权进入 awaiting_destination。refund_destination 不保证被采用,也不能当作 cash_destination 的第二份事实来源。artist_fee_coverage > 0 只可能出现在纯 Stripe 关联账户场景,含义是“超出标准上限、由画师承担的手续费”(前端可用它替代 payment_fee_bearer 判断:大于 0 即画师承担)。artist_estimated_net < 0 也只可能出现在该场景,负数由画师 Connected Account 承接;执行阶段通过关联账户扣款落实,不会产生平台内部钱包负数。stripe_connected_account_debit),确认成功后再发起用户退款;需要画师补足时使用 stripe_connected_account_credit。对账阶段读取 Provider 账本核对金额:金额不符 → 退款 failed;Provider 尚未结算或数据不可用 → 退款保持 processing 并自动重试。改价接口会在现有退款试算之外,再按 Commission 正常完成结算规则预测变更后的平台费、后续支付手续费与画师净收入。该预测不会改变退款上限、退款去向或订单退款分摊。
financials.payment_fee:已经发生的实际手续费,来自现有已付款 Order;不会用配置费率反推。financials.future_payment_fee_estimate:对 U 的预估。U 包括本次改价需要补缴的金额和以后尚未支付的节点金额,不能只用 need_pay_amount 计算。financials.artist_settlement_basis:改价场景固定为 N,即变更后的 Commission 总稿酬。financials.platform_fee:按 N 计算;费率来自画师数据库配置,不固定为 5%,并继续应用 Open Call 减免和平台费钱包抵扣政策。financials.estimated_artist_net:在当前退款结算结果上加入未付款金额、扣除后续手续费预估,并把平台费调整为按 N 计算后的金额。没有历史退款或手续费转移时,可理解为 N − 实际支付手续费 − 后续支付手续费预估 − 平台费。/ 100。取消 Preview、active_cancellation、改价 Preview/创建/列表/详情、WorkTask 详情 price_change 共用 financials、refund_policy、destinations 这三个顶层结构。financials 共用基础字段;改价载荷额外包含 future_payment_fee_estimate。
设计约定:
financials(退款/取消/改价资源自身不再重复返回标量金额);角色专属的派生信息只在 role_view;UI 录入辅助只在 refund_entry。financials 不按角色裁剪:同一资源的用户端与画师端返回相同结构,包含画师结算相关字段(artist_settlement_basis、platform_fee、estimated_artist_net、artist_fee_coverage)与手续费;是否展示由 UI 决定。改价资源额外包含后续支付手续费预估。需要角色专属信息时读 role_view(画师平台费拆解、用户偏好币种估算与网关/Credit 投影)。financials作用:本次(或本次试算)全部业务金额事实的唯一来源——集中回答“已付多少、历史上退了多少、这次退多少、退款后还留多少、上限是多少、手续费/平台费/画师收入各是多少”。所有资源返回相同的基础字段;改价 Preview/创建/列表/详情以及 WorkTask 详情的 price_change 额外返回 future_payment_fee_estimate。同一资源在用户端与画师端内容相同(不做角色裁剪)。
零值语义:取消/退款在未付款或已无可退额度时仍返回基础结构(各金额 0、币种照常)。改价在未付款时,退款相关字段为 0,但 artist_settlement_basis、platform_fee、future_payment_fee_estimate、estimated_artist_net 会按新总价预测,因此可能是非零值。前端不需要处理 null。
| 字段 | 类型 | 含义 | 前端用途 | 可为负 |
|---|---|---|---|---|
paid | Money | 累计已付业务金额 P(终身累计,退款后不回写) | 展示“客户已支付” | 否 |
previous_refunded | Money | 历史已完成退款合计 R | 展示“历史已退” | 否 |
refund | Money | 本次业务退款额 X | 展示/回填「本次退款额」;退款资源取金额也用它 | 否 |
retained | Money | 退款后保留在 Commission 的金额 P − R − X | 展示“退款后保留” | 否 |
refund_limits.standard | Money | 标准上限 P − F − R | 默认建议额,对应 UI「标准可退款」 | 否 |
refund_limits.maximum | Money | 有效上限(纯 Stripe 关联账户时 = absolute,其余场景 = standard) | 对应 UI「输入框上限」;唯一上限,超过它提交返回 22004 | 否 |
refund_limits.absolute | Money | 绝对上限 P − R | 解释纯 Stripe 场景为何可以多退;不要当输入上限 | 否 |
payment_fee | Money | 累计已发生的实际支付手续费 F | 对应 UI「支付手续费」或改价 UI「已产生支付手续费」 | 否 |
future_payment_fee_estimate | Money | 仅改价载荷存在;变更后全部未付款金额的手续费预估 | 对应改价 UI「后续支付手续费预估」;不得绑定到 payment_fee | 否 |
artist_fee_coverage | Money | 本次由画师承担的手续费 max(0, X − standard) | 对应 UI「画师承担手续费」;大于 0 即由画师承担 | 否 |
artist_settlement_basis | Money | 取消/退款:退款后的画师结算基数;改价:变更后总稿酬 | 对应 UI「退款后余额(画师)」或改价 UI「变更后稿酬」 | 是 |
platform_fee | Money | 取消/退款:退款后平台费;改价:按变更后总稿酬预测的平台费 | 对应 UI「平台服务费」 | 否 |
estimated_artist_net | Money | 取消/退款后的画师净收入,或改价后预计最终净收入 | 对应 UI「预计画师净收入」;为负说明由关联账户承接 | 是 |
约定:
currency 都是 Commission 业务币种。future_payment_fee_estimate;该字段只属于改价资源。financials 不含计算快照、指纹、Provider 明细与内部执行步骤。refund_entry(仅取消 Preview 返回)作用:退款额输入的录入辅助——给前端提供三个快捷金额与手续费口径说明,本身不承载结算事实(金额事实一律读 financials)。未付款 Preview 同样返回该块(各金额为 0)。
| 字段 | 类型 | 含义 | 前端用途 |
|---|---|---|---|
presets.unfinished_stages | Money | 未完成节点稿酬 = max(0, 有效上限 − 已完成节点金额合计) | 快捷按钮“退还未完成节点” |
presets.standard | Money | 标准上限(= financials.refund_limits.standard) | 默认填值 / 快捷按钮“标准退款” |
presets.full | Money | 有效上限(= financials.refund_limits.maximum) | 快捷按钮“全额退款”(纯 Stripe 场景可大于 standard) |
fee_policy | string | 手续费策略标识,如 payment_fee_standard_cap_then_post_refund_platform_fee | 仅用于展示/日志,不解析 |
refund_policy作用:本次退款能力的判定结果快照——由后端对“Commission 全部有效已付 Order”的收款构成计算得出,用于解释“为什么能/不能超过标准上限、能不能退到 Credit”。前端只消费结论,不要自行判断收款方式。
| 字段 | 类型 | 含义 | 前端用途 |
|---|---|---|---|
maximum_policy | string | standard 或 stripe_connected_absolute(后者才可能超过标准上限) | 区分“标准可退”与“可超标准退款”的文案/埋点 |
can_exceed_standard | bool | 是否允许超过 standard(等价于全部有效 Order 都是 Stripe 关联账户收款) | 对应 UI「是否允许超标准上限」;决定画师承担提示是否展示 |
can_refund_to_credit | bool | 是否允许退到 Credit | 参考用;取消/改价的可选值以 destinations.available 为准,退款债权以 cash_destinations.allowed 为准 |
has_stripe_connected_order | bool | 是否存在 Stripe 对 Stripe Order | 诊断与说明文案 |
all_orders_stripe_connected | bool | 是否全部有效 Order 都是 Stripe 对 Stripe | 诊断与说明文案 |
has_unknown_order | bool | 是否存在无法确认支付/收款信息的 Order | 数据异常提示(可引导联系支持) |
destinations(仅取消 Preview / 改价能力集合)适用范围:本节的 destinations.available / destinations.unavailable 只出现在取消 Preview、取消对象与改价试算/改价对象上。退款债权不返回 destinations,其能力集合是 cash_destinations.allowed(可用去向数组)与 cash_destinations.unavailable(原因码对象),取值与语义和本节完全一致。
作用:本次退款的去向集合——告诉前端“钱可以退到哪里”。业务上只展示 available 中的选项:被禁用的去向不渲染为置灰项;unavailable 的原因码仅用于提示文案与排查。退款额为 0 时 available 为空数组,此时不展示去向选择。
全部去向可用时:
存在被禁用去向时(前端只渲染 available 里的 original):
| 字段 | 类型 | 含义 | 前端用途 |
|---|---|---|---|
available | string[] | 可提交的去向,取值 original、credit | 对应 UI「可用退款去向」;只渲染这些选项,提交时原样回传其中一个 |
unavailable | object | 映射:键为被禁用的去向,值为稳定原因码 | 供「去向不可用说明」文案与排查使用;不渲染置灰选项 |
| 原因码 | 触发条件 | 前端文案建议 |
|---|---|---|
stripe_connected_account_present | Commission 存在 Stripe 关联账户收款(destination charge)Order | “该委托含 Stripe 关联账户收款,只能原路退回” |
refund_payment_data_incomplete | 存在无法确认的支付/收款数据 | “支付数据待确认,暂不可退到 Credit” |
refund_to_credit_not_supported | 防御性默认:能力快照存在但未给出原因(正常流程不会出现) | “该委托的退款只能原路退回” |
兜底约定:遇到本表以外的原因码时按通用文案处理(例如“该去向暂不可用”),不要直接把原始原因码展示给用户。缺失 refund_capabilities 的损坏债权返回空能力集合(available 为空),既不伪造原因码,也禁止提交去向。
为什么 unavailable 是对象、而 available 是数组:两者分工不同——available 是要渲染的有序选项列表,unavailable 是按去向查原因的索引(unavailable.credit 一次取值就能拿到说明文案,不必遍历)。没有禁用项时返回 {} 而不是 [],保证该字段的 JSON 类型恒定(前端无需 Array.isArray 分支)。
can_submit 与disabled_reason作用:回答两个相互独立的问题——“这次请求能不能提交”(can_submit)与“退款额输入为什么不能用”(disabled_reason)。未付款或已无可退额度时输入被禁用,但零退款取消/改价仍然可以提交。
| 字段 | 维度 | 说明 |
|---|---|---|
can_submit | 流程 | 本次请求能否提交;计算不完整时为 false,不由退款上限推导(对应 UI「提交按钮」)。改价试算另有政策拒绝分支(低于最低退款金额、次数用完、第二次未退剩余全部、本次请求碎额超限、缺少汇率、新价低于不可退支付手续费),此时也为 false(其中低于最低退款金额仅适用于非全退的部分降价,R = M 全退豁免门槛),见 2026-09-26_small_refund_threshold_write_off.md |
disabled_reason | 退款额输入 | 输入框不可用的原因,与 can_submit 相互独立(对应 UI「退款额输入是否可用」);除下表取值外,退款政策另有 below_minimum_amount、refund_attempts_exhausted、final_refund_must_be_remaining、write_off_cap_exceeded、exchange_rate_unavailable、below_payment_fee(below_minimum_amount 现在只由改价入口返回;取消入口整笔剩余低于门槛时仍可选择 R = M 全退),见 2026-09-26_small_refund_threshold_write_off.md |
| 场景 | can_submit | disabled_reason | 前端处理 |
|---|---|---|---|
| 有可退额度 | true | null | 退款额输入可用 |
| 未付款,或已无可退额度(历史退款=已付) | true | no_refundable_amount | 禁用退款额输入;仍允许零退款取消、改价 |
| 试算数据不完整 | false | calculation_incomplete | 禁用输入并禁止提交,展示 issues |
所有情况都返回 HTTP 200;不要用失败响应表达“无可退额度”。
命名空间区分:顶层 disabled_reason 只描述退款输入;cancellation_entry.disabled_reason 只描述取消入口可用性(见 §5.3),两者可能同时出现在一个响应里。
role_view作用:承载角色专属、且不在 financials 中的派生信息——用户视角回答“这笔钱以什么形式、什么币种退回去”,画师视角回答“平台费是怎么算出来的”。已付款与未付款形状一致(未付款时全部为零值 Money)。
用户视角(回答“这笔钱以什么形式、什么币种退回去”):
| 字段 | 类型 | 含义 | 前端用途 |
|---|---|---|---|
estimated_display_amount | Money | null | 用户偏好币种下的估算退款额;缺汇率时为 null | 对应 UI「偏好币种估算(用户)」;为 null 时不展示该行 |
gateway_refunds | Money[] | 按网关币种分列的预计退款(原路退回部分) | 对应 UI「网关退款明细(用户)」;多币种逐条渲染 |
credit_refunds | Money[] | 按币种分列的 Credit 退款(含 credit 目的地转换部分) | 对应 UI「Credit 退款明细(用户)」 |
estimate_notice | string | 说明文案(英文常量) | 作为脚注展示,需前端本地化 |
画师视角(回答“平台费怎么算出来的”):
| 字段 | 类型 | 含义 | 前端用途 |
|---|---|---|---|
platform_fee_breakdown.basis | Money | 平台费基数(退款后的正余额) | 对应 UI「平台费拆解(画师)」——基数 |
platform_fee_breakdown.gross | Money | 平台费(Open Call 减免前) | 对应 UI「平台费拆解(画师)」——减免前金额 |
platform_fee_breakdown.open_call_fee_waiver | Money | Open Call 手续费减免额 | 对应 UI「平台费拆解(画师)」——减免行;为 0 时可省略 |
platform_fee_breakdown.wallet_applied | Money | 平台费钱包抵扣额 | 对应 UI「平台费拆解(画师)」——抵扣行;为 0 时可省略 |
画师视角不再重复返回支付手续费、退款额、退款后余额、最终平台费与预计净收入——这些都在 financials 中(payment_fee、refund、artist_settlement_basis(=退款后余额)、platform_fee、estimated_artist_net)。
warnings 与issues作用:试算过程的两类附带信息——可提示的提醒与必须处理的数据问题,让前端在“能提交但需说明”与“不能提交”之间做区分。
warnings:可提示用户但不阻塞,例如“已从 Stripe 账本归集历史退款”。issues:计算数据问题,通常伴随 can_submit = false,此时禁止提交。{ code, message, order_id? };message 仅用于排查,前端按 code 分支。作用:取消、改价、退款债权与财务决议的状态字典——前端按状态决定按钮可用性、是否轮询、以及失败时的引导文案;不要在状态之外自行推导流程阶段。
退款进度只读
obligation_status与execution_status双轴,退款债权不存在单轴status。不能用execution_status = retryable_failed/manual_review判断客户是否收到退款,因为客户退款完成后,画师/平台回收失败仍可能让财务决议进入manual_review。
取消申请 CommissionCancellation.status:
| 状态 | 含义 | 前端处理 |
|---|---|---|
pending | 等待对方确认 | 按 can_withdraw/can_accept/can_reject 展示按钮 |
refund_processing | 正在执行退款 | 禁用重复操作并轮询 |
settlement_processing | 正在结算画师侧 | 禁用重复操作并轮询 |
completed | 完成 | 刷新 Commission 与退款记录 |
failed | 失败 | 展示 failure_code/failure_stage,引导联系支持 |
rejected | 对方拒绝 | 结束轮询 |
withdrawn | 发起方撤回 | 结束轮询 |
取消对象同时返回两个事实字段:
| 字段 | 取值 | 含义 |
|---|---|---|
business_status | pending / accepted / rejected / withdrawn / cancelled | 双方协商与 Commission 业务事实 |
financial_status | not_started 或 Financial Resolution 状态 | 退款、追缴、对账和画师结算的总体进度 |
退款债权使用双轴状态:
| 字段 | 状态 | 含义 |
|---|---|---|
obligation_status | open | 对客户的退款义务仍未履行 |
obligation_status | fulfilled | 客户退款已经履行 |
obligation_status | voided | 债权已作废 |
execution_status | awaiting_destination | 等待用户选择退款去向 |
execution_status | ready | 去向与计划已确定,等待执行 |
execution_status | processing | 正在执行或等待 Provider 对账 |
execution_status | partially_completed | 客户退款步骤只完成了一部分 |
execution_status | retryable_failed | 暂时失败,后端仍可自动恢复 |
execution_status | manual_review | 自动恢复耗尽或事实无法安全核对 |
execution_status | not_required | 债权被补偿性业务决议关闭,无需执行;与 obligation_status = voided 稳定配对 |
execution_status | completed | 客户退款履行步骤全部完成 |
退款债权只有上述双轴状态,没有单轴 status,也不存在 CommissionRefundStatus 枚举;两轴取值都是非空字符串。
Financial Resolution 状态:
| 状态 | 含义 |
|---|---|
recorded | 业务决议已记录,等待资金执行 |
processing | 正在执行资金步骤 |
partially_completed | 部分义务已完成,仍有依赖未收尾 |
completed | 所有退款、追缴、对账与结算依赖完成 |
manual_review | 自动恢复无法继续,需要人工处理 |
备注:
CommissionFinancialResolution.status的partially_completed是可达状态——部分义务已完成但仍有依赖未收尾(写入点见CommissionFinancialResolutionService与DispatchPendingCommissionResolutionsCommand)。该枚举没有voided:需要撤销决议时应先定义补偿业务与审计规则,当前模型不包含它。另注:
CommissionRefund.execution_status也有同名partially_completed,二者含义不同,不要混用。
execution_status 只聚合客户退款履行步骤。obligation_status 与 execution_status 都是非空字段(数据库列均为 NOT NULL,obligation_status 默认 open),前端不需要处理 null,也不需要任何“旧值兜底”。客户已经收到退款、但 Transfer 冲正或画师追缴失败时,obligation_status 仍为 fulfilled,问题体现在 financial_resolution.status = manual_review;前端不能把它展示为“客户退款失败”。
voided 是不可执行终态:作废债权不再接受去向选择,也不执行任何步骤;指向它的陈旧执行消息只做幂等 no-op,不会重放 Provider 操作。作废时两轴在同一事务内写为 obligation_status = voided + execution_status = not_required,因此它既不是“退款失败”,也不是“人工已付款”,前端不要显示为可重试失败。not_required 只表示“无需执行”,isCustomerFulfilled() 仍只对 completed 成立。allocation_integrity_invalid 场景见 §10:open 债权进入 manual_review,已 fulfilled 的债权保持客户双轴状态并记录到 Refund 诊断与关联 Financial Resolution。
改价 WorkTaskPriceChange.status:pending(等待对方确认)、wait_pay(改价已同意,等待客户补款)、paid(新价格已应用)、rejected、canceled。降价不需要实际收款,但应用新价格后也使用 paid;因此 paid 不是“退款已支付”。退款进度必须通过 commission_refund_id 查询退款对象。
execution.steps[].type)作用:退款执行进度的步骤字典——用于把 execution.steps 翻译成用户可读的进度文案(例如“正在退回原支付渠道”),并定位失败发生在哪一步。
| 类型 | 说明 |
|---|---|
stripe_refund | Stripe 退款 |
stripe_transfer_reversal | Stripe Transfer 冲正(关联账户资金回收) |
stripe_connected_account_debit | 对画师关联账户扣款(承担超出标准上限的手续费) |
stripe_connected_account_credit | 向画师关联账户补足 |
paypal_refund / alipay_refund | 网关原路退款 |
user_wallet_refund | 退回用户 Credit 钱包 |
user_credit_refund | 退款额转为用户 Credit |
artist_wallet_recovery | 从画师钱包回收已结算资金 |
取消对象的 settlement.steps[].type 使用另一组取值:artist_wallet_adjustment(画师钱包调整)、plat_fee_wallet_adjustment(平台费钱包调整)。
步骤状态:pending、processing、succeeded、failed。financial_effect 区分 customer_fulfillment、artist_recovery、platform_recovery;前端判断客户是否收到退款时只看 customer_fulfillment 及债权双轴状态。
本章作用:先给全景——有哪些端点、分别干什么、哪些是画师独有的;具体参数与响应在 §6。
端点领域速查:
| 路径或字段 | 所属领域 | 前端用它判断什么 |
|---|---|---|
/work_tasks/cancellation/*、active_cancellation | 取消领域 | 取消协商、接受/拒绝、取消业务是否完成 |
/work_task_price_changes/*、price_change | 改价领域 | 新旧价格、审批、待补款、价格是否已经应用 |
/commission_refunds/*、refund_pool | 退款领域 | 退款债权、去向选择、客户退款是否履行 |
financial_resolution | 财务决议领域 | 退款、追缴、对账和结算义务是否全部收尾 |
| 补款 Order 与支付回调 | 支付领域 | 加价差额是否实际支付 |
领域之间只通过关联字段衔接:改价对象的 commission_financial_resolution_id 指向财务决议,commission_refund_id 指向退款债权。不要使用一个领域的 status 推断另一个领域的进度。
/api)| 方法 | 路径 | 用途 | 幂等 |
|---|---|---|---|
| GET | /work_tasks/info | 详情,含取消、改价、refund_pool 与 financial_resolution | — |
| POST | /work_tasks/cancellation/preview | 取消试算 | — |
| POST | /work_tasks/cancellation/info | 最近一次取消申请(含终态) | — |
| POST | /work_tasks/cancellation/request | 发起取消申请 | idempotency_key |
| POST | /work_tasks/cancellation/withdraw | 撤回自己的申请 | — |
| POST | /work_tasks/cancellation/reject | 拒绝对方申请 | — |
| POST | /work_tasks/cancellation/accept | 接受对方申请(可带去向) | — |
| POST | /work_task_price_changes/preview | 改价试算 | — |
| POST | /work_task_price_changes/create | 发起改价 | — |
| POST | /work_task_price_changes/list | 改价记录 | — |
| POST | /work_task_price_changes/approve | 批准画师改价 | — |
| POST | /work_task_price_changes/reject | 拒绝画师改价 | — |
| POST | /work_task_price_changes/cancel | 取消自己的改价申请 | — |
| POST | /commission_refunds/list | 退款记录 | — |
| POST | /commission_refunds/info | 退款详情 | — |
| POST | /commission_refunds/select_destination | 选择退款去向 | — |
| POST | /commission_refunds/select_destinations | 为多笔待选择债权批量选择同一去向 | — |
/api/artist_center)与用户端结构一致,差异:
/work_tasks/cancellation/{preview,info,request,withdraw,reject,accept};画师发起 request 不接收 refund_destination(画师不能决定客户去向);画师 accept 也不新增去向选择,沿用用户在申请中提交的偏好。/work_task_price_changes/info(详情,仅画师端提供);画师 create 不接收 refund_destination。/commission_refunds/{list,info},没有 select_destination。| 字段 | 说明 |
|---|---|
cancellation_entry | 入口状态:{ mode, can_start, disabled_reason } |
active_cancellation | 仍占用该 Commission 的取消申请摘要,没有则 null |
price_change | 仍处于 pending/wait_pay 的改价对象,没有则 null |
refund_pool | 未完成退款债权汇总:数量、待操作数量、待选择债权 id、合计金额与批量去向交集 |
financial_resolution | 当前财务决议摘要;没有则 null |
cancellation_entry.mode:
direct:可直接取消(无已付订单且状态为 pending/wait_pay)——仍调用统一 preview 与 request;不存在独立的直接取消端点;negotiated:需要对方确认;unavailable:入口不可用,用稳定 disabled_reason 决定提示文案。cancellation_entry.disabled_reason 可能取值:active_cancellation、payment_processing、active_price_change、payment_data_incomplete、status_not_cancellable。不存在 active_refund 取值。
active_cancellation 只包含非终态(pending、refund_processing、settlement_processing、failed);completed/rejected/withdrawn 视为历史,通过 cancellation/info 查询。
| 情况 | HTTP | Body |
|---|---|---|
| 成功 | 200 | { "data": ... }(列表为 { "data": [...], "total": n }) |
| 业务规则失败 | 400 | { "code": 22004, "message": "..." } |
| 字段格式错误 | 422 | Laravel 校验结构 |
| 不存在 / 无权访问 | 404 | — |
没有 202;异步流程统一用 200 + 资源自身的状态字段表达(取消对象是 status,退款债权是 obligation_status / execution_status 双轴,改价对象是 status)。
改价领域的业务失败同样返回带
code的 HTTP400:重复发起待处理改价是60003 WorkTaskPriceChangeAlreadyPending,approve/reject/cancel以及补款完成时的状态冲突是60004 WorkTaskPriceChangeStateInvalid(见 §9)。前端按code分支,无需再兼容无code的 400。
本章作用:逐个端点的参数表与行为要点(返回什么、失败会怎样)。响应字段的含义与用途见 §4。
POST /api/work_tasks/cancellation/preview(用户端)作用:打开取消弹窗时调用,拿到建议退款额、可退上限、可用去向与角色视角金额。能否发起取消由 WorkTask 详情的 cancellation_entry 决定,不要用本接口判断。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | integer | 是 | WorkTask ID,最小 1 |
business_refund_amount | integer | 否 | 业务币种最小单位,最小 0;省略时返回默认建议额(= refund_limits.standard);显式传 0 表示不退款并试算画师结算 |
响应为取消 Preview(见 §4 与附录 A.1)。要点:
mode = direct 时展示“立即取消”说明;negotiated 时展示协商表单,两种模式都提交到 cancellation/request。payment_state = unpaid,financials 为零值,can_submit = true、disabled_reason = no_refundable_amount。financials.refund;建议额读 refund_limits.standard(或 refund_entry.presets.standard,两者相同)。POST /api/artist_center/work_tasks/cancellation/preview 参数相同,role_view 为画师视角;画师调整金额时防抖重新试算并丢弃过期响应。POST /api/work_tasks/cancellation/request(用户端)作用:发起取消申请(negotiated)或直接完成取消(direct),写入退款额与去向并返回完整取消对象。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | integer | 是 | WorkTask ID |
business_refund_amount | integer | 条件 | 已付款时必须显式提交(可为 0);未付款传 0 或省略 |
refund_destination | string | 否 | original 或 credit;可省略,省略时债权进入 awaiting_destination 等待用户选择 |
idempotency_key | string | 是 | 8–128 字符;同一次逻辑提交的重试必须复用;改金额/改去向换新键 |
mode = direct:立即完成,返回 mode = direct、status = completed,无需 accept。mode = negotiated:创建 pending 申请,等待对方处理。22010。request 不传 refund_destination。POST /api/work_tasks/cancellation/info作用:查看最近一次取消申请并轮询进度——当前处于哪个阶段、退款与结算各自执行到哪一步。
请求 { "id": <work_task_id> },返回该 Commission 最近一次取消申请(含终态);无记录时 { "data": null }。用于轮询详情(含 refund、settlement、business_status 与 financial_status)。
与 active_cancellation 的区别:后者只返回仍占用 Commission 的申请摘要;本接口返回最近一次(即便已结束)。
POST /api/work_tasks/cancellation/withdraw | reject | accept作用:申请生成后的三种处置动作;按钮是否展示以取消对象的 can_withdraw / can_accept / can_reject 为准。
| 端点 | 参数 | 说明 |
|---|---|---|
withdraw | request_id | 发起方撤回自己的 pending 申请 |
reject | request_id | 对方拒绝 pending 申请 |
accept(用户端) | request_id、refund_destination(可选) | 接受画师申请;省略去向时债权进入 awaiting_destination |
accept(画师端) | request_id | 接受用户申请;沿用用户在发起时提交的偏好,未提交时债权进入 awaiting_destination |
三者都返回完整取消对象。接受后业务决议立即生效;存在未完成退款债权不会回滚取消。资金部分进入退款池和异步结算,前端结合 business_status、financial_status 及关联退款的双轴状态轮询。
所属领域:退款领域(Commission Refund)。 这些端点查询和操作退款池。取消与降价产生的每笔差额都是独立债权;未选择去向、执行中、暂时失败和人工处理中的债权都会保留并占用自己的资金预留。它们不会修改改价状态或 WorkTask 价格。
| 端点 | 参数 | 说明 |
|---|---|---|
POST /commission_refunds/list | work_task_id(必填)、page(默认 1)、size(默认 15,1–50) | 按 ID 倒序返回 { summary, data, total } |
POST /commission_refunds/info | id | 单个退款详情,结构与列表元素相同 |
POST /commission_refunds/select_destination | id、refund_destination(必填) | 仅 awaiting_destination 状态可调用;成功后自动进入执行队列 |
POST /commission_refunds/select_destinations | work_task_id、refund_ids、refund_destination | 为多笔 awaiting_destination 债权原子地选择同一去向;refund_ids 取 refund_pool.action_required_refund_ids 或 list 的 data[].id;返回 { data, summary },refund_ids 含不属于该 Commission 的债权返回 404 |
financials.refund;单笔业务允许去向读 cash_destinations.allowed,当前执行能力读 execution.readiness(ready / blocked / manual_review)与 execution.blocked_reason(no_refundable_amount / no_available_destination / refund_payment_data_incomplete)。select_destination 状态不符返回 22015,去向不可用返回 22016。债权进入 ready 后不能再改去向。select_destinations 的 refund_ids 至少一个且不能重复。服务端重新计算所有选中债权的去向交集;任一债权状态不符返回 22015,所选去向不在交集内返回 22018,整批要么全部生效、要么全部不生效。refund_ids 的取法:worktask/info 的 refund_pool.action_required_refund_ids,或 commission_refunds/list 的 data[].id(只取 execution_status = awaiting_destination 的元素)。前者与 refund_pool.bulk_allowed_destinations 是同一集合,成对使用即可,不需要自己跨页拼 id。refund_pool.bulk_allowed_destinations = [] 是正常查询结果,表示当前没有共同去向;前端禁用批量入口,不需要显示错误。list 与 info;list 的 work_task_id 必须属于该画师。退款列表关键结构:
退款债权不再返回
status、destination和重复的destinations:进度读obligation_status+execution_status,去向能力读cash_destinations.allowed/cash_destinations.unavailable。上例financials仅节选基础金额字段,其余金额与币种对象同样按 §4.1 返回。
批量选择请求示例(refund_ids 直接取 summary.action_required_refund_ids,refund_destination 取 summary.bulk_allowed_destinations 之一):
所属领域:改价领域(WorkTask Price Change)。 这些端点负责试算新价格、记录双方改价意愿、审批、待补款及把新价格应用到 WorkTask。它们不会执行退款。
| 端点 | 参数 | 说明 |
|---|---|---|
POST /work_task_price_changes/preview | work_task_id、price(整数,最小 0) | 加价或无需退款时 financials.refund 为 0,仍返回完整 financials |
POST /work_task_price_changes/create(用户端) | work_task_id、price(数值,最小 0)、refund_destination(可选) | 用户降价等待画师批准;用户加价直接进入 wait_pay |
POST /work_task_price_changes/create(画师端) | work_task_id、price | 画师降价立即应用;画师加价等待用户批准 |
POST /work_task_price_changes/list | work_task_id、page、size | { data, total },元素为改价对象 |
POST /api/artist_center/work_task_price_changes/info | id | 仅画师端 |
approve / reject / cancel | id | 改价状态迁移;权限按发起方与方向判定 |
refund_destination 在用户降价请求中是可选的提前去向偏好,属于改价请求附带信息:
ready,不可用或未填写则进入 awaiting_destination。price_change.refund_destination;它记录的是“偏好/已应用去向”,不是退款债权 cash_destination 的兼容别名或第二份事实来源。create 不接收去向;画师降价立即应用后,由用户在退款池选择。price_change.refund_destination 当作退款状态或最终到账证明。四种发起路径与当前代码一致:
| 发起方与方向 | create 后改价状态 | 对方动作 | 新价格何时应用 | 是否进入退款领域 |
|---|---|---|---|---|
| 用户加价 | wait_pay | 不需要画师批准 | 客户完成补款后变为 paid | 否 |
| 画师加价 | pending | 用户 approve 后变为 wait_pay | 客户完成补款后变为 paid | 否 |
| 用户降价 | pending | 画师 approve | 批准事务内立即变为 paid 并更新 WorkTask 价格 | 正退款额时创建债权 |
| 画师降价 | paid | 无需用户批准 | create 事务内立即更新 WorkTask 价格 | 正退款额时创建债权 |
这里的 paid 属于改价状态,含义是“新价格已应用”。画师降价没有补款,也会直接进入 paid。
同一 Commission 同时只能存在一笔待处理改价:create 时若已存在 pending 或 wait_pay 的改价申请,用户端与画师端都会返回 HTTP 400 与 code=60003(见 §9),并且不会新增改价记录。approve/reject/cancel 等动作遇到不允许的状态时返回 HTTP 400 与 code=60004。
退款额试算:本次退款 = max(0, 有效上限 − 新稿酬)。把价格降到有效上限时退款为 0,仍返回完整财务摘要;降价生效后会记录 Financial Resolution,但不会创建零金额 CommissionRefund。连续改价会结合历史已完成退款与尚未完成债权的资金预留,只新增未覆盖的差额。
awaiting_destination 等待用户重新选择。pending 返回 HTTP 400 与 code=60004(见 §9),因此不要把 approve 当作可重试的幂等接口。改价财务展示:
financials.payment_fee 是已付 Order 的实际手续费。financials.future_payment_fee_estimate 是变更后全部剩余待付稿酬的手续费预估,包含本次补款和后续节点;不要用 need_pay_amount × 费率 在前端重算。financials.artist_settlement_basis 等于 new_price。financials.platform_fee 按 new_price × artist.worktask_plat_fee_ratio 进入平台费政策计算,不使用“扣除支付手续费后的余额”作为基数。financials.estimated_artist_net 已同时考虑实际手续费、后续手续费预估和按新总价计算的平台费,前端直接展示。financials 是改价后的结算预测。真正的退款债权金额与执行状态,在降价生效后以 commission_refunds/info 返回为准。旧直接取消端点
POST /api/work_tasks/cancel与POST /api/artist_center/work_tasks/cancel已删除(调用返回404)。未付款直接取消与已付款协商取消现在都只有统一入口:cancellation/preview+cancellation/request(未付款时mode = direct立即完成,已付款时mode = negotiated等待对方确认)。
本章作用:把端点串成流程——先调什么、谁来确认、什么时候开始轮询、失败如何处理。
同 §7.1 镜像:画师 request 不带去向 → 用户 preview(带申请金额)取得 destinations → 用户 accept 时传 refund_destination。
前端应把同一次降价看成三个相邻但独立的领域对象:
| 所属领域 | 对象与接口 | 负责什么 | 不负责什么 |
|---|---|---|---|
| 改价领域 | WorkTaskPriceChange、work_task_price_changes/* | 新旧价格、发起/审批、待补款、价格是否已应用、改价后的财务预测 | 不表示退款是否到账 |
| 财务决议领域 | CommissionFinancialResolution、WorkTask 的 financial_resolution | 把已生效降价转换为不可变资金事实,协调退款与后续资金依赖 | 不接收用户的退款去向操作 |
| 退款领域 | CommissionRefund、commission_refunds/*、refund_pool | 退款债权、资金预留、去向选择、Provider 执行、客户是否收到退款 | 不决定新价格是否生效 |
| 支付领域 | WorkTask 补款 Order | 加价后的实际收款及支付手续费事实 | 不参与降价退款去向选择 |
跨领域只通过关联键衔接:price_change.commission_financial_resolution_id 指向财务决议,price_change.commission_refund_id 指向该次降价产生的退款债权。状态不能跨对象解释。
读取规则:
price_change.status = paid 后停止等待改价审批;如果 commission_refund_id 非空,再进入退款领域查询。price_change.financials.refund 是改价试算/记录中的预计退款金额;退款债权创建后,以 commission_refunds/info.data.financials.refund 和双轴状态为准。price_change.refund_destination 只是批准前的去向偏好(批准后写入该次债权实际采用的去向),不表示 Provider 已受理;最终去向看退款对象的 cash_destination。price_change 只返回 pending / wait_pay 改价。进入 paid 后,应从改价列表取得 commission_refund_id,再查询退款详情或退款池。| 场景 | 建议 |
|---|---|
| 取消处理中 | 每隔数秒轮询 cancellation/info;completed 后刷新 WorkTask 详情 |
| 退款处理中 | 轮询 commission_refunds/info;processing 可能因 Provider 未结算而停留,属正常,不要重复提交 |
退款 retryable_failed | 后端恢复任务会按限速策略继续处理;前端保持轮询,不重复提交退款或去向选择 |
退款或财务决议 manual_review | 停止自动操作,展示处理中/联系支持;债权仍占用对应资金,不能当作已释放 |
取消 failed | 展示 failure_stage + failure_code,停止自动重试 |
收到 400 冲突类错误 | 刷新 WorkTask、取消申请、退款、改价后重新渲染可用操作 |
22008、21012 等)。本章作用:写页面时的查表——UI 上的每一项去哪个字段取数;字段含义与用途见 §4。
路径基准:本表「取消 Preview」列省略响应根
data.(即data.financials.paid写作financials.paid);active_cancellation.*表示 WorkTask 详情中该对象的路径。
| UI 展示项 | 取消 Preview | active_cancellation |
|---|---|---|
| 客户已支付 | financials.paid | active_cancellation.financials.paid |
| 历史已退 | financials.previous_refunded | active_cancellation.financials.previous_refunded |
| 本次退款额 | financials.refund | active_cancellation.financials.refund |
| 退款后保留 | financials.retained | active_cancellation.financials.retained |
| 标准可退款 | financials.refund_limits.standard | active_cancellation.financials.refund_limits.standard |
| 输入框上限 | financials.refund_limits.maximum | active_cancellation.financials.refund_limits.maximum |
| 支付手续费 | financials.payment_fee | active_cancellation.financials.payment_fee |
| 画师承担手续费 | financials.artist_fee_coverage | 同名字段 |
| 平台服务费 | financials.platform_fee | 同名字段 |
| 预计画师净收入 | financials.estimated_artist_net | 同名字段 |
| 退款后余额(画师) | financials.artist_settlement_basis | 同名字段 |
| 可用退款去向 | destinations.available | active_cancellation.destinations.available |
| 去向不可用说明(可选文案;不渲染置灰项) | destinations.unavailable | 同名 |
| 退款额输入是否可用 | disabled_reason | —(申请已提交,不可改额) |
| 提交按钮 | can_submit | active_cancellation.can_accept / can_reject / can_withdraw |
| 是否允许超标准上限 | refund_policy.can_exceed_standard | 同名 |
| 网关退款明细(用户) | role_view.gateway_refunds | active_cancellation.role_view.gateway_refunds |
| Credit 退款明细(用户) | role_view.credit_refunds | 同名 |
| 偏好币种估算(用户) | role_view.estimated_display_amount | 同名 |
| 平台费拆解(画师) | role_view.platform_fee_breakdown | 同名 |
role_view按视角互斥:画师视角只返回platform_fee_breakdown,用户视角只返回gateway_refunds/credit_refunds/estimated_display_amount/estimate_notice;不要假设两类字段同时存在。
改价对象(列表元素)与 WorkTask 详情 price_change 使用以下绑定;表中路径同样省略响应根 data.。注意:work_task_price_changes/preview 只返回 work_task_id、old_price、new_price、financials、refund_policy、destinations、can_submit、disabled_reason、warnings、issues,不返回下表的 status、need_pay_amount、work_task_paid_amount、refund_destination、commission_refund_id、commission_financial_resolution_id;Preview 阶段只做金额试算与去向预览。
| UI 展示项 | 改价字段 | 说明 |
|---|---|---|
| 改价前稿酬 | old_price | Money |
| 改价后稿酬 | new_price | Money |
| 改价业务状态 | status | 只表示改价协商、待补款或价格已应用 |
| 本次待补缴 | need_pay_amount | 只表示当前改价需要支付的金额,不代表全部剩余待付款 |
| 客户累计已支付 | financials.paid | 已完成退款不会减少该值 |
| 降价预计退款额 | financials.refund | 改价试算投影;不表示退款债权已创建或已到账 |
| 已产生支付手续费 | financials.payment_fee | 已付款 Order 的实际手续费 |
| 后续支付手续费预估 | financials.future_payment_fee_estimate | 对全部剩余待付款的预估;不要复用 payment_fee |
| 变更后稿酬/平台费基数 | financials.artist_settlement_basis | 等于 new_price |
| 预计平台服务费 | financials.platform_fee | 按变更后总价及画师配置费率计算 |
| 预计画师最终净收入 | financials.estimated_artist_net | 已包含实际手续费、未来手续费预估和平台费 |
| 提前去向偏好 | refund_destination | 改价批准前用户填写的偏好;批准后由后端写入该次退款债权实际采用的去向;不能用来判断退款执行状态 |
| 退款债权关联 | commission_refund_id | 非空后转到退款领域查询;为空不代表改价失败 |
| 财务决议关联 | commission_financial_resolution_id | 表示降价财务事实已经记录,不表示退款完成 |
WorkTask 详情与退款池使用以下绑定:
| UI 展示项 | 字段 | 说明 |
|---|---|---|
| 未完成退款数量 | refund_pool.open_count | 可用于入口角标 |
| 需要选择去向数量 | refund_pool.action_required_count | 大于 0 时提示用户处理 |
| 未完成退款总额 | refund_pool.financials.open_amount | Money,直接使用内嵌币种展示 |
| 待选择债权 id | refund_pool.action_required_refund_ids | 与 bulk_allowed_destinations 同一集合;直接作为 select_destinations 的 refund_ids,空数组表示无需选择 |
| 批量可选去向 | refund_pool.bulk_allowed_destinations | 空数组时禁用批量入口 |
| 当前财务进度 | financial_resolution.status | 与客户退款义务状态分开显示 |
| 单笔债权状态 | obligation_status + execution_status | 双轴状态;退款债权不再有单轴 status |
| 单笔允许去向 | cash_destinations.allowed | 只渲染返回的选项 |
| 单笔当前阻塞 | execution.readiness + execution.blocked_reason | 用于处理中或联系支持提示 |
说明:
currency({ id, code, symbol, is_zero_decimal }),渲染时取该金额同级的 currency 即可;gateway_refunds / credit_refunds 是 Money 列表,元素内自带币种,不需要站点币种表。financials 是唯一业务金额来源;退款/取消/改价资源对象不再返回 business_refund_amount 等标量镜像(读 financials.refund)。role_view 只含角色专属派生信息;financials 对双方都可见,因此画师净收入等字段不再在 role_view 重复。financials.refund 必须结合所属对象理解:在 PriceChange 上是改价投影,在 CommissionRefund 上是已创建债权金额。price_change 只代表 pending/wait_pay 的改价申请;改价进入 paid 后要查看退款进度,请用改价列表取得 commission_refund_id 再查退款。本章作用:联调排错——按 code 分支处理,不要解析 message 文案。
改价入口的业务失败与其它领域一致,返回带
code的 HTTP400:重复发起待处理改价是60003,approve/reject/cancel以及补款完成时的状态冲突是60004。字段格式错误仍由422返回(见 §5.4)。
| 错误码 | 枚举 | 触发场景 | 前端建议 |
|---|---|---|---|
22001 | RefundSubjectNotFound | 退款主体不存在 | 刷新页面 |
22002 | RefundSubjectUnsupported | 主体类型不支持 | 隐藏入口 |
22003 | RefundNoPaidOrders | 无可退款订单 | 刷新支付状态 |
22004 | RefundAmountInvalid | 金额超过有效上限 / 无剩余额度下提交非零金额 | 用 refund_limits.maximum 重新限制输入 |
22005 | RefundPaymentDataIncomplete | 支付/对账数据不完整、存在支付中订单 | 禁止提交并联系支持 |
22006 | RefundAllocationInvalid | 资金分配或历史分摊不自洽 | 禁止提交并联系支持 |
22007 | RefundWorkTaskStatusInvalid | Commission 状态不允许取消 | 刷新状态 |
22008 | CommissionCancellationAlreadyPending | 已有进行中的取消 | 展示已有申请 |
22009 | CommissionCancellationNotFound | 取消申请不存在 | 刷新 |
22010 | CommissionCancellationStateInvalid | 状态不允许该操作 / 幂等键复用条款不同 | 刷新状态;检查幂等键 |
22011 | CommissionCancellationOwnRequest | 发起人不能响应自己的申请 | 仅保留撤回按钮 |
22012 | CommissionCancellationTermsChanged | 提交时事实已变化 | 重新试算并重新发起 |
22013 | CommissionRefundExecutionIncomplete | 存在执行中(processing / partially_completed)的退款债权时尝试继续支付 | 展示退款池并等待处理 |
22015 | CommissionRefundStateInvalid | 退款状态不允许该操作(如非 awaiting_destination 选去向) | 刷新退款状态 |
22016 | CommissionRefundDestinationInvalid | 所选去向不在可用集合内(退款债权读 cash_destinations.allowed,取消/改价读 destinations.available),或降价退款未选择去向 | 按最新可用去向重选 |
22017 | CommissionCancellationActionUnavailable | 取消动作与当前模式不匹配(如对已付款 Commission 走直接取消),或取消入口不可用 | 刷新 cancellation_entry,统一改用 cancellation/preview + cancellation/request |
22018 | CommissionRefundNoCommonDestination | 批量选择的去向不在所选债权的共同允许集合内 | 刷新退款池;交集为空时禁用批量入口 |
22019 | CommissionRefundReconciliationUnavailable | 对账资格校验不通过:债权未完成、执行计划缺失、步骤未全部成功,或 Stripe recovery/reversal 配对与 Provider 引用不完整 | 按最新状态刷新后再对账(对账为管理后台受控操作,接口细节见 pipipen-api 代码) |
22020 | CommissionFinancialOperationRequestConflict | 内部人工财务操作的 X-Request-Id 复用但请求条款不同,或上一次请求仍停在 processing / 已 failed | 不要自动重放;先核对目标资源当前事实,再决定是否用新的 X-Request-Id 重试 |
22021 | CommissionCancellationSettlementPlanInvalid | 取消结算时无法把冻结快照落成结算步骤:缺少平台费钱包计划、平台费还原无法映射到原始抵扣,或 Credit 画师调整缺少目标钱包(持久数据不变量) | 不要重试;联系支持核对冻结快照与钱包事实 |
60001 | WorkTaskPriceChangeInvalid | 改价提交的新价格与当前价格相同 | 提示重新输入价格 |
60002 | WorkTaskPriceChangeTermsChanged | 改价批准时事实已变化 | 重新试算并重新提交改价 |
60003 | WorkTaskPriceChangeAlreadyPending | 该 Commission 已存在 pending / wait_pay 的改价申请,用户端或画师端再次发起 | 展示已有改价申请并等待其结束,不再提交 |
60004 | WorkTaskPriceChangeStateInvalid | approve/reject/cancel 或补款完成时改价状态不允许该动作(如重复批准已 paid 的改价) | 刷新改价状态后再操作 |
21012 | WorkTaskPaymentActionConflict | 存在进行中的改价/退款/取消导致冲突 | 刷新相关资源 |
本章作用:说明本功能的开发阶段契约不承诺兼容,并列出本次清理移除的字段、端点、命令、主题与错误码;逐条旧→新绑定映射见 §11。
本功能仍处于开发阶段,接口按“正确契约优先”演进,不提供旧字段兼容层。 注意区分两件事:
- API 不兼容:旧字段、旧路由、旧命令、旧 Topic 不再返回或存在;调用方必须按本文档升级。
- 数据库可增量部署:已运行过历史 migration 的测试数据库不需要
migrate:fresh,执行普通php artisan migrate即可。正向 migration2026_09_16_000001_finalize_commission_refund_schema会先校验commission_refunds.execution_status无NULL且双轴取值都在当前枚举内,通过后删除status、destination、active_work_task_id,并把execution_status收紧为非空;资金金额、双轴状态、cash_destination与 Financial Resolution 关联都保留。校验发现漂移数据时会中止 deployment 并要求人工处理,不会按旧status重新推导或补造状态。
本次破坏性清理(2026-09-16 按代码复核):
status、destination,也不再返回与 cash_destinations 重复的 destinations。前端只能读 obligation_status + execution_status 双轴,以及 cash_destination / cash_destinations.allowed / cash_destinations.unavailable。旧 CommissionRefundStatus 枚举已不存在。destinations.available / destinations.unavailable 只属于取消 Preview、取消对象与改价能力集合;退款债权改用 cash_destinations.allowed / cash_destinations.unavailable。commission_refunds 表移除 status、destination、active_work_task_id 列;新增 commission_financial_resolution_id(可空)、obligation_status(非空,默认 open)、execution_status(非空、无默认值)、cash_destination(可空)、preference_applied、action_required_at、manual_review_reason。两轴状态均为非空,前端不再需要处理历史 null。commission_refunds 表删除 next_retry_at、last_notified_at 两列(2026_09_17_000004):它们没有任何生产读者,重试由执行事实与 Outbox 驱动。这两个字段从未是公开契约字段;未来的通知需求必须记录自己的投递事实,不复用含义不清的通用时间戳。POST /api/work_tasks/cancel 与 POST /api/artist_center/work_tasks/cancel(调用返回 404)。未付款直接取消只能走统一 cancellation/preview + cancellation/request(mode = direct)。refund:retry-commission 与 commission-refund:backfill-allocations。保留的运维命令为 commission-refund:retry、refund:retry-commission-cancellation、commission-refund:reconcile、commission-financial-resolution:dispatch-pending、commission-financial-resolution:resume、wallet:record-stripe-refund-adjustment。commission.financial_resolution.recorded。现存主题为 commission.refund.execute、commission.financial_resolution.resume、commission.cancellation_settlement.execute。22015–22019 取值与含义不变,并新增 22020(内部人工操作请求冲突)与 22021(取消结算计划数据不变量)。取消入口的 active_refund 禁用原因同样不再存在。voided 是不可执行终态:已作废债权不能选择去向、不能被执行;陈旧执行消息对它是幂等 no-op(不调用 Provider、不增加执行次数、不重放步骤)。voidRefund 只允许作废仍为 open、全部 allocation 仍 reserved 且客户履约步骤尚未开始的债权;已开始 Provider 操作、部分履行或已 fulfilled 的债权拒绝作废。voided 与 execution_status = not_required 稳定配对(2026_09_17_000003):作废时两轴一次提交,历史数据中曾被投影为 retryable_failed 的作废债权会被规范化为 not_required。该迁移在写入前校验作废债权没有已开始/成功的客户履约步骤、且 allocation 已全部 released,不满足则中止并输出数量与样例 ID。not_required 既不是失败也不是履约完成。allocation_integrity_invalid:客户 Step 与其 allocation 的关联属于数据不变量。open 债权出现该异常时进入 execution_status = manual_review;fulfilled 债权保持 obligation_status = fulfilled + execution_status = completed(不把客户已收到的钱展示为退款失败),异常记录在 Refund 的 manual_review_reason / last_error_code / last_error,并把该债权自身的 Financial Resolution 以及通过 dependency 依赖它的 Resolution 一并置为 manual_review。在本次清理之前已经完成的其它统一化(相对更早原型仍然不兼容):
refund.business_paid_amount、refund.business_refund_amount、refund.business_retained_amount、refund.standard_maximum_refundable_amount、refund.absolute_maximum_refundable_amount、refund.effective_maximum_refundable_amount、refund.suggested_refund_amount、refund.artist_fee_coverage_amount、refund.payment_fee_amount、refund.payment_fee_bearer、refund.business_currency,以及取消/退款资源上的 business_paid_amount、business_refund_amount、*_maximum_refundable_amount、currency(金额与其币种统一由 financials 提供)。refund 块改名为 refund_entry,只保留 presets(Money)与 fee_policy。calculation_complete 与 can_submit_cancellation_request 不再返回,统一使用 can_submit(并配 disabled_reason)。old_price、new_price、need_pay_amount、work_task_paid_amount 改为 Money;old_price_money、new_price_money、business_refund_amount 移除。role_view:金额改为 Money;移除与 financials 重复的字段(payment_processing_fee、refund_to_user、final_platform_fee、estimated_artist_net、currency、business_currency);gateway_refunds / credit_refunds 由 { 币种码: 金额 } 改为 Money 列表;画师平台费拆解移入 role_view.platform_fee_breakdown。work_task.contract_amount 改为 Money,work_task.business_currency 移除。financials 新增 retained(退款后保留额)。financials 新增 future_payment_fee_estimate;payment_fee 明确只表示实际已产生手续费。改价的 artist_settlement_basis、platform_fee、estimated_artist_net 改为按变更后总稿酬预测。work_task.destinations(改用顶层 destinations,语义属于本次退款)、role_view.balance_after_refund(改用 financials.artist_settlement_basis)。financials 不按角色裁剪:用户端同样包含 artist_settlement_basis、platform_fee、estimated_artist_net、artist_fee_coverage 与 payment_fee;角色专属信息只在 role_view。refund_pool 与 financial_resolution;一张 Commission 可同时存在多笔未完成退款债权。obligation_status + execution_status 双轴状态、cash_destination、cash_destinations、action_required_at、preference_applied。business_status 与 financial_status,业务决定不会因后续 Provider 执行失败而回滚。POST /commission_refunds/select_destinations;共同去向为空是正常读模型结果,非法提交使用 22018。本章作用:改前端时的操作清单——按当前绑定逐条替换;变更原因见 §10。
下表按 pipipen-front 当前 Commission 详情页(用户端 / 画师端)的实际绑定整理,逐条给出新字段。请求参数与错误码不变。
| 旧绑定 | 新绑定 | 说明 |
|---|---|---|
refund_preview.refund.standard_maximum_refundable_amount | refund_preview.financials.refund_limits.standard.amount | 默认建议退款额 |
refund_preview.refund.absolute_maximum_refundable_amount | refund_preview.financials.refund_limits.maximum.amount | 语义修正:absolute 在平台收款场景大于有效上限,旧绑定会让输入越过硬上限并被 22004 拒绝;输入上限一律用 maximum |
refund_preview.refund.business_paid_amount | refund_preview.financials.paid.amount | 已支付额 |
refund_preview.refund.business_retained_amount | refund_preview.financials.retained.amount | 退款后保留额 |
refund_preview.refund.payment_fee_amount | refund_preview.financials.payment_fee.amount | 支付手续费 |
refund_preview.refund.artist_fee_coverage_amount | refund_preview.financials.artist_fee_coverage.amount | 画师承担的手续费 |
refund_preview.refund.payment_fee_bearer | financials.artist_fee_coverage.amount > 0 ? 'artist' : 'user' | 承担方由覆盖额推导,不再单独返回 |
refund_preview.refund.presets.unfinished_stages / .standard / .full | refund_preview.refund_entry.presets.unfinished_stages / .standard / .full(各取 .amount) | 三个快捷额 |
refund_preview.refund.fee_policy | refund_preview.refund_entry.fee_policy | — |
refund_preview.role_view.refund_to_user | refund_preview.financials.refund.amount | 退款给用户 |
refund_preview.role_view.payment_processing_fee | refund_preview.financials.payment_fee.amount | — |
refund_preview.role_view.balance_after_refund | refund_preview.financials.artist_settlement_basis.amount | 退款后余额 |
refund_preview.role_view.final_platform_fee | refund_preview.financials.platform_fee.amount | — |
refund_preview.role_view.estimated_artist_net | refund_preview.financials.estimated_artist_net.amount | — |
refund_preview.role_view.platform_fee_basis / gross_platform_fee / open_call_fee_waiver / plat_fee_wallet_applied | refund_preview.role_view.platform_fee_breakdown.basis / .gross / .open_call_fee_waiver / .wallet_applied(均为 Money) | 画师专属拆解 |
refund_preview.role_view.gateway_refunds.CNY(映射取值) | refund_preview.role_view.gateway_refunds[].amount + .currency | 改为 Money 列表,不再需要站点币种表 |
refund_preview.role_view.credit_refunds.CNY(映射取值) | refund_preview.role_view.credit_refunds[].amount + .currency | 同上 |
refund_preview.role_view.estimated_display_amount(数字) | refund_preview.role_view.estimated_display_amount.amount(Money;缺汇率时整体 null) | — |
refund_preview.role_view.display_currency | refund_preview.role_view.estimated_display_amount.currency | 币种内嵌进金额 |
refund_preview.can_submit_cancellation_request / calculation_complete | refund_preview.can_submit | 提交判定单字段;disabled_reason 说明退款输入为何不可用 |
active_cancellation.business_refund_amount | active_cancellation.financials.refund.amount | 申请金额 |
active_cancellation.business_refund_amountt(用户端详情页拼写多一个 t,实际恒为 0) | active_cancellation.financials.refund.amount | 迁移时顺带修掉拼写;否则该处显示始终为 0 |
active_cancellation.business_paid_amount | active_cancellation.financials.paid.amount | — |
active_cancellation.standard_maximum_refundable_amount | active_cancellation.financials.refund_limits.standard.amount | — |
active_cancellation.absolute_maximum_refundable_amount | active_cancellation.financials.refund_limits.maximum.amount | 输入上限语义同前 |
active_cancellation.role_view.*(画师卡片明细) | 同上规则:金额改指 financials.*,平台费拆解改指 role_view.platform_fee_breakdown.* | — |
work_task.destinations | 顶层 destinations | 去向属于本次退款 |
work_task.business_currency | 各金额自带的 currency | — |
金额硬编码 / 100 | 依据 currency.is_zero_decimal 决定小数位 | 修正零小数位币种(如 JPY)显示 |
改价:old_price / new_price / need_pay_amount / work_task_paid_amount(数字) | 同名 Money(读取 .amount) | 价格改为金额对象 |
改价:business_refund_amount | price_change.financials.refund.amount | — |
改价:old_price_money / new_price_money | 移除(直接用 old_price / new_price) | — |
改价 UI「后续支付手续费预估」绑定 price_change.financials.payment_fee | price_change.financials.future_payment_fee_estimate | payment_fee 是已产生的实际手续费,不能重复用于未来预估 |
保持不变:请求参数 business_refund_amount(数字,业务币种最小单位)、refund_destination、price、idempotency_key;错误码与 HTTP 语义;active_cancellation.id / initiator_role / viewer_is_initiator / can_withdraw / can_accept / can_reject;commission_refund_id;取消对象与改价对象的 status(退款债权没有单轴 status)。
Q:退款额输入上限为什么不是 financials.refund_limits.absolute?
A:absolute 是理论上限(P − R),只有全部有效 Order 都是 Stripe 关联账户收款时才能达到;其他场景提交超过 maximum 的金额会返回 22004。上限只用 maximum(见 §3.1)。
Q:退款完成后为什么 financials.paid 没有减少?
A:paid 是「终身累计已付」这一支付事实,退款不会回写它;当前可退额度由 refund_limits 与 previous_refunded 表达(见 §3.1)。
Q:disabled_reason = no_refundable_amount 时还能提交吗?
A:能。它只表示退款额输入不可用(未付款或已无可退额度),零退款取消与改价仍然合法;能否提交看 can_submit(见 §4.5)。
Q:用户端为什么能看到画师的平台费与净收入?
A:financials 是双方的共同事实层,不做角色裁剪;角色专属信息(画师平台费拆解、用户偏好币种估算与网关/Credit 投影)在 role_view。是否展示由 UI 决定(见 §4.6)。
Q:为什么 destinations.available 只有 original?
A:Commission 中存在 Stripe 关联账户收款(destination charge)Order;原因码在 destinations.unavailable.credit(见 §3.2、§4.4)。
Q:退款为什么长时间停在 processing?
A:对账阶段需要读取 Provider 账本;Provider 尚未结算或账本暂不可用时后端会保持 processing 并自动重试,属正常状态,不要重复提交或引导用户重试(见 §3.4、§7.4)。
Q:客户已经收到退款,为什么 Financial Resolution 仍是 manual_review?
A:客户退款履行与画师/平台资金回收是两组事实。obligation_status = fulfilled 表示客户退款已完成;Transfer 冲正、画师追缴或对账未完成时,Financial Resolution 仍可进入 manual_review。前端不能把它显示成客户退款失败(见 §4.8)。
Q:多笔退款的批量去向为什么是空数组?
A:每笔债权在创建时冻结自己的可用去向,多笔债权可能没有共同去向。refund_pool.bulk_allowed_destinations = [] 是合法结果,应禁用批量入口并让用户逐笔选择,不要自行回退到 Credit 或原路退款(见 §6.5)。
Q:worktask/info 的 refund_pool 里为什么没有每笔退款的 id?
A:refund_pool 是摘要读模型,不是债权列表。批量选择要用的 id 在 refund_pool.action_required_refund_ids,它与 bulk_allowed_destinations 是同一集合、可直接提交;要渲染每一笔债权的金额、cash_destinations.allowed 与执行状态,读 commission_refunds/list 的 data[](其 summary 与 refund_pool 同构,见 §4、§6.5)。
Q:改价 status = paid 了,但退款没完成?
A:paid 只表示新价格已应用;退款是独立资源,请用改价对象的 commission_refund_id 查退款状态(见 §4.8)。
Q:用户降价时必须选择退款去向吗?
A:不必须。work_task_price_changes/create 的 refund_destination 是可选偏好。未填写或降价生效时偏好已不可用,改价仍然生效,退款债权进入 awaiting_destination,再由用户通过退款领域接口选择(见 §6.6、§7.3)。
Q:为什么改价对象和退款对象都有 financials.refund?
A:所属领域不同。改价对象中的值是“该价格方案生效后预计产生的退款额”;CommissionRefund 中的值是已经创建并预留资金的债权金额。前端用 commission_refund_id 从改价领域跳转到退款领域,退款执行状态只读后者(见 §7.3、§8)。
Q:改价里的 payment_fee 和 future_payment_fee_estimate 有什么区别?
A:payment_fee 汇总已付款 Order 实际发生的手续费;future_payment_fee_estimate 按画师 pay_fee_ratio 估算变更后全部未付款稿酬的手续费。前者是事实,后者是预测,不能复用同一字段(见 §3.6、§6.6)。
Q:为什么改价的平台费不是先减支付手续费再乘费率?
A:Commission 正常完成结算以总稿酬为平台费基数,支付手续费和平台费分别扣除。因此改价的 platform_fee 按 new_price 与画师数据库费率进入平台费政策计算;费率不是固定 5%(见 §3.6)。
Q:Preview 里为什么看不到 orders、stripe、calculation_snapshot?
A:内部字段一律不返回;需要执行进度用 commission_refunds/info 的 execution.steps(见 §4.9、§6.5)。
Q:金额是不是都除以 100?
A:不要硬编码。每个金额自带 currency.is_zero_decimal:false 除以 100 显示两位小数,true 直接显示整数(见 §2.3)。
业务事实与资金执行分离。取消接受、降价生效或 Commission 完成时,接口先以 HTTP 200 返回已经落库的业务结果;退款、追缴、对账和画师结算随后通过 Financial Resolution 异步收尾。
目标金额 − 历史已结算金额,不会重复发放已经结算的部分。manual_review。manual_review,不会猜测跨币种金额。本节描述的是基础设施可靠性,不是新增的前端业务领域或接口。Outbox 可以理解为数据库里的“可靠待办”:后端在保存取消、降价或财务决议的同一个事务中,也保存一条后续资金任务。这样即使接口返回后服务立刻重启,任务也不会只存在于内存队列里而丢失。完整术语解释见 §2.5。
后台执行顺序:
processing。后端保留已成功的事实并继续对账,不会把“暂时查不到最终结果”当作失败后重新退款。retry_after 与退款步骤的 300 秒租约),业务恢复最多重新投递 5 次;仍无法安全恢复时进入 manual_review,停止自动动钱。fulfilled、但收尾投影(取消 hand-over、依赖 satisfied_at)因进程崩溃或投递耗尽而缺失时,恢复扫描与人工 resume 会按已持久化终态补齐这些投影,不会重新向客户退款;归属不一致时保留客户 fulfilled,把异常写入 Refund 诊断并把关联 Financial Resolution 置为 manual_review。cancellation_handover_failed 的债权会被排除在自动收尾扫描之外,不再反复占用每轮扫描预算;它们仍可由人工受控的 reconcile / resume 处理,收尾成功后该 blocker 被清除。已经被并发 worker 标记为 completed 的 Financial Resolution 是终态:收尾失败只写 Refund 诊断,绝不把它降级回 manual_review。trigger_type + trigger_id,不依赖待修复的 commission_refund_id 投影:该投影为空、错指或对应取消行缺失时,扫描仍能发现并安全停放。cancellation_handover_failed 只表示已证明的持久领域冲突;数据库死锁、连接/查询错误、类型错误和其它未知系统异常会整体回滚并保留自动重试能力,不会被伪装成人工审核事实。单条被 deferred 的记录不消耗本轮修复预算,也不阻止更高 ID 的候选被处理。对前端而言,只需要遵守三个边界:HTTP 200 表示业务结果已经保存,不表示异步资金步骤已经全部完成;pending / processing 时按接口状态轮询;进入 manual_review 后停止自动操作,并分别读取退款债权与财务决议状态,不能统一显示成“客户退款失败”。
按版本号顺序执行本功能的全部数据库迁移:
2026_09_04_000001_create_commission_cancellation_refund_tables2026_09_11_000001_create_commission_financial_resolution_tables2026_09_11_000002_allow_multiple_open_commission_refunds2026_09_14_000001_add_execution_facts_to_commission_financial_resolutions2026_09_15_235959_backfill_legacy_commission_refund_allocations2026_09_16_000001_finalize_commission_refund_schema2026_09_17_000001_add_requested_refund_destination_to_commission_cancellations2026_09_17_000002_create_commission_financial_operation_logs2026_09_17_000003_normalize_voided_commission_refund_execution_status2026_09_17_000004_drop_unused_commission_refund_retry_notification_columns已运行过历史 migration 的测试数据库只需普通 php artisan migrate(保持上述顺序),由正向迁移先校验数据再改结构,不需要 migrate:fresh。
迁移采用 expand → backfill → validate → contract:先加字段/表并回填,再让代码依赖,最后删旧字段。任何无法证明数据正确的步骤都会失败关闭:迁移中止并输出异常行数量与样例 ID,交人工核查;修好数据后重新执行普通 php artisan migrate 即可继续,不依赖 fresh。
数据迁移的 down() 不回退已经形成的财务历史(例如 2026_09_17_000003 明确 no-op),避免把终态事实猜回一个错误标签。
commission_cancellations.requested_refund_destination 仅为服务端幂等条款的不可变事实,不是公开 API 字段,不会出现在取消对象里;对外仍然只读 refund_destination(实际冻结去向)。
队列时序不变量:retry_after 是连接级参数,必须大于同一连接上所有 Job 的 max(timeout)。本应用 default / payments / translate / compress_image worker 共用 QUEUE_CONNECTION,最长 Job timeout 为 1800 秒(TranslateJob、CompressImage),因此仓库默认值取 2100 秒(DB_QUEUE_RETRY_AFTER / BEANSTALKD_QUEUE_RETRY_AFTER / REDIS_QUEUE_RETRY_AFTER);COMMISSION_RECOVERY_STALE_SECONDS 默认 2400 秒,高于 retry_after 与 300 秒步骤租约。发布前必须核对生产/测试环境的真实 override 满足同一关系,单元测试只能证明仓库默认值,不能证明服务器变量已配置。
保持 Outbox 扫描每分钟运行,财务恢复扫描每五分钟运行,并确保队列 Worker 正常消费。
当前没有“3~7 天后自动选择去向或自动退款”。未选择去向的债权继续留在退款池,等待用户明确选择。
每个示例都标注了它演示什么,可直接对照复制。
演示:平台收款 + 已付款,退款额等于标准上限,refund_policy 表明可退 Credit(maximum 与 standard 相等)。
演示:纯 Stripe 关联账户收款时 maximum 大于 standard、credit 被禁用、超出部分计入画师承担额。
片段示例,
currency: {}为省略占位,实际返回完整币种对象。
演示:取消对象的完整形态——摘要字段 + currency_id + refund(退款对象)+ settlement(结算步骤)+ 时间戳。
说明:内层
refund.financials、refund.refund_policy的结构与上文一致,此处为节省篇幅省略其余字段。
演示:Commission 已付 CN80.00、实际手续费 CN5.49,由 CN80.00 加价到 CN400.00;画师平台费率为 10%,后续支付手续费预估率为 4%。payment_fee 与 future_payment_fee_estimate 分开返回,平台费按变更后总稿酬计算。
future_payment_fee_estimate = ceil((40000 − 8000) × 4%) = 1280;platform_fee = 40000 × 10% = 4000;estimated_artist_net = 40000 − 549 − 1280 − 4000 = 34171。最终手续费以实际支付 Order 为准。
演示:未付款场景——payment_state = unpaid、mode = direct、financials 全零但仍带币种、disabled_reason = no_refundable_amount。
写请求时的查表;字段语义与边界见 §6。
| 参数 | 类型/范围 | 出现在 |
|---|---|---|
id | integer ≥ 1 | 取消 preview/info/request、退款 info、改价 info/approve/reject/cancel |
request_id | integer ≥ 1 | 取消 withdraw/reject/accept |
work_task_id | integer,必须存在 | 退款 list/select_destinations、改价 preview/create/list |
refund_ids | integer[],至少 1 个且不可重复 | 退款 select_destinations |
business_refund_amount | integer ≥ 0 | 取消 preview/request(条件必填) |
refund_destination | original | credit | 用户取消 request/accept(可选提前偏好,省略则进入 awaiting_destination)、退款 select_destination/select_destinations(必填)、用户改价 create(降价时可选提前偏好)。画师取消 request/accept 与画师改价 create 均不接受 |
idempotency_key | string 8–128 | 取消 request |
price | integer/numeric ≥ 0 | 改价 preview/create |
page / size | ≥ 1 / 1–50 | 退款 list、改价 list |