支付网关重构代码修改计划

1. 文档目的

本文档基于 docs/payment-gateway-redesign-prd.md,用于指导后续代码实现、任务拆分和阶段验收。

当前阶段采用两层支付模型:

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

不新增独立 payment_intentspayment_attempts 表。

2. 已确认约束

2.1 接口兼容

第一阶段以后端改造为主,前端继续使用现有接口:

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

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

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

2.2 业务决策

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

2.3 支付安全边界

  • continue 只能继续当前 active provider session,不能静默切换渠道。
  • 自动重建只能同渠道。
  • 不能因为支付 session 过期自动取消业务订单。
  • PayPal/Alipay 已打开的外部页面无法保证被物理关闭,必须处理 late success。
  • 同一 worktask 同一时刻只能有一个 active 支付动作:
stage_pay
full_pay
price_change
  • 同一 paying order 同一时刻只能有一个 active provider session。

3. 目标代码结构

3.1 不新增的结构

当前阶段不新增:

app/Models/PaymentIntent.php
app/Models/PaymentAttempt.php
database/migrations/*create_payment_intents_table.php
database/migrations/*create_payment_attempts_table.php

3.2 建议新增枚举或 error code

不需要新增 PaymentIntent/PaymentAttempt 状态枚举。

建议只在 App\Enums\ErrorCode 中补充当前两层模型需要的错误码:

PaymentSessionExpired
PaymentSessionNotFound
PaymentOrderNotPayable
PaymentSessionConflict
PaymentChannelNotAllowed
PaymentAlreadySucceeded
PaymentProviderUnavailable
WalletDeductionConflict
WorkTaskPaymentActionConflict
PaymentConflictRequiresManualReview

3.3 建议新增服务

建议继续放在现有 app/Service/Payment 下,避免引入过重目录结构:

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

职责:

  • OrderPaymentService: 统一 order 继续支付、取消订单、支付成功处理入口。
  • PaymentSessionResolver: 解析 order 当前 active provider session,检测多 active session。
  • PaymentSessionContinueService: 按渠道继续或同渠道重建 session。
  • PaymentSessionCancelService: 取消 order 和 provider session,处理 wallet deduction 退款/释放。
  • PaymentSessionExpireService: 处理 session 过期,不取消 order。
  • PaymentConflictService: 记录 late success/conflicted,不自动退款。
  • WorkTaskPaymentGuard: stage/full/price_change 互斥检查。

3.4 需要改造的现有文件

优先改造:

app/Http/Controllers/Api/User/OrderController.php
app/Http/Controllers/Api/User/WorkTaskPayController.php
app/Service/Payment/WorkTaskPaymentService.php
app/Service/Payment/WorkTaskPayingOrderService.php
app/Service/Payment/ProductPaymentService.php
app/Service/Product/ProductLicenseService.php
app/Service/Paypal/PaypalWebhookService.php
app/Service/Alipay/AlipayWebhookService.php
app/Service/Stripe/StripeWebhookService.php
app/Console/Commands/ExpirePaymentSessions.php
app/Enums/ErrorCode.php

需要结合实际代码查找改价入口:

app/Http/Controllers/Api/User/WorkTaskPriceChangeController.php
app/Http/Controllers/Api/Artist/WorkTaskPriceChangeController.php
app/Models/WorkTaskPriceChange.php

需要加固 project 选择入口:

app/Http/Controllers/Api/User/ProjectRequestController.php

4. 数据库修改计划

当前阶段不新增支付意图/支付尝试表。

4.1 可选:新增 payment_conflicts 表

建议新增 payment_conflicts,用于记录人工处理项,不自动退款。

建议字段:

id
order_id
provider_session_type
provider_session_id
pay_channel
provider_trade_no
reason
amount
currency_id
order_status
session_status
payload json nullable
status
note text nullable
resolved_by nullable
resolved_at nullable
created_at
updated_at

建议状态:

open
reviewing
resolved
ignored

说明:

  • 如果不想第一阶段加表,也可以先把冲突信息落到 provider session 的 status=conflictederror_messageraw_response,并增加日志。
  • 从长期人工处理可观测性看,建议加 payment_conflicts

4.2 可选:provider session 表补充字段

如果现有字段不足,可给三张渠道 session 表补充统一字段:

error_message nullable
conflicted_at nullable
cancelled_at nullable
failed_at nullable
finished_at nullable

当前已经有部分字段时,不重复新增。

4.3 不新增字段

当前阶段不新增:

stripe_checkout_sessions.payment_attempt_id
alipay_checkout_sessions.payment_attempt_id
paypal_checkout_sessions.payment_attempt_id

因为不落地独立 payment_attempts 表。

5. 分阶段实施计划

阶段 0:准备和基线测试

目标:确认当前行为,避免重构时丢失已有修复。

任务:

  • 运行现有支付相关测试。
  • 记录当前通过的测试命令和结果。
  • 梳理当前 worktask/product/payment feature 测试覆盖。
  • 确认 .env.testing 使用 testing 数据库。

建议命令:

composer test
vendor/bin/phpunit tests/Feature/OrderContinueAndCancelTest.php
vendor/bin/phpunit tests/Unit/Service/Product/ProductLicenseServiceTest.php
vendor/bin/phpunit tests/Unit/Service/Product/ProductPaymentServiceTest.php

验收:

  • 当前测试基线明确。
  • 支付相关测试可重复运行。

阶段 1:实现 PaymentSessionResolver

目标:统一解析一个 order 当前 active provider session。

任务:

  • 新增 PaymentSessionResolver
  • 统一识别 active session:
    • Stripe: PENDING, PROCESSING
    • Alipay: PENDING, PROCESSING
    • PayPal: PENDING, PROCESSING
  • 支持返回 session channel:
    • stripe
    • alipay
    • paypal
    • internal
  • 检测一个 order 下多个 active provider sessions。
  • 多 active session 返回 error code,不自动选择。

建议方法:

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

    public function activeSessions(Order $order): array;

    public function assertSingleActiveSession(Order $order): void;

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

测试:

  • 单 Stripe active session 可识别。
  • 单 Alipay active session 可识别。
  • 单 PayPal active session 可识别。
  • 多 active session 返回 error code。
  • 没有 active session 返回 null。

验收:

  • 不改变现有接口行为。
  • resolver 可供 continue、cancel、paying_order 复用。

阶段 2:统一/api/orders/continue

目标:修复 continue 的核心风险,不再静默切渠道。

修改文件:

app/Http/Controllers/Api/User/OrderController.php
app/Service/Payment/PaymentSessionResolver.php
app/Service/Payment/PaymentSessionContinueService.php

任务:

  • OrderController::continue 只负责:
    • 校验 request。
    • 锁定用户 order。
    • 调用 continue service。
    • 返回兼容响应结构。
  • 移除默认 Stripe fallback。
  • 按 resolver 返回的 active session channel 继续支付。

渠道规则:

  • Stripe:
    • open 返回原 client_secret
    • expired 且可安全重建时,同渠道创建新的 Stripe session。
  • Alipay:
    • 未到 expires_at 返回原 pay_url
    • 到期后标记旧 session expired,创建新的 Alipay session 和新的 out_trade_no
    • 不创建新的业务 order。
  • PayPal:
    • pending/requires_action 返回原 pay_url
    • processing 返回 PaymentProcessing
    • failed/cancelled/明确不可继续后,才允许同渠道新 PayPal session。
  • Internal:
    • 返回 out_trade_no

测试:

  • Alipay pending continue 返回 Alipay。
  • PayPal pending continue 返回 PayPal。
  • Stripe pending continue 返回 Stripe。
  • Alipay 过期后创建新的 Alipay session 和新的 out_trade_no
  • PayPal processing 返回 PaymentProcessing
  • 多 active session 返回 error code。
  • 无任何路径 fallback 到 Stripe。
  • 0 元订单返回 internal,并包含 out_trade_no

验收:

  • /api/orders/continue 对外接口不变。
  • 支付渠道不会错乱。

阶段 3:统一/api/orders/cancel

目标:收敛取消订单语义,保证 worktask 不被取消,PayPal processing 不被误取消。

修改文件:

app/Http/Controllers/Api/User/OrderController.php
app/Service/OrderService.php
app/Service/Payment/PaymentSessionCancelService.php
app/Service/Paypal/PaypalWebhookService.php
app/Http/Controllers/Api/User/WorkTaskPayController.php

任务:

  • /api/orders/cancel 语义保持为取消当前支付订单。
  • 对 worktask:
    • Order -> cancelled
    • wallet deduction refund/release
    • provider session cancelled/expired/abandoned
    • worktask 保持 wait_pay
  • 对 product:
    • Order -> cancelled
    • product license cancelled
    • 库存恢复
    • wallet deduction refund/release
  • PayPal pending/requires_action 可以取消 order,但旧 PayPal 页面可能 late success。
  • PayPal processing 不允许取消 order,返回 PaymentProcessing
  • PayPal succeeded 不允许取消 order,返回 PaymentAlreadySucceeded
  • 修改 PayPal cancel callback,不能再因为用户从 PayPal 页面取消就直接取消业务 order;只能同步远端状态并按规则处理。

测试:

  • worktask order cancel 后 worktask 仍 wait_pay
  • product order cancel 后 license cancelled 且库存恢复。
  • wallet deduction refund/release。
  • PayPal pending 可取消 order。
  • PayPal processing 取消返回 PaymentProcessing
  • PayPal succeeded 取消返回 PaymentAlreadySucceeded
  • PayPal cancel callback 不直接取消已 processing/不可确认状态的 order。

验收:

  • 前端继续使用现有 /api/orders/cancel
  • 不通过取消订单隐式取消 worktask。

阶段 4:worktask 创建支付接入 WorkTaskPaymentGuard

目标:保证 stage/full/price_change 互斥。

修改文件:

app/Service/Payment/WorkTaskPaymentService.php
app/Service/Payment/WorkTaskPaymentGuard.php
app/Service/Payment/WorkTaskPayingOrderService.php
相关 WorkTaskPriceChange 入口

任务:

  • 新增 WorkTaskPaymentGuard
  • 在 worktask 创建支付事务中:
    • work_tasks
    • 锁 active paying orders。
    • 锁 active price changes。
    • 检查 stage/full/price_change 互斥。
    • 锁 wallet rows。
    • 创建 order。
    • 写现有 wallet deduction。
    • 创建 provider session。
  • /api/work_tasks/paying_order 基于 paying order 和 active provider session 返回。
  • no paying_order 时返回空,不重新计算。

互斥规则:

  • 存在 active stage/full/price_change paying order 时,不能创建另一类支付。
  • 存在 pending/wait_pay 改价时,不能创建 stage/full 支付。
  • 存在 active stage/full paying order 时,不能发起改价。
  • 多 active provider session 返回 WorkTaskPayingPaymentConflictPaymentSessionConflict

测试:

  • stage/full/price_change 并发互斥。
  • 有 active price_change 支付时,stage/full 创建失败。
  • 有 active stage/full 支付时,price_change 创建失败。
  • buyer 和 artist 查询 paying_order 行为正确。
  • no paying_order 时不触发重新计算。

验收:

  • worktask 同一时刻只有一个 active 支付动作。

阶段 5:product 创建和继续支付统一

目标:product 和 worktask 共享 continue/cancel/session 行为。

修改文件:

app/Service/Payment/ProductPaymentService.php
app/Service/Product/ProductLicenseService.php
app/Http/Controllers/Api/User/ProductPayController.php
app/Http/Controllers/Api/User/ProductLicenseController.php

任务:

  • ProductPaymentService 保留:
    • product option 校验。
    • 库存锁。
    • product license 创建。
    • amount calculation。
    • wallet deduction。
    • provider session 创建。
  • ProductLicenseService::continuePayment 改为复用统一 continue service。
  • product cancel 复用统一 cancel order 逻辑,并保留 license/库存处理。

测试:

  • product Stripe/Alipay/PayPal/internal 创建支付。
  • product continue 返回当前 active provider session 渠道。
  • Alipay 过期同渠道重建新 session。
  • PayPal processing 返回错误码。
  • product cancel 取消 license 并恢复库存。
  • 库存不会因 continue 或同渠道重建重复扣减。

验收:

  • product 和 worktask 的 continue/cancel 行为一致。

阶段 6:webhook/callback 统一成功和冲突处理

目标:支付成功、重复通知、late success 走统一处理。

修改文件:

app/Http/Controllers/Api/User/WorkTaskPayController.php
app/Http/Controllers/Api/StripeController.php
app/Listeners/StripeEventListener.php
app/Service/Stripe/StripeWebhookService.php
app/Service/Alipay/AlipayWebhookService.php
app/Service/Paypal/PaypalWebhookService.php
app/Service/Payment/PaymentConflictService.php

任务:

  • Stripe webhook/callback 定位 Stripe session。
  • Alipay notify/return 定位 Alipay session。
  • PayPal return/cancel/webhook 定位 PayPal session。
  • 成功处理统一:
    • 锁 provider session。
    • 锁 order。
    • 校验金额和币种。
    • order paying 时完成支付。
    • order paid 且同 session 时幂等返回。
    • order paid 但其他 session 成功,进入 conflicted。
    • order cancelled 后 late success,进入 conflicted。
  • conflicted 只记录人工处理,不自动退款。

测试:

  • 重复 webhook 幂等。
  • PayPal late success after cancelled order 进入 conflicted。
  • Alipay late success after cancelled order 进入 conflicted。
  • 已 paid order 收到其他 session 成功进入 conflicted。
  • 金额不匹配进入 conflicted。
  • 不触发自动退款。

验收:

  • 支付成功只会履约一次。
  • late success 可追踪。
  • conflicted 可人工处理。

阶段 7:过期任务改造

目标:支付 session 过期不再自动取消业务 order。

修改文件:

app/Console/Commands/ExpirePaymentSessions.php
routes/console.php
app/Service/Payment/PaymentSessionExpireService.php

任务:

  • 保留 ExpirePaymentSessions 命令名,降低调度改动。
  • Alipay:
    • expires_at 后标记 session expired。
    • 不取消 order。
  • Stripe:
    • 同步/expire Stripe Checkout Session。
    • 标记 session expired。
    • 不取消 order。
  • PayPal:
    • 不靠本地 expires_at 判死。
    • 只做 query/reconcile。
    • processing 不取消。
  • 清理现有自动 cancel order 逻辑。

测试:

  • Alipay expired 后 order 仍 paying。
  • Stripe expired 后 order 仍 paying。
  • PayPal pending 不因本地过期被取消。
  • continue 在 expired 后按渠道策略返回原链接、重建或错误码。

验收:

  • 没有支付 session 过期自动取消业务订单的路径。

阶段 8:project 选择流程加固

目标:project request 选择 artist 创建 worktask 时保证幂等和一致性。

修改文件:

app/Http/Controllers/Api/User/ProjectRequestController.php

任务:

  • chooseAndCreateWorkTask 改为完整事务。
  • project_requests 当前行。
  • projects 当前行。
  • 检查 project request 状态仍可选择。
  • 检查 project 是否已有 active chosen worktask。
  • 检查同 project request 是否已有 active worktask。
  • 当前业务只允许一个 project 选择一个 artist。
  • 创建 worktask/stages/group 保持同一事务。
  • 重复请求返回已有结果或明确 error code,具体按前端期望决定。

测试:

  • 重复点击选择不会创建多个 worktask。
  • 并发选择同一个 project request 只能成功一次。
  • 并发选择同一个 project 的不同 project request 只能成功一个。
  • 已有 active worktask 时再次选择返回明确 error code。

验收:

  • project 到 worktask 的转换不会重复创建。

阶段 9:清理旧逻辑和文档同步

目标:移除不再使用的重复逻辑,保证文档和代码一致。

任务:

  • 清理 OrderController 中遗留的渠道 continue 私有方法。
  • 清理 ProductLicenseService 中重复的 recreate Stripe/Alipay/PayPal 逻辑。
  • 清理所有默认 fallback Stripe 的路径。
  • 清理裸 abort(409) 或 HTTP 409。
  • 更新 API 文档。
  • 更新 PRD 和 plan 中已完成状态。

测试:

  • 全量 composer test
  • 支付相关 feature/unit 测试。
  • 人工验证前端现有接口响应结构。

验收:

  • 代码只有一个支付网关主路径。
  • worktask/product 业务层不再直接散落 provider continue 逻辑。

6. Error Code 修改计划

app/Enums/ErrorCode.php 增加或确认以下错误码:

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

规则:

  • 业务冲突使用 error code,不返回裸 409。
  • 多 active provider session 使用 PaymentSessionConflict 或已有 WorkTaskPayingPaymentConflict
  • stage/full/price_change 互斥使用 WorkTaskPaymentActionConflict
  • late success 需要人工处理时使用 PaymentConflictRequiresManualReview

7. 响应兼容要求

现有支付创建和继续支付接口必须继续返回前端已使用字段。

7.1 Stripe

{
  "data": {
    "pay_channel": "stripe",
    "pay_data": {
      "checkout_session_id": "cs_xxx",
      "client_secret": "cs_secret_xxx"
    },
    "checkout_session_id": "cs_xxx",
    "client_secret": "cs_secret_xxx",
    "amount": 1000,
    "currency": {}
  }
}

7.2 Alipay

{
  "data": {
    "pay_channel": "alipay",
    "pay_data": {
      "pay_url": "https://...",
      "out_trade_no": "202606..."
    },
    "pay_url": "https://...",
    "out_trade_no": "202606...",
    "amount": 1000,
    "currency": {}
  }
}

7.3 PayPal

{
  "data": {
    "pay_channel": "paypal",
    "pay_data": {
      "pay_url": "https://...",
      "paypal_order_id": "xxx"
    },
    "pay_url": "https://...",
    "paypal_order_id": "xxx",
    "amount": 1000,
    "currency": {}
  }
}

7.4 Internal

{
  "data": {
    "pay_channel": "internal",
    "pay_data": {
      "out_trade_no": "202606..."
    },
    "out_trade_no": "202606...",
    "amount": 0,
    "currency": {}
  }
}

8. 测试矩阵

8.1 Worktask

  • 创建 stage Stripe 支付。
  • 创建 stage Alipay 支付。
  • 创建 stage PayPal 支付。
  • 创建 full Stripe 支付。
  • 创建 full Alipay 支付。
  • 创建 full PayPal 支付。
  • 创建 price_change Stripe 支付。
  • 创建 price_change Alipay 支付。
  • 创建 price_change PayPal 支付。
  • 0 元支付返回 internal。
  • paying_order 无支付中订单返回空。
  • paying_order 多 active order 返回 error code。
  • paying_order 多 active provider session 返回 error code。
  • continue 不切渠道。
  • cancel order 不取消 worktask。

8.2 Product

  • 创建 Stripe 支付。
  • 创建 Alipay 支付。
  • 创建 PayPal 支付。
  • 0 元支付返回 internal。
  • continue 不切渠道。
  • product license cancel 释放现有 wallet deduction。
  • 库存不因 continue 或同渠道重建重复扣。

8.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。
  • PayPal late success 对已 paid order 进入 conflicted。

8.4 Project

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

9. 风险和回滚

9.1 风险

  • 历史订单可能已有多个 active provider session,需要 resolver 明确返回冲突。
  • Alipay 新 out_trade_no 会影响 notify 定位,必须保证 callback 使用 Alipay session 定位 order。
  • PayPal late success 需要严格幂等,不能重复履约。
  • Wallet deduction 继续复用现有机制,必须确认取消退款和成功确认路径不会重复执行。
  • Product 库存当前在创建支付时扣减,同渠道重建不能重复扣库存。

9.2 回滚策略

  • 不新增核心支付表,数据库回滚压力低。
  • 每个入口按阶段改造,可按入口回滚。
  • 不删除旧 provider session 表。
  • 不删除旧字段。
  • 旧接口保持不变,前端可继续调用。

10. 每阶段完成定义

每个阶段完成前必须满足:

  • 代码通过格式和静态语法检查。
  • 相关 feature/unit 测试通过。
  • 不新增裸 409。
  • 不新增静默 fallback 到其他支付渠道。
  • 不新增支付 session 过期自动取消业务订单逻辑。
  • 不新增自动退款逻辑。
  • 文档同步更新。

建议命令:

composer test
vendor/bin/phpunit tests/Feature/OrderContinueAndCancelTest.php
vendor/bin/phpunit tests/Unit/Service/Product/ProductLicenseServiceTest.php
vendor/bin/phpunit tests/Unit/Service/Product/ProductPaymentServiceTest.php

11. 建议实施顺序摘要

1. PaymentSessionResolver
2. /api/orders/continue 接入统一 continue
3. /api/orders/cancel 收敛取消订单语义
4. worktask 创建支付接入 WorkTaskPaymentGuard
5. product 创建/继续/取消接入统一 session 行为
6. webhook/callback 成功和冲突处理统一
7. ExpirePaymentSessions 改为只处理 session,不取消 order
8. project chooseAndCreateWorkTask 加锁幂等
9. 清理旧重复逻辑和同步文档