支付网关重构 PRD

1. 背景

当前线上支付流程中,PayPal/Alipay/Stripe 的支付会话和业务订单强绑定,导致以下问题:

  • 用户已经在第三方支付页面完成支付,但本地订单可能因为自动取消机制进入取消状态,最终形成支付成功但本地订单冲突。
  • orders/continue 曾出现支付渠道错乱,例如原 Alipay 订单继续支付时返回 Stripe 支付结果。
  • worktask 和 product 各自实现创建支付、继续支付、过期重建、取消逻辑,行为不统一。
  • 支付页面过期、支付单取消、业务订单取消被混在一起处理。
  • PayPal/Alipay 无法像 Stripe Checkout Session 一样可靠关闭已经打开的第三方支付页面,需要 late success 兜底。

本 PRD 用于定义当前阶段的支付网关改造目标、边界、数据模型、流程和验收标准。

2. 当前结论

当前阶段不落地独立 payment_intents 表,也不引入完整三层支付模型。

采用两层模型:

Order = 一次待支付订单 / 轻量支付意图
Provider Session = 一次渠道支付尝试

对应现有表:

orders
stripe_checkout_sessions
alipay_checkout_sessions
paypal_checkout_sessions

原因:

  • 当前前端和业务只有“继续支付”和“取消订单”,没有“保留订单并切换支付渠道”的交互。
  • 用户取消订单后,钱包抵扣应回退,之后重新选择渠道和钱包,重新创建 order 和 provider session。
  • worktask 没有传统订单中心,订单只是阶段支付、全额支付、改价时生成的支付单。
  • product 有 product license,更接近订单中心,但当前行为也是继续支付和取消订单。
  • 独立支付意图层会增加迁移复杂度,短期收益不足。

3. 设计目标

3.1 产品目标

  • 保持当前前端接口和页面交互尽量不变。
  • 用户看到的核心动作仍是:
继续支付
取消订单
  • worktask 的“取消订单”只取消当前支付订单,不取消 worktask。
  • product 的“取消订单”取消 unpaid product license/order,并恢复库存和钱包抵扣。
  • 用户不会因为第三方支付页面异常、刷新、返回、重试而重复履约或丢失已支付订单。

3.2 技术目标

  • 在现有两层模型上收敛支付网关逻辑。
  • Order 承担当前阶段的轻量支付意图。
  • provider session 表承担支付尝试。
  • 同一个业务对象同一时刻只能有一个 paying order。
  • 一个 paying order 同一时刻只能有一个 active provider session。
  • continue 只能继续当前 active session,或在安全条件下同渠道重建 session。
  • 禁止任何静默切换支付渠道。
  • 支付 session 过期不能自动取消业务订单。
  • PayPal/Alipay late success 必须进入幂等、冲突、人工退款/人工处理兜底。

4. 非目标

  • 不新增 payment_intents 表。
  • 不新增 payment_attempts 表。
  • 不实现“保留 order 并切换支付渠道”的用户能力。
  • 不实现自动退款。
  • 不承诺能物理关闭用户已经打开的 PayPal/Alipay 第三方支付页面。
  • 不改变 worktask/product/project 的核心业务形态。
  • 不把 project 本身改造成支付对象。

5. 核心领域模型

5.1 实体关系

5.2 层级定义

Business Object
  work_task / product_license

Order
  当前支付订单
  当前阶段的轻量支付意图
  记录业务金额、币种、钱包抵扣、金额计算过程

Provider Session
  Stripe/Alipay/PayPal 的一次支付尝试
  记录渠道支付链接、provider id、状态、过期时间

5.3 数量关系

  • 同一 worktask 同一时刻只能有一个 paying order。
  • 同一 product license 同一时刻只能有一个 paying order。
  • 同一 paying order 同一时刻只能有一个 active provider session。
  • 同一 order 可以有历史 provider sessions,但 active session 只能有一个。
  • 如果出现多个 active provider sessions,返回明确 error code,不自动选择其中一个。

6. 状态设计

6.1 Order 状态

当前重点使用:

paying
paid
cancelled

