支付网关当前未提交改动 Code Review

日期:2026-06-12

本文档基于当前 pipipen-api 工作区所有未提交改动整理,用于对照 docs/payment-gateway-redesign-prd.mddocs/payment-gateway-redesign-implementation-plan.md 验收本轮支付网关改造。

1. Review 结论

当前改动整体符合既定目标:在不新增独立 payment_intents / payment_attempts 持久化表的前提下,继续沿用 orders + provider checkout sessions 两层模型,并把 Order 作为轻量支付意图、Stripe/Alipay/PayPal session 作为渠道支付尝试。

已达成的核心目标:

  • 取消“支付 session 过期自动取消业务订单”的机制。
  • continue 不再静默 fallback 到 Stripe,只能继续当前 active session,或在安全条件下同渠道重建。
  • Worktask 和 product 的继续支付、取消订单、过期重建、冲突保护逻辑趋于统一。
  • Worktask 的 stage pay、full pay、price change 互斥约束已增强。
  • PayPal cancel callback 不再直接取消本地 order,避免用户从 PayPal 返回取消页导致本地订单被取消。
  • Alipay / Stripe late success 在本地订单已取消、金额不匹配或订单缺失时进入 conflicted,不自动履约、不自动退款。
  • paying_order 不存在时返回空,不再重复计算;存在时返回前端展示所需的 order/payment/pre_calc 数据。
  • Artist 可以查询自己参与的 worktask paying order。
  • Project 选择 project request 创建 worktask 时增加锁和 active worktask 防重。

当前未发现必须阻断提交的高优先级问题。剩余风险主要是支付平台天然限制和运营处理链路:PayPal/Alipay 已打开页面无法可靠物理关闭,late success 进入 conflicted 后需要人工审核流程承接。

2. 当前未提交改动范围

新增文件

文件作用
app/Service/Payment/PaymentSessionResolver.php统一识别 active/recoverable/blocking provider session,检测多个 active session,统一 cancel 前置保护。
app/Service/Payment/PaymentConflictService.php统一标记 Stripe/Alipay conflicted,保存 provider payload、错误原因和冲突时间。
app/Service/Payment/PaypalSessionRecoveryService.phpPayPal continue/expire 场景下同步远端状态;404 视为远端订单缺失并标记 failed,其它不可用返回业务错误。
database/migrations/2026_06_12_000001_add_conflict_fields_to_alipay_and_stripe_checkout_sessions.php给 Stripe/Alipay session 增加 conflicted 状态和冲突记录字段。
tests/Feature/ProjectRequestChooseWorkTaskTest.php覆盖 project request 选择后创建 worktask 的防重复逻辑。

修改文件

文件主要变更
app/Console/Commands/ExpirePaymentSessions.php过期任务只标记 provider session,不再取消 order;PayPal 通过 recovery service 同步。
app/Enums/AlipayCheckoutSessionStatus.php新增 CONFLICTED
app/Enums/StripeCheckoutSessionStatus.php新增 CONFLICTED
app/Enums/ErrorCode.php新增支付相关 error code:session expired/not found/not payable/channel not allowed/manual review 等。
app/Models/AlipayCheckoutSession.php增加 raw_responseconflicted_at cast。
app/Models/StripeCheckoutSession.php增加 raw_responseconflicted_at cast。
app/Http/Controllers/Api/User/OrderController.php/api/orders/continue 接入统一 session resolver;支持同渠道重建;禁止渠道错乱;冲突态返回 error code。
app/Http/Controllers/Api/User/WorkTaskPayController.phpPayPal cancel callback 不再取消本地 order。
app/Http/Controllers/Api/User/ProjectRequestController.php选择 project request 创建 worktask 时加锁并防重复 active worktask。
app/Listeners/StripeEventListener.phpStripe session expired webhook 只标记 session expired,不再取消 order。
app/Service/OrderService.php取消订单内部重新锁 order,阻止 processing/finished/conflicted session 被取消;取消后只处理 pending session。
app/Service/Payment/WorkTaskPayingOrderService.phppaying_order 将 conflicted session 当作 active payment 返回;Alipay payment 返回 out_trade_no
app/Service/Payment/WorkTaskPaymentService.phpWorktask Alipay 创建支付时 pay_data 返回 out_trade_no
app/Service/Payment/WorkTaskPaymentValidator.php将部分 400 abort 改为明确 error code。
app/Service/Product/ProductLicenseService.phpProduct continue/cancel 接入统一 session 行为,支持同渠道重建、PayPal recovery、conflicted 保护。
app/Service/Stripe/StripeCheckoutSessionService.php新增基于旧 session 参数同渠道重建 Stripe checkout session。
app/Service/Stripe/StripeWebhookService.phpStripe completed webhook 增加 session/order 锁、金额/币种校验、late success conflicted 处理。
app/Service/Alipay/AlipayWebhookService.phpAlipay webhook 改为按 Alipay session 的 out_trade_no 定位 order,支持重建后新 out_trade_no
tests/Feature/OrderContinueAndCancelTest.php覆盖 worktask paying_order、continue/cancel、Alipay/Stripe late success、过期任务、PayPal recovery 等主路径。
tests/Feature/PaypalCancelCallbackTest.php覆盖 PayPal cancel callback 不取消本地 order、不释放钱包/库存。
tests/Unit/Service/Product/ProductLicenseServiceTest.php覆盖 product continue/cancel 多渠道、过期重建、冲突态保护。

