日期:2026-06-12
本文档基于当前 pipipen-api 工作区所有未提交改动整理,用于对照 docs/payment-gateway-redesign-prd.md 与 docs/payment-gateway-redesign-implementation-plan.md 验收本轮支付网关改造。
当前改动整体符合既定目标:在不新增独立 payment_intents / payment_attempts 持久化表的前提下,继续沿用 orders + provider checkout sessions 两层模型,并把 Order 作为轻量支付意图、Stripe/Alipay/PayPal session 作为渠道支付尝试。
已达成的核心目标:
continue 不再静默 fallback 到 Stripe,只能继续当前 active session,或在安全条件下同渠道重建。conflicted,不自动履约、不自动退款。paying_order 不存在时返回空,不再重复计算;存在时返回前端展示所需的 order/payment/pre_calc 数据。当前未发现必须阻断提交的高优先级问题。剩余风险主要是支付平台天然限制和运营处理链路:PayPal/Alipay 已打开页面无法可靠物理关闭,late success 进入 conflicted 后需要人工审核流程承接。
| 文件 | 作用 |
|---|---|
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.php | PayPal 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_response、conflicted_at cast。 |
app/Models/StripeCheckoutSession.php | 增加 raw_response、conflicted_at cast。 |
app/Http/Controllers/Api/User/OrderController.php | /api/orders/continue 接入统一 session resolver;支持同渠道重建;禁止渠道错乱;冲突态返回 error code。 |
app/Http/Controllers/Api/User/WorkTaskPayController.php | PayPal cancel callback 不再取消本地 order。 |
app/Http/Controllers/Api/User/ProjectRequestController.php | 选择 project request 创建 worktask 时加锁并防重复 active worktask。 |
app/Listeners/StripeEventListener.php | Stripe session expired webhook 只标记 session expired,不再取消 order。 |
app/Service/OrderService.php | 取消订单内部重新锁 order,阻止 processing/finished/conflicted session 被取消;取消后只处理 pending session。 |
app/Service/Payment/WorkTaskPayingOrderService.php | paying_order 将 conflicted session 当作 active payment 返回;Alipay payment 返回 out_trade_no。 |
app/Service/Payment/WorkTaskPaymentService.php | Worktask Alipay 创建支付时 pay_data 返回 out_trade_no。 |
app/Service/Payment/WorkTaskPaymentValidator.php | 将部分 400 abort 改为明确 error code。 |
app/Service/Product/ProductLicenseService.php | Product continue/cancel 接入统一 session 行为,支持同渠道重建、PayPal recovery、conflicted 保护。 |
app/Service/Stripe/StripeCheckoutSessionService.php | 新增基于旧 session 参数同渠道重建 Stripe checkout session。 |
app/Service/Stripe/StripeWebhookService.php | Stripe completed webhook 增加 session/order 锁、金额/币种校验、late success conflicted 处理。 |
app/Service/Alipay/AlipayWebhookService.php | Alipay 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 多渠道、过期重建、冲突态保护。 |
| PRD/计划目标 | 当前实现情况 | 结论 |
|---|---|---|
| 不新增独立 payment intent 表 | 沿用 orders + provider sessions,新增的是 resolver/service,不新增核心支付表。 | 已达成 |
| 同一业务对象同一时刻只能有一个 paying order | Worktask 创建支付、改价、paying_order 查询均检测冲突;project 创建 worktask 也加防重。 | 基本达成 |
| 同一 paying order 同一时刻只能有一个 active provider session | PaymentSessionResolver::assertSingleActiveSession() 和 WorkTaskPayingOrderService 检测多个 active session 并返回 error code。 | 已达成 |
continue 不允许渠道错乱 | /api/orders/continue 和 product continue 都根据 resolver 返回的 session channel 分发,不再默认创建 Stripe。 | 已达成 |
| 过期 session 不自动取消 order | artisan command 和 Stripe expired listener 都改为只处理 session;order 继续保持 paying。 | 已达成 |
| 支持同渠道重建 | Stripe/Alipay expired 后同渠道重建;PayPal 仅在远端 404/failed/cancelled 等安全条件下重建。 | 已达成 |
| PayPal cancel callback 不取消 order | callback 只同步/跳转/返回 202,不再调用 OrderService::cancelOrder()。 | 已达成 |
| Late success 不自动退款 | Stripe/Alipay/PayPal 成功但本地状态异常进入 conflicted/manual review。 | 已达成 |
| Worktask cancel order 不取消 worktask | cancel 仍只取消 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 | 当前支付冲突路径使用 ValidationException 和 ErrorCode;仍有历史非本轮范围 abort,但支付主路径已收敛。 | 基本达成 |
重点:
BusinessLogicError: No payment session found,不默认创建 Stripe。重点:
conflicted 优先于 processing 暴露,避免人工处理态被用户取消掉。重点:
conflicted,并保存 raw_response、error_message、conflicted_at。alipay_checkout_sessions.out_trade_no 找 session,再锁原 order,解决同一 order 重建 Alipay session 后 out_trade_no 与 orders.out_trade_no 不一致的问题。新增 migration 修改 Stripe/Alipay session enum:
pendingprocessingfinishedexpiredconflicted并增加:
raw_response: 保存 provider 原始响应,方便人工排查。error_message: 保存冲突原因。conflicted_at: 保存进入冲突态的时间。这样 PayPal 已有的 conflicted 思路被扩展到 Stripe/Alipay,三类渠道对 late success 的业务语义保持一致。
PaymentSessionResolver这是本轮改造里最关键的后端收敛点。
它将 provider session 分为三类:
pending、processing、conflicted。expired,PayPal expired/cancelled/failed。processing、finished、conflicted。设计理由:
pending 表示用户可能还在第三方页面操作,continue 可以返回当前渠道。processing 表示渠道正在处理,不允许取消、不允许重建。conflicted 表示资金和本地业务状态已不一致,不允许继续支付或取消,需要人工处理。/api/orders/continue本轮修复了此前 Alipay continue 命中 Stripe 的根因:旧逻辑在找不到特定渠道 session 后会继续走创建 Stripe 的兜底路径。
现在逻辑变为:
internal,并带 out_trade_no。ProductLicenseService 原先自己维护 Stripe/Alipay/PayPal continue 和取消检查,逻辑与 worktask 不一致。
现在 product 侧也接入:
PaymentSessionResolverPaypalSessionRecoveryServiceStripeCheckoutSessionService::recreateFromPreviousSession()OrderService::cancelOrder()保留 product 特有业务:
OrderService。/api/work_tasks/paying_order 的目的被收敛为“查询是否存在支付中的 order”:
has_paying_order: falsepaying_order: nullpayment: nullpre_calc: nullamount_calc_process 构造的 pre_calc,不重新计算。conflicted session 仍作为 active payment 返回,避免前端误判为没有支付中订单。旧行为:用户从 PayPal 页面取消返回后,后端可能直接取消本地 order,导致用户仍在其它打开页面完成 PayPal 支付时形成冲突。
新行为:
202 Payment cancellation processing。/api/orders/cancel。这符合当前产品交互:页面按钮叫“取消订单”,而 PayPal cancel callback 只是第三方页面返回事件,不应等同于取消本地订单。
过期任务现在只处理 provider session:
expires_at 到期:标记 expired。expires_at 到期:调用 Stripe expire,再本地标记 expired。expires_at 到期:同步远端状态;404 标记 failed,其它异常记录日志。不再因为 session 过期自动取消 order,避免“用户支付成功但本地订单已取消”的主要冲突来源。
虽然 project 本身不是支付对象,但它会生成 worktask,因此本轮也补了并发一致性:
project_requests。projects。GroupCreated 事件在事务提交后 dispatch。目标是保证一个 project 当前业务规则下只选择一个 artist,并只生成一个 active worktask。
已执行:
结果:
已执行语法和 diff 检查:
结果:
git diff --check 无空白错误。tests/Feature/OrderContinueAndCancelTest.php 的 CRLF/LF 提示。未执行全量 composer test 的原因:
mbstring,但仍缺少 fileinfo 和 redis。主要新增/更新测试覆盖:
out_trade_no。out_trade_no。out_trade_no。out_trade_no webhook 能处理原 order。pay_data 内部和顶层展开字段。OrderController 和 ProductLicenseService 仍各自有 continue 编排代码。短期可接受,后续若继续演进可抽成更完整的 PaymentGatewayService。abort(400),不属于本次所有业务范围。conflicted 状态和错误码阻止用户继续操作,还没有后台处理页面或运营 runbook。当前未发现必须阻断合并的高优先级实现风险。
数据库 enum migration 可能锁表
ALTER TABLE ... MODIFY COLUMN ENUM(...)。回滚 migration 前必须处理 conflicted 数据
down() 会把 enum 恢复到不包含 conflicted。conflicted 行,直接回滚可能失败。Manual review 流程缺失
raw_response、如何手动确认履约或退款。Alipay close 失败被忽略
PayPal 远端状态不可用时用户无法继续
PayPal checkout session status is unavailable,不自动创建新 session。ExpirePaymentSessions 统计数量可能是候选数
Product 和 order continue 编排仍有重复
Project active worktask 状态集合包含硬编码状态
activeProjectWorkTaskStatuses() 中包含 'artist_accepted'。CRLF/LF 提示
tests/Feature/OrderContinueAndCancelTest.php 有换行符提示。internal 和 out_trade_no。pay_data 内字段,不依赖旧的单一顶层字段;当前后端仍保留顶层展开以兼容。paypal_checkout_sessions.status = conflictedstripe_checkout_sessions.status = conflictedalipay_checkout_sessions.status = conflictedorder_id、raw_response、error_message、conflicted_atfileinfo、redis。PaymentGatewayService,进一步减少 OrderController 和 ProductLicenseService 的重复 continue 编排。ExpirePaymentSessions 增加更准确的成功/跳过/失败计数。本轮改动解决的是支付网关最核心的一致性问题:第三方支付页面不可控时,本地系统不能再依赖“自动取消 order”来优化交互,而应该把 order 与 provider session 的状态边界区分清楚。
当前实现选择了较稳妥的策略:
从当前测试结果和代码复核看,已经达到本阶段 PRD/plan 的主要目标,可以进入人工联调和 staging 验证阶段。