含义:

  • paying: 当前支付订单待支付。
  • paid: 支付成功,业务已或可履约。
  • cancelled: 用户取消当前支付订单,wallet deduction 已按现有流程退款/释放。

6.2 Provider Session 状态

三类 session 保持各自现有 enum,但网关层统一抽象为:

pending / requires_action
processing
finished / succeeded
expired
cancelled
failed
conflicted

含义:

  • pending / requires_action: 用户需要打开支付页面或完成授权。
  • processing: 渠道已受理,等待最终结果。
  • finished / succeeded: 该渠道支付成功。
  • expired: session 已过期。
  • cancelled: session 被本地取消或渠道确认取消。
  • failed: 渠道明确失败。
  • conflicted: 渠道成功但本地 order 已取消/已支付/金额不匹配等,需要人工处理。

6.3 状态图

7. 核心业务流程

7.1 创建支付订单

规则:

  • 创建支付时必须锁业务对象。
  • worktask 需要锁 work_tasks,并检查 active paying order。
  • product 需要锁 product_options 库存,创建 product_license,再创建支付订单。
  • 钱包/礼品卡抵扣继续使用现有 wallet deduction 机制。
  • 0 元支付仍走 internal/confirm zero 流程,不创建第三方 provider session。

7.2 继续支付

规则:

  • continue 只能继续当前 active provider session。
  • 不允许 Alipay order fallback 到 Stripe。
  • 不允许 PayPal order fallback 到 Stripe。
  • 同渠道重建只在安全条件下允许。
  • 没有 active session 时,不默认创建 Stripe。

7.3 取消订单

当前阶段不设计“取消支付尝试”。

用户点击取消时,取消的是当前支付订单:

Order -> cancelled
active provider session -> cancelled/expired/abandoned
wallet deduction -> refund/release
worktask -> 保持 wait_pay
product license -> cancelled,并恢复库存

worktask 说明:

  • worktask 的取消订单只取消支付订单,不取消 worktask。
  • worktask 在 wait_pay 状态下有专门的 worktask cancel 接口。
  • 前端可以继续使用“取消订单”文案,因为当前业务没有独立订单中心和支付尝试切换概念。

product 说明:

  • product 有 product license,更接近传统订单中心。
  • unpaid product license 的取消订单会取消 license/order,并恢复库存。

7.4 支付成功回调

规则:

  • 支付成功必须幂等。
  • order 已 paid 时,同一个 session 重复通知直接幂等返回。
  • order 已 paid 但另一个 session 成功,进入 conflicted/manual review。
  • order 已 cancelled 后 late success,进入 conflicted/manual review。
  • 金额或币种不匹配,进入 conflicted/manual review。
  • 第一阶段不自动退款。

8. 渠道策略

8.1 Stripe

能力:

  • 支持 Checkout Session 过期。
  • 平台可较可靠关闭 open session。
  • 适合同渠道自动重建。

策略:

  • continue: 如果 session open,返回原 client_secret
  • session expired 或即将不可用时,可调用 Stripe expire/同步状态。
  • 过期后可创建新的 Stripe checkout session,仍挂在同一个 order 下。
  • webhook 到来时必须以 provider session/order 锁保证幂等。

8.2 Alipay

能力限制:

  • timeout_express
  • 用户打开支付链接但未登录/未生成支付宝 trade 前,query 可能返回 404。
  • 未到过期时间前,query 404 不代表安全过期。

策略:

  • continue: 未到 expires_at 前返回原 pay_url
  • 到期后标记旧 Alipay session expired。
  • 到期后可创建新的 Alipay session 和新的 out_trade_no,仍挂在同一个 order 下。
  • 不复用旧 Alipay out_trade_no
  • 不因为 Alipay session 过期创建新的业务 order。
  • webhook/query 成功时按 late success 规则处理。

8.3 PayPal

当前代码使用 PayPal intent = CAPTURE

能力限制:

  • 没有和 Stripe Checkout Session 等价的可靠本地过期机制。
  • 不能保证关闭用户已经打开的 PayPal approve 页面。
  • 支付可能进入 processing,后续才成功或失败。