3. 设计目标对照

PRD/计划目标当前实现情况结论
不新增独立 payment intent 表沿用 orders + provider sessions,新增的是 resolver/service,不新增核心支付表。已达成
同一业务对象同一时刻只能有一个 paying orderWorktask 创建支付、改价、paying_order 查询均检测冲突;project 创建 worktask 也加防重。基本达成
同一 paying order 同一时刻只能有一个 active provider sessionPaymentSessionResolver::assertSingleActiveSession()WorkTaskPayingOrderService 检测多个 active session 并返回 error code。已达成
continue 不允许渠道错乱/api/orders/continue 和 product continue 都根据 resolver 返回的 session channel 分发,不再默认创建 Stripe。已达成
过期 session 不自动取消 orderartisan command 和 Stripe expired listener 都改为只处理 session;order 继续保持 paying。已达成
支持同渠道重建Stripe/Alipay expired 后同渠道重建;PayPal 仅在远端 404/failed/cancelled 等安全条件下重建。已达成
PayPal cancel callback 不取消 ordercallback 只同步/跳转/返回 202,不再调用 OrderService::cancelOrder()已达成
Late success 不自动退款Stripe/Alipay/PayPal 成功但本地状态异常进入 conflicted/manual review。已达成
Worktask cancel order 不取消 worktaskcancel 仍只取消 order;测试覆盖 worktask 保持 wait_pay。已达成
Product cancel 恢复库存和钱包product cancel 仍委托 OrderService::cancelOrder(),保留 license/库存/钱包回滚。已达成
no paying_order 返回空,不重新计算WorkTaskPayingOrderService 无 paying order 时返回 pre_calc: null已达成
Artist 可查询 paying_order/api/work_tasks/paying_order 支持 buyer 或对应 artist 查询。已达成
返回 error code 而非裸 409当前支付冲突路径使用 ValidationExceptionErrorCode;仍有历史非本轮范围 abort,但支付主路径已收敛。基本达成

4. 核心流程变化

4.1 Continue 流程

重点:

  • Alipay continue 只返回 Alipay session 或创建新的 Alipay session。
  • Stripe continue 只返回 Stripe session 或创建新的 Stripe session。
  • PayPal continue 会先同步远端订单;只有远端明确 404/failed/cancelled 等不可继续时才新建 PayPal session。
  • 没有 active/recoverable session 时返回 BusinessLogicError: No payment session found,不默认创建 Stripe。

4.2 Cancel Order 流程

重点:

  • conflicted 优先于 processing 暴露,避免人工处理态被用户取消掉。
  • Stripe pending 取消时调用 Stripe session cancellation。
  • Alipay pending 取消时尝试 close,然后本地标记 expired;close 失败不阻断取消,后续 late success 进入 conflicted。
  • PayPal pending 取消时只标记本地 cancelled;不能承诺关闭用户已打开的 PayPal 页面。

4.3 Late Success 流程

重点:

  • Stripe/Alipay 增加 conflicted,并保存 raw_responseerror_messageconflicted_at
  • Alipay webhook 改为按 alipay_checkout_sessions.out_trade_no 找 session,再锁原 order,解决同一 order 重建 Alipay session 后 out_trade_noorders.out_trade_no 不一致的问题。
  • Stripe completed webhook 不再只处理 pending session;非 finished/conflicted 的 session 都会被锁定后判断。

5. 关键改动说明

5.1 数据模型和状态

