fix(payment): unify and harden payment attempts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
53933988bf
commit
089421cd3e
18 files changed
+680
-476
No files matched your search
@@ -210,100 +210,56 @@ class CheckoutService {
|
||||
return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')];
|
||||
}
|
||||
|
||||
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
|
||||
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
|
||||
: false;
|
||||
if ($lock === false) {
|
||||
return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')];
|
||||
}
|
||||
|
||||
try {
|
||||
// 携带订单号与摘要,供网关生成支付单。
|
||||
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
||||
|
||||
// Points payment: synchronous deduction via \AKSRT\Credits\Credits
|
||||
// Points payment is a purely internal settlement (no provider attempt),
|
||||
// but still needs the attempt lock against double-click double deduction.
|
||||
if ($payment_method === 'points') {
|
||||
return $this->pay_with_points($user_id, $order_number, $order);
|
||||
}
|
||||
|
||||
// External gateways: allocate a fresh out_trade_no so re-pay works when
|
||||
// the provider rejects "same order number with changed params".
|
||||
$needs_attempt = !in_array($payment_method, ['balance', 'points'], true);
|
||||
if ($needs_attempt && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$attempt = $repo->allocate_payment_attempt($order_number);
|
||||
if ($attempt === null) {
|
||||
return ['success' => false, 'message' => __('无法生成支付单号,请稍后重试。', 'aksrt')];
|
||||
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
|
||||
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
|
||||
: false;
|
||||
if ($lock === false) {
|
||||
return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')];
|
||||
}
|
||||
$order = $repo->get_user_order($user_id, $order_number) ?: $order;
|
||||
$order['out_trade_no'] = (string) $attempt['out_trade_no'];
|
||||
// Close previous provider trade when possible (WeChat/Alipay) so the new attempt is clean.
|
||||
if (!empty($attempt['previous'])
|
||||
&& $attempt['previous'] !== $attempt['out_trade_no']
|
||||
&& method_exists($gateway, 'close_trade')) {
|
||||
try {
|
||||
$gateway->close_trade((string) $attempt['previous']);
|
||||
} catch (\Throwable $e) {
|
||||
// best-effort
|
||||
}
|
||||
try {
|
||||
return $this->pay_with_points($user_id, $order_number, $order);
|
||||
} finally {
|
||||
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
|
||||
}
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
// 携带订单号与摘要,供网关生成支付单。
|
||||
// PaymentOrchestrator 内部持有 attempt 锁并分配 provider out_trade_no。
|
||||
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
||||
$order['payment_method'] = $payment_method;
|
||||
|
||||
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($gateway, $order, [
|
||||
'paid_note' => __('用户在前台完成支付。', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')];
|
||||
return ['success' => false, 'message' => $payment_result['message']];
|
||||
}
|
||||
|
||||
// 跳转/扫码型网关(支付宝/微信/PayPal/Stripe):返回支付凭据,订单保持待支付,
|
||||
// 由 /payment/{gateway}/notify|return 回调确认到账;此处绝不预先标记已支付。
|
||||
$code_url = $payment_result['payment_data']['code_url'] ?? '';
|
||||
if (!empty($payment_result['payment_url']) || $code_url !== '') {
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) $code_url,
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// 同步型网关(站内余额):create_payment 已实际扣款,标记已支付(触发 aksrt_order_paid 履约)。
|
||||
$paid = $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
'payment_method' => $payment_method,
|
||||
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
|
||||
'note' => __('用户在前台完成支付。', 'aksrt'),
|
||||
'require_transition' => true,
|
||||
]);
|
||||
if (!$paid) {
|
||||
$compensation = $gateway->refund(
|
||||
$order_number,
|
||||
(float) ($order['amount'] ?? 0),
|
||||
'PAY_ROLLBACK_' . substr(hash('sha256', $order_number), 0, 24)
|
||||
);
|
||||
if (empty($compensation['success'])) {
|
||||
error_log(sprintf(
|
||||
'[AKSRT Payment] rollback failed order=%s gateway=%s: %s',
|
||||
$order_number,
|
||||
$payment_method,
|
||||
(string) ($compensation['message'] ?? 'unknown error')
|
||||
));
|
||||
return ['success' => false, 'message' => __('订单状态已变化,扣款补偿失败,请联系管理员处理。', 'aksrt')];
|
||||
}
|
||||
return ['success' => false, 'message' => __('订单状态已变化,扣款已退回。', 'aksrt')];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order' => $this->shop->get_order_detail_view($user_id, $order_number),
|
||||
'message' => $payment_result['message'] ?? __('支付成功。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
],
|
||||
];
|
||||
} finally {
|
||||
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
|
||||
}
|
||||
}
|
||||
|
||||
private function pay_with_points(int $user_id, string $order_number, array $order): array {
|
||||
|
||||
Reference in new issue
Block a user