策略:

  • continue: pending/requires_action 返回原 pay_url
  • processing 时返回 PaymentProcessing,不允许取消订单,不允许重建支付。
  • 已成功时不允许取消订单。
  • 只有明确 failed/cancelled/404 等可判定不可继续结果时,才允许在同一个 order 下创建新的 PayPal session。
  • PayPal late success 如果本地 order 已 cancelled/paid/金额不匹配,进入 conflicted/manual review。

8.4 Internal

用于 0 元支付或完全由钱包/礼品卡覆盖的支付。

策略:

  • 无第三方 provider session。
  • 前端调用 confirm zero 接口完成支付。
  • 必须保证 wallet deduction 确认和业务履约在同一事务或可重试事务中完成。

9. 币种和渠道限制

当前系统支付渠道与币种有关:

  • CNY worktask/product 可用 Alipay、PayPal、Stripe。
  • 非 CNY worktask/product 当前只应使用 Stripe。
  • 非 CNY 使用 Stripe 时,需要 artist 具备可用 Stripe connected account。

当前金额计算与 pay_channel 有关:

  • Alipay 使用 CNY。
  • Stripe/PayPal 对 CNY 订单会走换汇后的支付金额。
  • Wallet deduction 记录在 order 的 amount_calc_process 中。

当前阶段不做切换渠道,所以重新选择渠道时会重新创建 order,并重新计算金额、汇率、钱包抵扣。

未来如果要做“更换支付方式但保留 order”,需要重新设计 provider amount/currency 和 wallet deduction 的关系。该能力不属于本阶段目标。

10. 钱包/礼品卡抵扣

第一阶段继续保留并复用现有 wallet deduction 机制。

规则:

  • 创建 order 时锁钱包并创建 wallet deduction 记录。
  • 支付成功时 wallet deduction 确认消耗。
  • 用户取消 order 时,沿用现有取消退款/释放流程。
  • 同渠道重建 provider session 不重复创建 wallet deduction。
  • order 已 cancelled 且 wallet deduction 已退款后,如果旧 provider session late success,进入 conflicted/manual review。

11. Worktask 适配

11.1 支付类型

worktask 同一时刻只能存在以下三类支付动作中的一个:

stage_pay
full_pay
price_change

互斥判断以 worktask 为粒度,不以渠道为粒度。

不能同时存在:

  • 一个 stage_pay 的 paying order 和一个 price_change 的 paying order。
  • 一个 full_pay 的 paying order 和一个 stage_pay 的 paying order。
  • 一个 pending/wait_pay 的改价流程和一个 stage/full paying order。

11.2 创建支付时必须锁

lock work_tasks
lock active paying orders for work_task
lock active work_task_price_changes
validate no conflicting payment action
create order/provider session or price_change
commit

建议新增领域服务:

class WorkTaskPaymentGuard
{
    public function assertNoConflictingPaymentAction(WorkTask $workTask, string $newPayType): void;
}

复用入口:

  • stage payment 创建。
  • full payment 创建。
  • price change 创建。
  • artist price change 创建。
  • paying_order 查询后的继续支付前校验。

11.3 取消订单

worktask 的取消订单只取消当前支付订单:

Order -> cancelled
wallet deduction -> refund/release
provider session -> cancelled/expired/abandoned
worktask -> wait_pay 不变

如果用户要取消 worktask 本身,调用 worktask 专用取消接口。

12. Product 适配

product 有 product_license,更接近传统电商订单中心,但当前支付交互仍然是:

继续支付
取消订单

规则:

  • 创建 product 支付时锁库存。
  • 创建 product license。
  • 创建 order。
  • 创建 wallet deduction。
  • 创建 provider session。
  • 取消 unpaid product order 时取消 product license,并恢复库存。
  • 继续支付时只继续当前 active provider session,或安全地同渠道重建。

13. Project 适配

当前 project 流程:

买家发布 Project
  -> 画师创建 ProjectRequest
  -> 买家选择 ProjectRequest
  -> 系统创建 WorkTask + WorkTaskStage
  -> 画师 accept
  -> WorkTask 进入 wait_pay
  -> 买家支付 WorkTask