新增 migration 修改 Stripe/Alipay session enum:

  • pending
  • processing
  • finished
  • expired
  • conflicted

并增加:

  • raw_response: 保存 provider 原始响应,方便人工排查。
  • error_message: 保存冲突原因。
  • conflicted_at: 保存进入冲突态的时间。

这样 PayPal 已有的 conflicted 思路被扩展到 Stripe/Alipay,三类渠道对 late success 的业务语义保持一致。

5.2PaymentSessionResolver

这是本轮改造里最关键的后端收敛点。

它将 provider session 分为三类:

  • active:pendingprocessingconflicted
  • recoverable:Stripe/Alipay expired,PayPal expired/cancelled/failed
  • blocking cancel:processingfinishedconflicted

设计理由:

  • pending 表示用户可能还在第三方页面操作,continue 可以返回当前渠道。
  • processing 表示渠道正在处理,不允许取消、不允许重建。
  • conflicted 表示资金和本地业务状态已不一致,不允许继续支付或取消,需要人工处理。
  • 多个 active session 表示历史或并发异常,不自动选择,避免错用支付渠道。

5.3/api/orders/continue

本轮修复了此前 Alipay continue 命中 Stripe 的根因:旧逻辑在找不到特定渠道 session 后会继续走创建 Stripe 的兜底路径。

现在逻辑变为:

  1. 锁定 order。
  2. 0 元订单返回 internal,并带 out_trade_no
  3. 通过 resolver 查找 active session。
  4. 只按 active session 的渠道继续。
  5. active session 过期时,先按渠道同步/校验,再同渠道重建。
  6. 没有任何可继续 session 时返回 error code,不创建默认 Stripe。

5.4 Product continue/cancel

ProductLicenseService 原先自己维护 Stripe/Alipay/PayPal continue 和取消检查,逻辑与 worktask 不一致。

现在 product 侧也接入:

  • PaymentSessionResolver
  • PaypalSessionRecoveryService
  • StripeCheckoutSessionService::recreateFromPreviousSession()
  • 同渠道 Alipay session 重建
  • cancel 时统一委托 OrderService::cancelOrder()

保留 product 特有业务:

  • product license 权限和状态校验。
  • product order 取消时取消 license。
  • 库存恢复和钱包退款仍走现有 OrderService

5.5 Worktask paying_order

/api/work_tasks/paying_order 的目的被收敛为“查询是否存在支付中的 order”:

  • 无 paying order 返回:
    • has_paying_order: false
    • paying_order: null
    • payment: null
    • pre_calc: null
  • 有 paying order 返回基于 order 已保存的 amount_calc_process 构造的 pre_calc,不重新计算。
  • 多个 paying order 返回 error code。
  • 多个 active provider session 返回 error code。
  • conflicted session 仍作为 active payment 返回,避免前端误判为没有支付中订单。

5.6 PayPal cancel callback

旧行为:用户从 PayPal 页面取消返回后,后端可能直接取消本地 order,导致用户仍在其它打开页面完成 PayPal 支付时形成冲突。

新行为:

  • callback 不直接取消 order。
  • 如果远端已 completed/conflicted/processing,按同步结果处理。
  • 如果只是用户关闭或取消 PayPal 页面,返回 cancel callback URL 或 202 Payment cancellation processing
  • 用户真正想取消业务支付订单时,仍调用 /api/orders/cancel

这符合当前产品交互:页面按钮叫“取消订单”,而 PayPal cancel callback 只是第三方页面返回事件,不应等同于取消本地订单。

5.7 ExpirePaymentSessions

过期任务现在只处理 provider session:

  • Alipay pending 且本地 expires_at 到期:标记 expired。
  • Stripe pending 且本地 expires_at 到期:调用 Stripe expire,再本地标记 expired。
  • PayPal pending 且 expires_at 到期:同步远端状态;404 标记 failed,其它异常记录日志。

不再因为 session 过期自动取消 order,避免“用户支付成功但本地订单已取消”的主要冲突来源。

5.8 Project request 创建 worktask

虽然 project 本身不是支付对象,但它会生成 worktask,因此本轮也补了并发一致性:

  • 锁定 project_requests
  • 锁定对应 projects
  • 检查同一个 project request 是否已有 active worktask。
  • 检查同一个 project 是否已有 active worktask。
  • group 创建放在事务内,GroupCreated 事件在事务提交后 dispatch。

目标是保证一个 project 当前业务规则下只选择一个 artist,并只生成一个 active worktask。

