支付网关重构代码修改计划
1. 文档目的
本文档基于 docs/payment-gateway-redesign-prd.md,用于指导后续代码实现、任务拆分和阶段验收。
当前阶段采用两层支付模型:
Order = 一次待支付订单 / 轻量支付意图
Provider Session = 一次渠道支付尝试
不新增独立 payment_intents 和 payment_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=conflicted、error_message、raw_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:
测试:
- 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 返回
WorkTaskPayingPaymentConflict 或 PaymentSessionConflict。
测试:
- 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. 清理旧重复逻辑和同步文档