结论:

  • Project 是招募/撮合容器,不是支付对象。
  • ProjectRequest 是画师应征报价,不是支付对象。
  • 被选择后生成的 WorkTask 是支付对象。
  • 当前业务只选择一个 artist,不设计多个 artist 同时参与同一个 project 的模式。

chooseAndCreateWorkTask 需要保证幂等和数据库锁:

  • project_requests
  • projects
  • 检查 project request 状态仍可选择。
  • 检查同一个 project request 是否已经存在 active worktask。
  • 检查 project 是否已有 active chosen worktask。
  • 创建 worktask/stages/group 必须在同一事务中。

14. 对外接口和兼容策略

第一阶段尽量不要求前端切换到全新接口。

继续保留:

POST /api/user/pay/work_task/create_checkout_session
POST /api/work_tasks/paying_order
POST /api/orders/continue
POST /api/orders/cancel

POST /api/user/pay/product/create_checkout_session
POST /api/product_licenses/continue_payment
POST /api/product_licenses/cancel

语义说明:

  • /api/orders/continue: 继续当前 order 的 active provider session。
  • /api/orders/cancel: 取消当前支付订单;对 worktask 不取消 worktask,对 product 按 product license cancel 逻辑处理。
  • /api/work_tasks/paying_order: 查询当前 worktask 是否有 paying order。

不新增以下接口作为当前阶段目标:

POST /api/payments/intents
POST /api/payments/intents/{id}/continue
POST /api/payments/intents/{id}/switch_channel
POST /api/orders/switch_payment_channel

15. Service 设计草案

不新增持久化 PaymentIntent 层,但需要抽出支付网关服务,收敛当前分散逻辑。

建议新增:

app/Service/Payment/OrderPaymentService.php
app/Service/Payment/PaymentSessionResolver.php
app/Service/Payment/PaymentSessionContinueService.php
app/Service/Payment/PaymentSessionCancelService.php
app/Service/Payment/PaymentSessionExpireService.php
app/Service/Payment/PaymentConflictService.php
app/Service/Payment/WorkTaskPaymentGuard.php

15.1 OrderPaymentService

职责:

  • 创建 order 后创建对应 provider session。
  • 继续支付。
  • 取消 order。
  • 处理支付成功。
  • 处理 late success。
  • 统一返回现有前端兼容结构。

示例方法:

class OrderPaymentService
{
    public function continue(Order $order, User $user): array;

    public function cancel(Order $order, User $user): void;

    public function handleProviderSuccess(object $providerSession, array $payload): void;

    public function recreateSameChannelSession(Order $order, object $expiredSession): array;
}

15.2 PaymentSessionResolver

职责:

  • 找到 order 当前 active provider session。
  • 检测多个 active session。
  • 提供统一 session 状态判断。

示例方法:

class PaymentSessionResolver
{
    public function activeSession(Order $order): ?object;

    public function assertNoMultipleActiveSessions(Order $order): void;

    public function channelOf(object $session): string;
}

15.3 PaymentConflictService

职责:

  • 记录 conflicted session。
  • 记录原因、provider payload、order 状态。
  • 不自动退款。
  • 为后续人工处理提供查询依据。

16. Error Code 建议

继续使用业务 error code,不返回裸 409。

建议新增或保留:

21001 PaymentPaypalConflict
21002 PaymentProcessing
21003 WorkTaskPayingPaymentConflict
21004 PaymentSessionExpired
21005 PaymentSessionNotFound
21006 PaymentOrderNotPayable
21007 PaymentSessionConflict
21008 PaymentChannelNotAllowed
21009 PaymentAlreadySucceeded
21010 PaymentProviderUnavailable
21011 WalletDeductionConflict
21012 WorkTaskPaymentActionConflict
21013 PaymentConflictRequiresManualReview

语义:

  • PaymentProcessing: 支付渠道处理中,用户应等待,不允许取消或重建。
  • PaymentSessionConflict: 多个 active provider session 或 late success 冲突。
  • PaymentChannelNotAllowed: 当前币种或业务对象不支持该渠道。
  • PaymentAlreadySucceeded: 订单已支付,前端应刷新业务状态。
  • WorkTaskPaymentActionConflict: stage/full/price_change 互斥冲突。
  • PaymentConflictRequiresManualReview: 需要人工退款/人工处理。