6. 测试覆盖

已执行:

vendor\bin\phpunit tests\Feature\OrderContinueAndCancelTest.php tests\Feature\PaypalCancelCallbackTest.php tests\Feature\PaypalWebhookReliabilityTest.php tests\Feature\PaypalReturnCallbackTest.php tests\Unit\Service\Product\ProductLicenseServiceTest.php tests\Feature\ProjectRequestChooseWorkTaskTest.php

结果:

OK (95 tests, 599 assertions)

已执行语法和 diff 检查:

php -l <changed php files>
git diff --check

结果:

  • PHP 语法检查通过。
  • git diff --check 无空白错误。
  • 仅有 tests/Feature/OrderContinueAndCancelTest.php 的 CRLF/LF 提示。

未执行全量 composer test 的原因:

  • 当前 PHP 扩展列表中有 mbstring,但仍缺少 fileinforedis
  • 本轮已执行支付相关的定向 feature/unit 回归。

主要新增/更新测试覆盖:

  • Worktask PayPal 创建支付保存 return/cancel callback URL。
  • Worktask paying_order 返回既有 order、payment、pre_calc。
  • Artist 可查询自己 worktask 的 paying_order。
  • 无 paying_order 时返回空且不重新计算。
  • 多 paying order / 多 active provider session 返回 error code。
  • Stage/full/price_change 支付互斥。
  • Worktask Alipay 创建和 continue 返回 out_trade_no
  • Cancel order 不取消 worktask。
  • 0 元订单 continue 返回 internal 和 out_trade_no
  • Alipay continue 不 fallback 到 Stripe。
  • Alipay expired 后同渠道重建新 session 和新 out_trade_no
  • Alipay late success after cancelled order 标记 conflicted。
  • Alipay 新 out_trade_no webhook 能处理原 order。
  • Stripe late success after cancelled order 标记 conflicted。
  • Stripe expired 后同渠道重建。
  • ExpirePaymentSessions 不取消 order。
  • PayPal expired/pending/recoverable/404/unavailable/approved capture 场景。
  • PayPal processing 不允许 continue/cancel。
  • Stripe/Alipay/PayPal conflicted 不允许继续支付。
  • Stripe conflicted 不允许取消订单。
  • Product continue 支持 Stripe/Alipay/PayPal/internal。
  • Product expired Stripe/Alipay/PayPal 同渠道恢复。
  • Product 多 active session 返回 error code。
  • Product conflicted session 阻止 continue/cancel。
  • Product cancel 恢复 order/license。
  • Project request 选择 worktask 防重复。

7. 是否达到既定目标

已达到

  1. 防止原 Alipay 继续支付返回 Stripe 的渠道错乱问题。
  2. 防止 provider session 过期自动取消 order。
  3. 防止 PayPal cancel callback 误取消本地 order。
  4. 支持过期后同渠道重建,同时不重复创建业务 order。
  5. 对 PayPal/Alipay/Stripe late success 建立冲突态兜底。
  6. Worktask 三类支付动作互斥。
  7. Product 和 worktask 的 continue/cancel 语义基本一致。
  8. 使用数据库锁和 cache lock 降低并发重复创建/取消/支付完成的风险。
  9. 前端关键返回字段保持兼容,包括 pay_data 内部和顶层展开字段。

部分达到或仍是技术债

  1. 支付网关逻辑已经抽出 resolver/recovery/conflict service,但 OrderControllerProductLicenseService 仍各自有 continue 编排代码。短期可接受,后续若继续演进可抽成更完整的 PaymentGatewayService
  2. Error code 已覆盖本轮支付主路径,但仓库仍有不少历史 abort(400),不属于本次所有业务范围。
  3. Manual review 只是通过 conflicted 状态和错误码阻止用户继续操作,还没有后台处理页面或运营 runbook。
  4. PayPal/Alipay 无法物理关闭已打开页面的问题只能业务兜底,不能彻底消除。

8. 风险评估

8.1 高优先级风险

当前未发现必须阻断合并的高优先级实现风险。