17. 迁移实施计划

阶段 1:统一 continue

  • 新增 PaymentSessionResolver
  • orders/continue 改为通过 resolver 决定当前渠道。
  • 移除默认 fallback 到 Stripe。
  • 多 active session 返回 error code。

验收:

  • Alipay pending continue 返回 Alipay。
  • PayPal pending continue 返回 PayPal。
  • Stripe pending continue 返回 Stripe。
  • PayPal processing 返回 PaymentProcessing
  • 0 元订单返回 internal 并包含 out_trade_no

阶段 2:worktask 创建支付接入统一 guard

  • 新增 WorkTaskPaymentGuard
  • 创建 stage/full/price_change 支付前统一检查互斥。
  • 创建支付时锁 worktask、active paying orders、active price changes。
  • /api/work_tasks/paying_order 基于 paying order 和 active session 返回。

验收:

  • stage/full/price_change 同一时刻只能存在一个支付中对象。
  • buyer/artist 查询 paying_order 行为正确。
  • no paying_order 时返回空,不重新计算。

阶段 3:取消订单语义收敛

  • /api/orders/cancel 保持为取消当前支付订单。
  • 对 worktask 不取消 worktask。
  • 对 product 取消 product license 并恢复库存。
  • PayPal processing 时不允许取消。
  • PayPal succeeded 时不允许取消。

验收:

  • worktask order cancel 后 worktask 仍 wait_pay
  • wallet deduction 退款/释放。
  • PayPal processing 取消返回 PaymentProcessing

阶段 4:product continue/cancel 统一

  • ProductLicenseService::continuePayment 改为复用统一 continue 服务。
  • product cancel 复用统一 cancel order 逻辑,并保留 license/库存处理。

验收:

  • product Stripe/Alipay/PayPal 行为与 worktask 一致。
  • 库存不会因 continue 或同渠道重建重复扣减。

阶段 5:webhook/callback 统一成功处理

  • Stripe/Alipay/PayPal 成功回调先定位 provider session。
  • 成功处理统一锁 provider session 和 order。
  • order paying 时完成支付。
  • order cancelled/paid/金额不匹配时进入 conflicted/manual review。
  • 不自动退款。

验收:

  • 重复 webhook 幂等。
  • PayPal late success after cancelled order 进入 conflicted。
  • Alipay late success after cancelled order 进入 conflicted。
  • 已 paid order 收到其他 session 成功进入 conflicted。

阶段 6:过期任务改造

  • ExpirePaymentSessions 不再因为 session 过期取消 order。
  • Stripe 只同步/expire session。
  • Alipay 到期只标记 session expired。
  • PayPal 只 reconcile,不靠本地 expires_at 判死。

验收:

  • 支付 session 过期不会自动取消业务订单。
  • continue 可按渠道策略返回原链接、重建或错误码。

阶段 7:project 选择流程加固

  • chooseAndCreateWorkTask 增加数据库锁。
  • 同一个 project 只能选择一个 artist。
  • 同一个 project request 不能重复创建 active worktask。

验收:

  • 重复点击选择不会创建多个 worktask。
  • 并发选择不会破坏 project/project_request 状态。

18. 验收标准

18.1 支付正确性

  • 原渠道 pending 时,继续支付必须返回原渠道。
  • 没有任何路径会把 Alipay/PayPal 静默 fallback 到 Stripe。
  • 同一业务对象同一时刻只有一个 paying order。
  • 同一 paying order 同一时刻只有一个 active provider session。
  • 多 active session 必须返回明确 error code。
  • 支付成功后 order 只能被标记 paid 一次。
  • 重复 webhook 不会重复履约。

18.2 用户体验

  • 用户点击继续支付时,如果当前 session 可继续,直接返回支付入口。
  • Stripe/Alipay 过期后,如安全可重建,则自动同渠道重建。
  • PayPal processing 时返回明确处理中状态。
  • 用户取消订单后,可以重新选择渠道和钱包发起新支付。
  • worktask 取消订单不会取消 worktask。