8.2 中优先级风险

  1. 数据库 enum migration 可能锁表

    • 本次 migration 使用 ALTER TABLE ... MODIFY COLUMN ENUM(...)
    • 如果线上表数据量较大,MySQL 可能产生 metadata lock 或较长 DDL 时间。
    • 建议上线前评估表大小,并在低峰窗口执行。
  2. 回滚 migration 前必须处理 conflicted 数据

    • down() 会把 enum 恢复到不包含 conflicted
    • 如果线上已经有 conflicted 行,直接回滚可能失败。
    • 建议回滚方案明确:先把 conflicted 数据迁移到可兼容状态或禁止直接回滚。
  3. Manual review 流程缺失

    • 当前系统会保存冲突原因和 provider payload,但没有后台自动处理流程。
    • 运营需要明确如何检查 raw_response、如何手动确认履约或退款。
  4. Alipay close 失败被忽略

    • 用户取消 order 时会尝试 close Alipay session,但 close 失败不阻断取消,并本地标记 expired。
    • 如果用户仍在已打开的 Alipay 页面支付成功,会进入 conflicted。
    • 这是符合当前“不自动退款、人工处理”的设计,但可能增加运营工单。
  5. PayPal 远端状态不可用时用户无法继续

    • PayPal expired/pending 的 continue 需要同步远端状态。
    • 如果 PayPal API 短暂异常,当前返回 PayPal checkout session status is unavailable,不自动创建新 session。
    • 这是为了资金安全,但用户体验会受短时 provider 故障影响。

8.3 低优先级风险

  1. ExpirePaymentSessions 统计数量可能是候选数

    • 命令中的计数基于查询出的候选 session 数量,不一定等于最终成功更新数量。
    • 不影响业务一致性,但如果需要运营监控,应进一步优化统计口径。
  2. Product 和 order continue 编排仍有重复

    • 逻辑已经比之前一致,但还不是完全单一入口。
    • 后续可继续抽象,降低长期维护成本。
  3. Project active worktask 状态集合包含硬编码状态

    • activeProjectWorkTaskStatuses() 中包含 'artist_accepted'
    • 如果后续 worktask 状态枚举调整,需要同步这里。
  4. CRLF/LF 提示

    • tests/Feature/OrderContinueAndCancelTest.php 有换行符提示。
    • 不影响测试,但提交时 Git 可能统一换行。

9. 建议的上线前检查

  1. 在 staging 执行 migration,确认 Stripe/Alipay session 表 DDL 时间。
  2. 手动验证前端以下流程:
    • Worktask Alipay 创建支付、关闭页面、继续支付仍返回 Alipay。
    • Worktask PayPal 打开支付页后点 PayPal cancel 返回,不取消本地 order。
    • Worktask 点击取消订单后仍不取消 worktask。
    • Product license continue 支持 Stripe/Alipay/PayPal。
    • 0 元 order continue 返回 internalout_trade_no
  3. 验证所有支付渠道前端都读取 pay_data 内字段,不依赖旧的单一顶层字段;当前后端仍保留顶层展开以兼容。
  4. 准备人工处理 conflicted 的 SQL/后台流程:
    • 查询 paypal_checkout_sessions.status = conflicted
    • 查询 stripe_checkout_sessions.status = conflicted
    • 查询 alipay_checkout_sessions.status = conflicted
    • 核对 order_idraw_responseerror_messageconflicted_at
  5. 确认线上 PHP 扩展满足全量测试和运行需要,尤其是 fileinforedis

10. 建议的后续改进

  1. 抽出更完整的 PaymentGatewayService,进一步减少 OrderControllerProductLicenseService 的重复 continue 编排。
  2. 增加后台 conflicted session 列表和处理动作:
    • 标记已人工退款。
    • 标记已人工履约。
    • 记录处理人、处理时间和备注。
  3. 增加 provider session 审计日志,记录每一次状态变化来源:
    • user cancel
    • provider webhook
    • expire command
    • continue recovery
  4. ExpirePaymentSessions 增加更准确的成功/跳过/失败计数。
  5. 长期如果要支持“保留 order 切换支付渠道”,再引入更完整的 payment intent / attempt 模型;当前阶段不建议提前引入。

11. 最终评价

本轮改动解决的是支付网关最核心的一致性问题:第三方支付页面不可控时,本地系统不能再依赖“自动取消 order”来优化交互,而应该把 order 与 provider session 的状态边界区分清楚。

当前实现选择了较稳妥的策略:

  • 用户体验上,继续支付仍尽量少点击,过期后可同渠道重建。
  • 资金安全上,不自动换渠道、不自动退款、不自动吞掉 late success。
  • 数据一致性上,关键路径加锁,冲突态显式记录并阻止用户继续操作。

从当前测试结果和代码复核看,已经达到本阶段 PRD/plan 的主要目标,可以进入人工联调和 staging 验证阶段。