18.3 业务适配

  • worktask stage/full/price_change 互斥保持有效。
  • project request 创建的 worktask 可直接复用 worktask 支付能力。
  • 一个 project 只能选择一个 artist。
  • product 购买和 worktask 支付共享同一套 continue/cancel/session 处理。
  • product 库存、license、wallet deduction 生命周期正确。

18.4 风险兜底

  • expired/cancelled session late success 能定位到原 session。
  • late success 如 order 仍 paying,且金额/币种匹配,可完成 order。
  • late success 如 order 已 cancelled,进入 conflicted/manual review。
  • late success 如 order 已 paid,进入 conflicted/manual review。
  • 支付 session 过期不会自动取消业务订单。
  • 不做自动退款。

19. 测试清单

19.1 Worktask

  • 创建 Stripe/Alipay/PayPal/internal 支付。
  • continue 返回当前渠道。
  • cancel order 后 worktask 保持 wait_pay
  • stage/full/price_change 并发互斥。
  • 有 active price_change 支付时,stage/full 创建失败。
  • 有 active stage/full 支付时,price_change 创建失败。
  • paying_order 无支付中订单返回空。
  • paying_order 多 active session 返回 error code。

19.2 Product

  • 创建 Stripe/Alipay/PayPal/internal 支付。
  • continue 返回当前渠道。
  • cancel order 取消 license 并恢复库存。
  • product continue 不切渠道。
  • product session 过期同渠道重建。

19.3 Provider

  • Stripe session open continue。
  • Stripe expired 同渠道重建。
  • Alipay 未过期 continue 原 pay_url。
  • Alipay 过期新 session + 新 out_trade_no。
  • Alipay query 404 但未过期不判死。
  • PayPal pending continue 原 pay_url。
  • PayPal processing 返回 error code。
  • PayPal processing 不允许 cancel order。
  • PayPal late success after cancelled order 进入 conflicted。

19.4 Project

  • 单 project 只能选择一个 artist。
  • 同 project request 不能重复创建 worktask。
  • 并发选择不会重复创建 worktask。

20. 监控和运营

建议新增日志和指标:

  • paying orders 数量。
  • active provider sessions 数量。
  • provider session expired/cancelled/succeeded 数量。
  • late success 数量。
  • conflicted 数量。
  • 人工退款/人工处理数量。
  • webhook 幂等命中次数。
  • provider query 失败次数。
  • continue 返回各渠道分布。

建议增加后台或内部查询能力:

  • 按 order_id 查询 provider sessions。
  • 按 provider trade no 查询 order/session。
  • 查询 conflicted sessions。
  • 手动触发 reconcile。
  • 手动标记人工处理完成。

21. 已确认业务决策

  • 当前阶段不新增独立 payment_intents 表。
  • 当前阶段不新增独立 payment_attempts 表。
  • Order 作为轻量支付意图。
  • Stripe/Alipay/PayPal checkout session 作为支付尝试。
  • 当前阶段不做切换支付渠道。
  • 用户点击“取消订单”取消当前支付订单;worktask 保持 wait_pay
  • Worktask 在 wait_pay 状态下通过专门的 worktask 取消接口取消。
  • Product unpaid order 取消时取消 product license,并恢复库存。
  • Project 当前只选择一个 artist,不设计多个 artist 同时参与同一个 project 的模式。
  • Alipay 到期重建时创建新的 session 和新的 out_trade_no;不复用旧的 Alipay out_trade_no,也不因为 session 过期创建新的业务 Order
  • 第一阶段继续保留现有 wallet deduction 机制。
  • Conflicted 场景第一阶段不做自动退款,只进入人工退款/人工处理流程。

22. 未来扩展条件

只有当后续需要以下能力时,再考虑引入独立 payment_intents/payment_attempts 表:

  • 保留同一个 order 并切换支付渠道。
  • wallet deduction 跨多个 attempt 保留。
  • 一个 order 下展示多个支付尝试历史。
  • 更复杂的统一支付运维后台。
  • 部分支付、混合支付、拆单支付。
  • 自动退款/自动冲正。
  • 跨渠道统一 reconciliation。