Merge pull request 'Worktree tighten payment gateways' (#308) from worktree-tighten-payment-gateways into main
Reviewed-on: CYL/wordpress-theme-aksrt#308
This commit is contained in:
@@ -209,37 +209,49 @@ trait PayOrders {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$deducted = \AKSRT\Credits\Credits::deduct(
|
||||
$user_id,
|
||||
$amount,
|
||||
'content_points_pay',
|
||||
['type' => 'post', 'id' => $post_id, 'order' => $order_number],
|
||||
sprintf(__('积分购买《%s》', 'aksrt'), get_the_title($post_id))
|
||||
);
|
||||
|
||||
if (is_wp_error($deducted) || $deducted === false) {
|
||||
\AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
|
||||
'note' => __('积分扣除失败,自动取消', 'aksrt'),
|
||||
]);
|
||||
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败。', 'aksrt');
|
||||
return new \WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败。', 'aksrt'), ['status' => 400]);
|
||||
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
|
||||
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
|
||||
: false;
|
||||
if ($lock === false) {
|
||||
return new \WP_Error('aksrt_pay_locked', __('该订单正在发起支付,请稍后重试。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
'payment_method' => 'points',
|
||||
'payment_transaction_id' => 'points_' . $order_number,
|
||||
'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount),
|
||||
]);
|
||||
|
||||
if (!$paid) {
|
||||
\AKSRT\Credits\Credits::add(
|
||||
try {
|
||||
$deducted = \AKSRT\Credits\Credits::deduct(
|
||||
$user_id,
|
||||
$amount,
|
||||
'content_points_pay_refund',
|
||||
'content_points_pay',
|
||||
['type' => 'post', 'id' => $post_id, 'order' => $order_number],
|
||||
sprintf(__('购买失败退款:%s', 'aksrt'), get_the_title($post_id))
|
||||
sprintf(__('积分购买《%s》', 'aksrt'), get_the_title($post_id))
|
||||
);
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败,已尝试退回积分。', 'aksrt'), ['status' => 500]);
|
||||
|
||||
if (is_wp_error($deducted) || $deducted === false) {
|
||||
\AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
|
||||
'note' => __('积分扣除失败,自动取消', 'aksrt'),
|
||||
]);
|
||||
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败。', 'aksrt');
|
||||
return new \WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败。', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
'payment_method' => 'points',
|
||||
'payment_transaction_id' => 'points_' . $order_number,
|
||||
'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount),
|
||||
'require_transition' => true,
|
||||
]);
|
||||
|
||||
if (!$paid) {
|
||||
\AKSRT\Credits\Credits::add(
|
||||
$user_id,
|
||||
$amount,
|
||||
'content_points_pay_refund',
|
||||
['type' => 'post', 'id' => $post_id, 'order' => $order_number],
|
||||
sprintf(__('购买失败退款:%s', 'aksrt'), get_the_title($post_id))
|
||||
);
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败,已尝试退回积分。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
} finally {
|
||||
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -453,69 +465,50 @@ trait PayOrders {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$gateway_manager = \AKSRT\Payment\Gateway::get_instance();
|
||||
$gateway = $gateway_manager->get_gateway($payment_method);
|
||||
if (!$gateway) {
|
||||
return new \WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
||||
return new \WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
|
||||
if (empty($resolved['gateway'])) {
|
||||
return new \WP_Error(
|
||||
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
|
||||
(string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')),
|
||||
['status' => 400, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
|
||||
'paid_note' => __('用户完成内容购买支付。', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
$msg = (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt'));
|
||||
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
|
||||
? 'aksrt_balance_insufficient'
|
||||
: 'aksrt_pay_failed';
|
||||
return new \WP_Error($code, $msg, [
|
||||
'status' => 402,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
return new \WP_Error(
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
|
||||
$payment_url = (string) ($payment_result['payment_url'] ?? '');
|
||||
if ($payment_url !== '' || $code_url !== '') {
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $code_url,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Sync gateway (balance): mark paid.
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->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'),
|
||||
]);
|
||||
if (!$paid) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('支付成功', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -514,7 +514,7 @@ trait VipServicePackages {
|
||||
return $methods;
|
||||
}
|
||||
$gw = \AKSRT\Payment\Gateway::get_instance();
|
||||
foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe'] as $id) {
|
||||
foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe', 'epay_alipay', 'epay_wxpay'] as $id) {
|
||||
$g = $gw->get_gateway($id);
|
||||
if (!$g) {
|
||||
continue;
|
||||
|
||||
@@ -344,59 +344,42 @@ trait VipServicePurchase {
|
||||
if (!class_exists('\AKSRT\Payment\Gateway')) {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$gateway = \AKSRT\Payment\Gateway::get_instance()->get_gateway($payment_method);
|
||||
if (!$gateway) {
|
||||
return new \WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
||||
return new \WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
|
||||
if (empty($resolved['gateway'])) {
|
||||
return new \WP_Error(
|
||||
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
|
||||
(string) ($resolved['message'] ?? __('当前支付方式不可用', 'aksrt')),
|
||||
['status' => 400, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
|
||||
'paid_note' => __('用户完成会员开通支付。', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
$msg = (string) ($payment_result['message'] ?? __('支付失败', 'aksrt'));
|
||||
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
|
||||
? 'aksrt_balance_insufficient'
|
||||
: 'aksrt_pay_failed';
|
||||
return new \WP_Error($code, $msg, [
|
||||
'status' => 402,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
return new \WP_Error(
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
|
||||
$payment_url = (string) ($payment_result['payment_url'] ?? '');
|
||||
if ($payment_url !== '' || $code_url !== '') {
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $code_url,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->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'),
|
||||
]);
|
||||
if (!$paid) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
@@ -637,35 +620,47 @@ trait VipServicePurchase {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$deducted = \AKSRT\Credits\Credits::deduct(
|
||||
$user_id,
|
||||
$cost,
|
||||
'vip_points_exchange',
|
||||
['order' => $order_number, 'level' => $level, 'days' => $days],
|
||||
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? ''))
|
||||
);
|
||||
if (is_wp_error($deducted) || $deducted === false) {
|
||||
\AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
|
||||
'note' => __('积分扣除失败,自动取消', 'aksrt'),
|
||||
]);
|
||||
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
|
||||
return new \WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]);
|
||||
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
|
||||
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
|
||||
: false;
|
||||
if ($lock === false) {
|
||||
return new \WP_Error('aksrt_pay_locked', __('该订单正在发起支付,请稍后重试。', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
'payment_method' => 'points',
|
||||
'payment_transaction_id' => 'points_' . $order_number,
|
||||
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
|
||||
]);
|
||||
if (!$paid) {
|
||||
\AKSRT\Credits\Credits::add(
|
||||
try {
|
||||
$deducted = \AKSRT\Credits\Credits::deduct(
|
||||
$user_id,
|
||||
$cost,
|
||||
'vip_points_exchange_refund',
|
||||
['order' => $order_number],
|
||||
__('会员兑换失败退款', 'aksrt')
|
||||
'vip_points_exchange',
|
||||
['order' => $order_number, 'level' => $level, 'days' => $days],
|
||||
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? ''))
|
||||
);
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]);
|
||||
if (is_wp_error($deducted) || $deducted === false) {
|
||||
\AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
|
||||
'note' => __('积分扣除失败,自动取消', 'aksrt'),
|
||||
]);
|
||||
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
|
||||
return new \WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
|
||||
'payment_method' => 'points',
|
||||
'payment_transaction_id' => 'points_' . $order_number,
|
||||
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
|
||||
'require_transition' => true,
|
||||
]);
|
||||
if (!$paid) {
|
||||
\AKSRT\Credits\Credits::add(
|
||||
$user_id,
|
||||
$cost,
|
||||
'vip_points_exchange_refund',
|
||||
['order' => $order_number],
|
||||
__('会员兑换失败退款', 'aksrt')
|
||||
);
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
} finally {
|
||||
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
@@ -296,56 +296,42 @@ trait BalanceServiceCharge {
|
||||
if (!class_exists('\AKSRT\Payment\Gateway')) {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$gateway = \AKSRT\Payment\Gateway::get_instance()->get_gateway($payment_method);
|
||||
if (!$gateway) {
|
||||
return new \WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
||||
return new \WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
|
||||
if (empty($resolved['gateway'])) {
|
||||
return new \WP_Error(
|
||||
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
|
||||
(string) ($resolved['message'] ?? __('当前支付方式不可用', 'aksrt')),
|
||||
['status' => 400, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
|
||||
'paid_note' => __('余额充值支付完成', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
return new \WP_Error(
|
||||
'aksrt_pay_failed',
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
|
||||
$payment_url = (string) ($payment_result['payment_url'] ?? '');
|
||||
if ($payment_url !== '' || $code_url !== '') {
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $code_url,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_amount,
|
||||
'credit_amount' => $credit_amount,
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Synchronous gateway (rare for recharge): mark paid → on_order_paid credits wallet.
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->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'),
|
||||
]);
|
||||
if (!$paid) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('支付确认失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
// Ensure credit even if status hook shape differs.
|
||||
$this->on_order_paid($order_number, \AKSRT\Payment\OrderRepository::get_instance()->get($order_number) ?: $order);
|
||||
// Synchronous gateway: PaymentOrchestrator's paid transition already fired
|
||||
// aksrt_order_paid, so fulfillment has run through the single hook path.
|
||||
|
||||
return [
|
||||
'redirect' => false,
|
||||
|
||||
@@ -195,41 +195,38 @@ trait BalanceServicePoints {
|
||||
if (!class_exists('\AKSRT\Payment\Gateway')) {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$gateway = \AKSRT\Payment\Gateway::get_instance()->get_gateway($payment_method);
|
||||
if (!$gateway) {
|
||||
return new \WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), ['status' => 400, 'order_number' => $order_number]);
|
||||
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
|
||||
if (empty($resolved['gateway'])) {
|
||||
return new \WP_Error(
|
||||
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
|
||||
(string) ($resolved['message'] ?? __('当前支付方式不可用', 'aksrt')),
|
||||
['status' => 400, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
||||
return new \WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), ['status' => 400, 'order_number' => $order_number]);
|
||||
}
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
|
||||
'paid_note' => __('购买积分支付完成', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
return new \WP_Error('aksrt_pay_failed', (string) ($payment_result['message'] ?? __('支付失败', 'aksrt')), ['status' => 402, 'order_number' => $order_number]);
|
||||
return new \WP_Error(
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
|
||||
$payment_url = (string) ($payment_result['payment_url'] ?? '');
|
||||
if ($payment_url !== '' || $code_url !== '') {
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $code_url,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $pay_amount,
|
||||
'points_amount' => $points_amount,
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
];
|
||||
}
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->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'),
|
||||
]);
|
||||
if (!$paid) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('支付确认失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$this->on_order_paid($order_number, \AKSRT\Payment\OrderRepository::get_instance()->get($order_number) ?: $order);
|
||||
// PaymentOrchestrator's paid transition already dispatched the fulfillment hook.
|
||||
return [
|
||||
'redirect' => false,
|
||||
'order_number' => $order_number,
|
||||
|
||||
@@ -441,72 +441,50 @@ class PlateFollowPay {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$gateway_manager = \AKSRT\Payment\Gateway::get_instance();
|
||||
$gateway = $gateway_manager->get_gateway($payment_method);
|
||||
if (!$gateway) {
|
||||
return new \WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
||||
return new \WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
|
||||
if (empty($resolved['gateway'])) {
|
||||
return new \WP_Error(
|
||||
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
|
||||
(string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')),
|
||||
['status' => 400, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
|
||||
'paid_note' => __('用户完成版块付费关注支付。', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
$msg = (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt'));
|
||||
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
|
||||
? 'aksrt_balance_insufficient'
|
||||
: 'aksrt_pay_failed';
|
||||
return new \WP_Error($code, $msg, [
|
||||
'status' => 402,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
return new \WP_Error(
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
|
||||
$payment_url = (string) ($payment_result['payment_url'] ?? '');
|
||||
if ($payment_url !== '' || $code_url !== '') {
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $code_url,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->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'),
|
||||
]);
|
||||
if (!$paid) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
if ($user_id > 0) {
|
||||
$this->mark_followed($user_id, $plate_id);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('关注成功', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -441,73 +441,50 @@ class TopicPay {
|
||||
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$gateway_manager = \AKSRT\Payment\Gateway::get_instance();
|
||||
$gateway = $gateway_manager->get_gateway($payment_method);
|
||||
if (!$gateway) {
|
||||
return new \WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
||||
return new \WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
|
||||
if (empty($resolved['gateway'])) {
|
||||
return new \WP_Error(
|
||||
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
|
||||
(string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')),
|
||||
['status' => 400, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
|
||||
'paid_note' => __('用户完成论坛主题购买支付。', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
$msg = (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt'));
|
||||
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
|
||||
? 'aksrt_balance_insufficient'
|
||||
: 'aksrt_pay_failed';
|
||||
return new \WP_Error($code, $msg, [
|
||||
'status' => 402,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
return new \WP_Error(
|
||||
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
|
||||
(string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')),
|
||||
['status' => 402, 'order_number' => $order_number]
|
||||
);
|
||||
}
|
||||
|
||||
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
|
||||
$payment_url = (string) ($payment_result['payment_url'] ?? '');
|
||||
if ($payment_url !== '' || $code_url !== '') {
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $code_url,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Sync gateway (balance): mark paid.
|
||||
$paid = \AKSRT\Payment\Order::get_instance()->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'),
|
||||
]);
|
||||
if (!$paid) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
if ($user_id > 0) {
|
||||
$this->mark_unlocked($user_id, $topic_id);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('支付成功', 'aksrt'),
|
||||
'message' => $payment_result['message'],
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -101,16 +101,47 @@ class BalanceGateway implements GatewayInterface {
|
||||
return ['success' => false, 'message' => __('余额功能未启用', 'aksrt')];
|
||||
}
|
||||
|
||||
$result = \AKSRT\Credits\Balance::add(
|
||||
$user_id,
|
||||
$amount,
|
||||
'order_refund',
|
||||
['type' => 'order', 'order' => $order_number],
|
||||
sprintf(__('订单 %s 退款', 'aksrt'), $order_number)
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
return ['success' => false, 'message' => $result->get_error_message() ?: __('退款失败', 'aksrt')];
|
||||
$request_id = sanitize_text_field($request_id);
|
||||
$refund_key = $request_id !== '' ? $request_id : 'order:' . $order_number;
|
||||
$lock_key = 'aksrt_balance_refund_' . hash('sha256', $refund_key);
|
||||
$existing = get_option($lock_key, []);
|
||||
if (is_array($existing) && ($existing['status'] ?? '') === 'succeeded') {
|
||||
return [
|
||||
'success' => true,
|
||||
'refund_id' => 'BAL_REF_' . $order_number,
|
||||
'message' => __('退款已处理(幂等)', 'aksrt'),
|
||||
];
|
||||
}
|
||||
if (is_array($existing) && ($existing['status'] ?? '') === 'pending') {
|
||||
return ['success' => false, 'status' => 'pending', 'message' => __('退款正在处理中,请稍后查询', 'aksrt')];
|
||||
}
|
||||
|
||||
$marker = ['status' => 'pending', 'token' => wp_generate_uuid4(), 'created_at' => time()];
|
||||
if (!add_option($lock_key, $marker, '', false)) {
|
||||
return ['success' => false, 'status' => 'pending', 'message' => __('退款正在处理中,请稍后查询', 'aksrt')];
|
||||
}
|
||||
|
||||
try {
|
||||
$result = \AKSRT\Credits\Balance::add(
|
||||
$user_id,
|
||||
$amount,
|
||||
'order_refund',
|
||||
['type' => 'order', 'order' => $order_number, 'request_id' => $request_id],
|
||||
sprintf(__('订单 %s 退款', 'aksrt'), $order_number)
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
delete_option($lock_key);
|
||||
return ['success' => false, 'message' => $result->get_error_message() ?: __('退款失败', 'aksrt')];
|
||||
}
|
||||
update_option($lock_key, [
|
||||
'status' => 'succeeded',
|
||||
'refund_id' => 'BAL_REF_' . $order_number,
|
||||
'completed_at' => time(),
|
||||
], false);
|
||||
return ['success' => true, 'refund_id' => 'BAL_REF_' . $order_number, 'message' => __('退款成功', 'aksrt')];
|
||||
} catch (\Throwable $e) {
|
||||
delete_option($lock_key);
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
return ['success' => true, 'refund_id' => 'BAL_REF_' . $order_number, 'message' => __('退款成功', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,26 +146,35 @@ class Gateway {
|
||||
* 用户浏览器同步返回。尽力确认到账(兜底异步通知延迟),再跳回订单中心。
|
||||
*/
|
||||
public function handle_return(\WP_REST_Request $request) {
|
||||
$gateway_id = sanitize_key((string) $request->get_param('gateway'));
|
||||
$order_number = sanitize_text_field((string) ($request->get_param('order')
|
||||
$gateway_id = sanitize_key((string) $request->get_param('gateway'));
|
||||
// Providers echo back whatever number we submitted, which for a re-pay is an
|
||||
// attempt-suffixed out_trade_no. Map it to the local order for query + redirect.
|
||||
$provider_ref = sanitize_text_field((string) ($request->get_param('order')
|
||||
?: $request->get_param('out_trade_no')
|
||||
?: ''));
|
||||
$order_number = $this->local_order_number($provider_ref);
|
||||
$gateway = $this->get_gateway($gateway_id);
|
||||
$status = 'pending';
|
||||
|
||||
if ($gateway) {
|
||||
if ($gateway_id === 'alipay') {
|
||||
// 支付宝同步返回带签名参数,可直接验签作为到账兜底
|
||||
if ($gateway_id === 'alipay' || str_starts_with($gateway_id, 'epay')) {
|
||||
// 支付宝 / 易支付同步返回带签名参数,可直接验签作为到账兜底
|
||||
$verify = $gateway->verify_callback($request);
|
||||
if (!empty($verify['success'])) {
|
||||
$order_number = $order_number ?: sanitize_text_field((string) ($verify['order_number'] ?? ''));
|
||||
if ($order_number === '') {
|
||||
$order_number = $this->local_order_number(
|
||||
sanitize_text_field((string) ($verify['order_number'] ?? ''))
|
||||
);
|
||||
}
|
||||
if (($verify['status'] ?? '') === 'paid' && $this->complete_order($gateway_id, $verify)) {
|
||||
$status = 'success';
|
||||
} elseif ($order_number !== '') {
|
||||
// Alipay synchronous returns generally omit trade_status; query after signature verification.
|
||||
$query = $gateway->query_status($order_number);
|
||||
// Synchronous returns generally omit trade_status; query after signature verification.
|
||||
$query = $gateway->query_status($provider_ref !== '' ? $provider_ref : $order_number);
|
||||
if (!empty($query['success']) && ($query['status'] ?? '') === 'paid') {
|
||||
$query['order_number'] = $order_number;
|
||||
if (empty($query['order_number'])) {
|
||||
$query['order_number'] = $order_number;
|
||||
}
|
||||
if ($this->complete_order($gateway_id, $query)) {
|
||||
$status = 'success';
|
||||
}
|
||||
@@ -178,7 +187,8 @@ class Gateway {
|
||||
if ($paypal_order_id !== '') {
|
||||
$cap = $gateway->capture_order($paypal_order_id);
|
||||
if (!empty($cap['success']) && ($cap['status'] ?? '') === 'paid') {
|
||||
$captured_order = sanitize_text_field((string) ($cap['order_number'] ?? ''));
|
||||
$captured_ref = sanitize_text_field((string) ($cap['order_number'] ?? ''));
|
||||
$captured_order = $this->local_order_number($captured_ref);
|
||||
if ($captured_order === ''
|
||||
|| ($order_number !== '' && !hash_equals($order_number, $captured_order))) {
|
||||
$this->redirect_to_orders($order_number, 'pending');
|
||||
@@ -191,8 +201,8 @@ class Gateway {
|
||||
}
|
||||
}
|
||||
} elseif ($order_number !== '') {
|
||||
// 其余网关:主动查询订单状态兜底
|
||||
$q = $gateway->query_status($order_number);
|
||||
// 其余网关:用返回时的 provider ref 查询该次尝试;local number 仅用于跳转。
|
||||
$q = $gateway->query_status($provider_ref !== '' ? $provider_ref : $order_number);
|
||||
if (!empty($q['success']) && ($q['status'] ?? '') === 'paid') {
|
||||
if (empty($q['order_number'])) {
|
||||
$q['order_number'] = $order_number;
|
||||
@@ -212,13 +222,27 @@ class Gateway {
|
||||
* 用户取消支付,跳回订单中心。
|
||||
*/
|
||||
public function handle_cancel(\WP_REST_Request $request) {
|
||||
$order_number = sanitize_text_field((string) ($request->get_param('order')
|
||||
$provider_ref = sanitize_text_field((string) ($request->get_param('order')
|
||||
?: $request->get_param('out_trade_no')
|
||||
?: ''));
|
||||
$this->redirect_to_orders($order_number, 'cancelled');
|
||||
$this->redirect_to_orders($this->local_order_number($provider_ref), 'cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a provider out_trade_no back to the local order number (identity when unknown).
|
||||
*/
|
||||
private function local_order_number(string $provider_ref): string {
|
||||
if ($provider_ref === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) {
|
||||
return $provider_ref;
|
||||
}
|
||||
$order = OrderRepository::get_instance()->resolve_by_out_trade_no($provider_ref);
|
||||
if (!$order) {
|
||||
return $provider_ref;
|
||||
}
|
||||
return sanitize_text_field((string) ($order['order_number'] ?? $provider_ref));
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等、带金额校验的订单完成。仅由验签通过的回调调用。
|
||||
*
|
||||
@@ -272,9 +296,11 @@ class Gateway {
|
||||
|
||||
$current = (string) ($order['status'] ?? '');
|
||||
|
||||
// 幂等:已支付/已交付/已完成视为成功,不重复处理(回调可能多次到达)
|
||||
// 幂等:仅同一交易号的重复回调可视为成功。不同交易号意味着
|
||||
// 用户可能重复付款,不能静默 ACK 为已正确同步。
|
||||
if (in_array($current, ['paid', 'delivered', 'completed'], true)) {
|
||||
return true;
|
||||
$stored_transaction = sanitize_text_field((string) ($order['payment_transaction_id'] ?? ''));
|
||||
return $stored_transaction !== '' && hash_equals($stored_transaction, $transaction_id);
|
||||
}
|
||||
// 只允许从未支付态完成
|
||||
if (!in_array($current, ['created', 'pending'], true)) {
|
||||
|
||||
@@ -294,7 +294,12 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
*/
|
||||
private function resolve_out_trade_no(string $order_number): string {
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
// A provider attempt id must be queried verbatim, including superseded Pn.
|
||||
if ($repo->resolve_by_out_trade_no($order_number) && !$repo->get($order_number)) {
|
||||
return $order_number;
|
||||
}
|
||||
$order = $repo->get($order_number);
|
||||
if ($order) {
|
||||
return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
|
||||
}
|
||||
|
||||
@@ -328,7 +328,11 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
*/
|
||||
private function resolve_out_trade_no(string $order_number): string {
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
if ($repo->resolve_by_out_trade_no($order_number) && !$repo->get($order_number)) {
|
||||
return $order_number;
|
||||
}
|
||||
$order = $repo->get($order_number);
|
||||
if ($order) {
|
||||
return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,14 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
$order_number = $order['order_number'] ?? '';
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
// Re-pay attempts carry a fresh provider number; the callback resolves it back.
|
||||
$out_trade_no = (string) ($order['out_trade_no'] ?? '');
|
||||
if ($out_trade_no === '') {
|
||||
$out_trade_no = class_exists('\AKSRT\Payment\OrderRepository')
|
||||
? \AKSRT\Payment\OrderRepository::provider_out_trade_no($order)
|
||||
: $order_number;
|
||||
}
|
||||
$amount = (float) ($order['amount'] ?? 0);
|
||||
$subject = $order['subject'] ?? __('订单支付', 'aksrt');
|
||||
$currency = $config['currency'] ?? 'USD';
|
||||
@@ -41,7 +48,7 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
$response = $this->api_request('POST', '/v2/checkout/orders', [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [[
|
||||
'reference_id' => $order_number,
|
||||
'reference_id' => $out_trade_no,
|
||||
'description' => $subject,
|
||||
'amount' => [
|
||||
'currency_code' => $currency,
|
||||
@@ -49,8 +56,8 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
],
|
||||
]],
|
||||
'application_context' => [
|
||||
'return_url' => home_url('/wp-json/aksrt/v1/payment/paypal/return?order=' . $order_number),
|
||||
'cancel_url' => home_url('/wp-json/aksrt/v1/payment/paypal/cancel?order=' . $order_number),
|
||||
'return_url' => home_url('/wp-json/aksrt/v1/payment/paypal/return?order=' . rawurlencode($out_trade_no)),
|
||||
'cancel_url' => home_url('/wp-json/aksrt/v1/payment/paypal/cancel?order=' . rawurlencode($out_trade_no)),
|
||||
'brand_name' => get_bloginfo('name'),
|
||||
'user_action' => 'PAY_NOW',
|
||||
],
|
||||
@@ -71,6 +78,7 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
'payment_url' => $approveUrl,
|
||||
'payment_data' => [
|
||||
'order_number' => $order_number,
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'paypal_id' => $data['id'] ?? '',
|
||||
],
|
||||
'message' => __('PayPal 订单已创建', 'aksrt'),
|
||||
|
||||
@@ -32,7 +32,14 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
}
|
||||
|
||||
$config = $this->get_config();
|
||||
$order_number = $order['order_number'] ?? '';
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
// Re-pay attempts carry a fresh provider number; the callback resolves it back.
|
||||
$out_trade_no = (string) ($order['out_trade_no'] ?? '');
|
||||
if ($out_trade_no === '') {
|
||||
$out_trade_no = class_exists('\AKSRT\Payment\OrderRepository')
|
||||
? \AKSRT\Payment\OrderRepository::provider_out_trade_no($order)
|
||||
: $order_number;
|
||||
}
|
||||
$amount = (float) ($order['amount'] ?? 0);
|
||||
$subject = $order['subject'] ?? __('订单支付', 'aksrt');
|
||||
$currency = strtolower($config['currency'] ?? 'usd');
|
||||
@@ -54,10 +61,11 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'mode' => 'payment',
|
||||
'success_url' => home_url('/wp-json/aksrt/v1/payment/stripe/return?session_id={CHECKOUT_SESSION_ID}&order=' . $order_number),
|
||||
'cancel_url' => home_url('/wp-json/aksrt/v1/payment/stripe/cancel?order=' . $order_number),
|
||||
'client_reference_id' => $out_trade_no,
|
||||
'success_url' => home_url('/wp-json/aksrt/v1/payment/stripe/return?session_id={CHECKOUT_SESSION_ID}&order=' . rawurlencode($out_trade_no)),
|
||||
'cancel_url' => home_url('/wp-json/aksrt/v1/payment/stripe/cancel?order=' . rawurlencode($out_trade_no)),
|
||||
'metadata' => [
|
||||
'order_number' => $order_number,
|
||||
'order_number' => $out_trade_no,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -72,11 +80,31 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
'api_key' => $config['secret_key'],
|
||||
]);
|
||||
|
||||
// Persist the session id before returning the redirect. Without this,
|
||||
// status/refund lookup may miss older sessions after traffic exceeds 100.
|
||||
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$persisted = \AKSRT\Payment\OrderRepository::get_instance()->merge_meta($order_number, [
|
||||
'stripe_session_id' => (string) $session->id,
|
||||
'stripe_sessions' => $this->append_session_mapping($order, $out_trade_no, (string) $session->id),
|
||||
]);
|
||||
if (!$persisted) {
|
||||
try {
|
||||
\Stripe\Checkout\Session::expire((string) $session->id, [
|
||||
'api_key' => $config['secret_key'],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// Best-effort cleanup; do not return an untraceable session.
|
||||
}
|
||||
return ['success' => false, 'message' => __('Stripe 支付会话保存失败,请重试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'payment_url' => $session->url,
|
||||
'payment_data' => [
|
||||
'order_number' => $order_number,
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'session_id' => $session->id,
|
||||
],
|
||||
'message' => __('Stripe 支付页面已创建', 'aksrt'),
|
||||
@@ -135,23 +163,8 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
$config = $this->get_config();
|
||||
|
||||
try {
|
||||
$stripe = $this->create_client($config);
|
||||
|
||||
// 注意:Stripe API 的 Session::all() 不支持按 metadata 过滤。
|
||||
// 此处通过创建时间倒序获取最近的 session,再匹配 metadata。
|
||||
// 生产环境建议在 create_payment 时保存 session_id 并用 Session::retrieve() 查询。
|
||||
$sessions = \Stripe\Checkout\Session::all([
|
||||
'limit' => 100,
|
||||
], ['api_key' => $config['secret_key']]);
|
||||
|
||||
// 手动按 metadata 匹配
|
||||
$found = null;
|
||||
foreach ($sessions->data as $session) {
|
||||
if (($session->metadata->order_number ?? '') === $order_number) {
|
||||
$found = $session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->create_client($config);
|
||||
$found = $this->find_session($order_number, $config, true);
|
||||
|
||||
if ($found) {
|
||||
$status = 'pending';
|
||||
@@ -191,32 +204,22 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
$config = $this->get_config();
|
||||
|
||||
try {
|
||||
$stripe = $this->create_client($config);
|
||||
|
||||
// 先查询 payment_intent(手动按 metadata 匹配)
|
||||
$sessions = \Stripe\Checkout\Session::all([
|
||||
'limit' => 100,
|
||||
], ['api_key' => $config['secret_key']]);
|
||||
|
||||
$found = null;
|
||||
foreach ($sessions->data as $session) {
|
||||
if (($session->metadata->order_number ?? '') === $order_number) {
|
||||
$found = $session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->create_client($config);
|
||||
$found = $this->find_session($order_number, $config, true);
|
||||
|
||||
if (!$found) {
|
||||
return ['success' => false, 'message' => __('未找到支付记录', 'aksrt')];
|
||||
}
|
||||
|
||||
$paymentIntentId = $found->payment_intent;
|
||||
$paymentIntentId = is_string($found->payment_intent)
|
||||
? $found->payment_intent
|
||||
: (string) ($found->payment_intent->id ?? '');
|
||||
|
||||
if (empty($paymentIntentId)) {
|
||||
if ($paymentIntentId === '') {
|
||||
return ['success' => false, 'message' => __('未找到支付意图', 'aksrt')];
|
||||
}
|
||||
|
||||
$currency = strtolower($config['currency'] ?? 'usd');
|
||||
$currency = strtolower((string) ($found->currency ?? $config['currency'] ?? 'usd'));
|
||||
|
||||
$options = ['api_key' => $config['secret_key']];
|
||||
if ($request_id !== '') {
|
||||
@@ -282,6 +285,66 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
return $config['publishable_key'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $order
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private function append_session_mapping(array $order, string $out_trade_no, string $session_id): array {
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
$map = is_array($meta['stripe_sessions'] ?? null) ? $meta['stripe_sessions'] : [];
|
||||
$map[$out_trade_no] = $session_id;
|
||||
if (count($map) > 20) {
|
||||
$map = array_slice($map, -20, null, true);
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the Checkout Session for an order. Prefers the session id persisted
|
||||
* at create time; falls back to scanning recent sessions for legacy orders.
|
||||
*
|
||||
* @return \Stripe\Checkout\Session|null
|
||||
*/
|
||||
private function find_session(string $order_number, array $config, bool $allow_legacy_scan = false) {
|
||||
$session_id = '';
|
||||
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$order = $repo->resolve_by_out_trade_no($order_number) ?: $repo->get($order_number);
|
||||
if ($order) {
|
||||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||||
$sessions = is_array($meta['stripe_sessions'] ?? null) ? $meta['stripe_sessions'] : [];
|
||||
$session_id = sanitize_text_field((string) ($sessions[$order_number] ?? ''));
|
||||
if ($session_id === '' && $repo->get($order_number)) {
|
||||
$current_ref = \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
|
||||
$session_id = sanitize_text_field((string) ($sessions[$current_ref] ?? $meta['stripe_session_id'] ?? ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($session_id !== '') {
|
||||
try {
|
||||
return \Stripe\Checkout\Session::retrieve($session_id, ['api_key' => $config['secret_key']]);
|
||||
} catch (\Exception $e) {
|
||||
// Fall through to the scan below.
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allow_legacy_scan) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Legacy orders created before session-id persistence can only be found
|
||||
// through Stripe's recent-session window.
|
||||
$sessions = \Stripe\Checkout\Session::all(['limit' => 100], ['api_key' => $config['secret_key']]);
|
||||
foreach ($sessions->data as $session) {
|
||||
if (($session->metadata->order_number ?? '') === $order_number) {
|
||||
return $session;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function get_config(): array {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return [];
|
||||
|
||||
@@ -390,7 +390,11 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
||||
*/
|
||||
private function resolve_out_trade_no(string $order_number): string {
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
if ($repo->resolve_by_out_trade_no($order_number) && !$repo->get($order_number)) {
|
||||
return $order_number;
|
||||
}
|
||||
$order = $repo->get($order_number);
|
||||
if ($order) {
|
||||
return \AKSRT\Payment\OrderRepository::provider_out_trade_no($order);
|
||||
}
|
||||
|
||||
@@ -546,8 +546,15 @@ trait OrderRepositoryCore {
|
||||
|
||||
/**
|
||||
* Resolve a local order from a provider out_trade_no (base or attempt-suffixed).
|
||||
*
|
||||
* The authoritative mapping is the order's own meta (provider_out_trade_no /
|
||||
* provider_out_trade_nos); the prefix heuristic is only a fallback and must
|
||||
* never be the sole resolver, otherwise a truncated attempt number would
|
||||
* strand a real payment.
|
||||
*/
|
||||
public function resolve_by_out_trade_no(string $out_trade_no): ?array {
|
||||
global $wpdb;
|
||||
|
||||
$out_trade_no = sanitize_text_field($out_trade_no);
|
||||
if ($out_trade_no === '') {
|
||||
return null;
|
||||
@@ -558,8 +565,37 @@ trait OrderRepositoryCore {
|
||||
return $direct;
|
||||
}
|
||||
|
||||
// Resume attempts: {order_number}P{n}
|
||||
if (preg_match('/^(AKS[0-9]{10,})P([0-9]{1,3})$/', $out_trade_no, $m)) {
|
||||
// Authoritative: the attempt number was recorded in the order note JSON.
|
||||
$needle = '"provider_out_trade_no":"' . $out_trade_no . '"';
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE note LIKE %s ORDER BY id DESC LIMIT 1",
|
||||
'%' . $wpdb->esc_like($needle) . '%'
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
if (is_array($row)) {
|
||||
return $this->normalize_row($row);
|
||||
}
|
||||
|
||||
// Superseded attempts stay in the history array.
|
||||
$history_needle = '"' . $out_trade_no . '"';
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()}
|
||||
WHERE note LIKE %s AND note LIKE %s
|
||||
ORDER BY id DESC LIMIT 1",
|
||||
'%' . $wpdb->esc_like('"provider_out_trade_nos"') . '%',
|
||||
'%' . $wpdb->esc_like($history_needle) . '%'
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
if (is_array($row)) {
|
||||
return $this->normalize_row($row);
|
||||
}
|
||||
|
||||
// Last resort: {order_number}P{n} prefix heuristic.
|
||||
if (preg_match('/^(.+)P([0-9]{1,3})$/', $out_trade_no, $m)) {
|
||||
$base = $this->get($m[1]);
|
||||
if ($base) {
|
||||
return $base;
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* Single execution path for "create a payment attempt for an existing order".
|
||||
*
|
||||
* Every cashier (shop / content / VIP / forum / recharge / points) must route
|
||||
* through here so that all of them get:
|
||||
* 1. a cross-request attempt lock (no double deduction on double-click)
|
||||
* 2. a fresh provider out_trade_no on re-pay (providers reject reused numbers)
|
||||
* 3. best-effort close of the previous provider trade
|
||||
* 4. strict CAS on the paid transition + rollback of synchronous deductions
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Payment;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PaymentOrchestrator {
|
||||
|
||||
/** Methods settled synchronously in-process (no provider round trip). */
|
||||
private const SYNC_METHODS = ['balance', 'points'];
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $order Order row plus subject/amount overrides.
|
||||
* @param array<string,mixed> $opts paid_note: string
|
||||
*
|
||||
* @return array{
|
||||
* success:bool,
|
||||
* code?:string,
|
||||
* message:string,
|
||||
* redirect?:bool,
|
||||
* payment_url?:string,
|
||||
* qr_content?:string,
|
||||
* transaction_id?:string,
|
||||
* out_trade_no?:string
|
||||
* }
|
||||
*/
|
||||
public static function pay(GatewayInterface $gateway, array $order, array $opts = []): array {
|
||||
$order_number = sanitize_text_field((string) ($order['order_number'] ?? ''));
|
||||
if ($order_number === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) {
|
||||
return self::fail('aksrt_pay_failed', __('订单号无效。', 'aksrt'));
|
||||
}
|
||||
|
||||
$method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||
if ($method === '') {
|
||||
return self::fail('aksrt_pay_failed', __('支付方式无效。', 'aksrt'));
|
||||
}
|
||||
|
||||
$lock = PaymentAttemptLock::acquire($order_number);
|
||||
if ($lock === false) {
|
||||
return self::fail('aksrt_pay_locked', __('该订单正在发起支付,请稍后重试。', 'aksrt'));
|
||||
}
|
||||
|
||||
try {
|
||||
$repo = OrderRepository::get_instance();
|
||||
|
||||
// A pending order remains bound to the gateway selected at creation.
|
||||
// Switching it would make a late callback from the previous gateway look
|
||||
// invalid after that provider may already have collected the money.
|
||||
$stored = $repo->get($order_number);
|
||||
$stored_status = sanitize_key((string) ($stored['status'] ?? ''));
|
||||
if (!in_array($stored_status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
|
||||
return self::fail('aksrt_order_not_payable', __('当前订单状态不可支付。', 'aksrt'));
|
||||
}
|
||||
$stored_method = sanitize_key((string) ($stored['payment_method'] ?? ''));
|
||||
if ($stored_method !== '' && !hash_equals($stored_method, $method)) {
|
||||
return self::fail('aksrt_pay_method_mismatch', __('支付方式与订单不一致,请重新下单。', 'aksrt'));
|
||||
}
|
||||
|
||||
// External providers refuse a second submission that reuses the same
|
||||
// merchant order number with changed params — mint a new one per attempt.
|
||||
if (!in_array($method, self::SYNC_METHODS, true)) {
|
||||
$attempt = $repo->allocate_payment_attempt($order_number);
|
||||
if ($attempt === null) {
|
||||
return self::fail('aksrt_pay_failed', __('无法生成支付单号,请稍后重试。', 'aksrt'));
|
||||
}
|
||||
$order['out_trade_no'] = (string) $attempt['out_trade_no'];
|
||||
$previous = (string) ($attempt['previous'] ?? '');
|
||||
if ($previous !== '' && $previous !== $order['out_trade_no'] && method_exists($gateway, 'close_trade')) {
|
||||
try {
|
||||
$gateway->close_trade($previous);
|
||||
} catch (\Throwable $e) {
|
||||
// Best-effort: the new attempt already uses a fresh number.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = $gateway->create_payment($order);
|
||||
if (empty($result['success'])) {
|
||||
$message = (string) ($result['message'] ?? __('支付失败。', 'aksrt'));
|
||||
return self::fail(self::failure_code($method, $message), $message);
|
||||
}
|
||||
|
||||
$payment_url = (string) ($result['payment_url'] ?? '');
|
||||
$qr_content = (string) ($result['payment_data']['code_url'] ?? '');
|
||||
if ($payment_url !== '' || $qr_content !== '') {
|
||||
// Redirect/scan gateways: the order stays pending until the signed
|
||||
// notify/return callback confirms it. Never pre-mark as paid here.
|
||||
return [
|
||||
'success' => true,
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $qr_content,
|
||||
'out_trade_no' => (string) ($order['out_trade_no'] ?? $order_number),
|
||||
'message' => (string) ($result['message'] ?? __('请在新页面完成支付。', 'aksrt')),
|
||||
];
|
||||
}
|
||||
|
||||
// Synchronous gateway (balance): funds already moved inside create_payment().
|
||||
$transaction_id = sanitize_text_field((string) ($result['transaction_id'] ?? ''));
|
||||
$paid = $repo->update_status($order_number, Order::STATUS_PAID, [
|
||||
'payment_method' => $method,
|
||||
'payment_transaction_id' => $transaction_id,
|
||||
'note' => (string) ($opts['paid_note'] ?? __('用户完成支付。', 'aksrt')),
|
||||
// Require a real status transition: if a callback already moved the
|
||||
// order, this attempt must roll its deduction back instead of
|
||||
// silently reporting success.
|
||||
'require_transition' => true,
|
||||
]);
|
||||
if (!$paid) {
|
||||
$rolled_back = self::rollback($gateway, $order_number, (float) ($order['amount'] ?? 0), $method);
|
||||
return self::fail(
|
||||
'aksrt_order_confirm_failed',
|
||||
$rolled_back
|
||||
? __('订单状态已变化,扣款已退回。', 'aksrt')
|
||||
: __('订单状态已变化,扣款补偿失败,请联系管理员处理。', 'aksrt')
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'redirect' => false,
|
||||
'transaction_id' => $transaction_id,
|
||||
'message' => (string) ($result['message'] ?? __('支付成功。', 'aksrt')),
|
||||
];
|
||||
} finally {
|
||||
PaymentAttemptLock::release($order_number, $lock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve + validate the gateway for a cashier request.
|
||||
*
|
||||
* @return array{gateway:GatewayInterface}|array{error_code:string,message:string}
|
||||
*/
|
||||
public static function resolve_gateway(string $payment_method): array {
|
||||
if (!class_exists('AKSRT\\Payment\\Gateway')) {
|
||||
return ['error_code' => 'aksrt_pay_unavailable', 'message' => __('支付网关不可用。', 'aksrt')];
|
||||
}
|
||||
$gateway = Gateway::get_instance()->get_gateway(sanitize_key($payment_method));
|
||||
if (!$gateway) {
|
||||
return ['error_code' => 'aksrt_gateway_unavailable', 'message' => __('当前支付方式不可用。', 'aksrt')];
|
||||
}
|
||||
if (!$gateway->is_configured()) {
|
||||
return ['error_code' => 'aksrt_gateway_unconfigured', 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')];
|
||||
}
|
||||
return ['gateway' => $gateway];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compensate a synchronous deduction that could not be committed.
|
||||
*/
|
||||
private static function rollback(GatewayInterface $gateway, string $order_number, float $amount, string $method): bool {
|
||||
if ($amount <= 0) {
|
||||
return true;
|
||||
}
|
||||
$request_id = 'PAY_ROLLBACK_' . substr(hash('sha256', $order_number), 0, 24);
|
||||
try {
|
||||
$compensation = $gateway->refund($order_number, $amount, $request_id);
|
||||
} catch (\Throwable $e) {
|
||||
$compensation = ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
if (!empty($compensation['success'])) {
|
||||
return true;
|
||||
}
|
||||
error_log(sprintf(
|
||||
'[AKSRT Payment] rollback failed order=%s gateway=%s: %s',
|
||||
$order_number,
|
||||
$method,
|
||||
(string) ($compensation['message'] ?? 'unknown error')
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function failure_code(string $method, string $message): string {
|
||||
if ($method === 'balance' && (str_contains($message, '余额') || str_contains($message, '不足'))) {
|
||||
return 'aksrt_balance_insufficient';
|
||||
}
|
||||
return 'aksrt_pay_failed';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success:false,code:string,message:string}
|
||||
*/
|
||||
private static function fail(string $code, string $message): array {
|
||||
return ['success' => false, 'code' => $code, 'message' => $message];
|
||||
}
|
||||
}
|
||||
@@ -328,88 +328,36 @@ class UserOrders {
|
||||
return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')];
|
||||
}
|
||||
|
||||
$lock = class_exists(PaymentAttemptLock::class)
|
||||
? PaymentAttemptLock::acquire($order_number)
|
||||
: false;
|
||||
if ($lock === false) {
|
||||
return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')];
|
||||
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
||||
$order['payment_method'] = $payment_method;
|
||||
|
||||
$payment_result = PaymentOrchestrator::pay($gateway, $order, [
|
||||
'paid_note' => __('用户在前台完成支付。', 'aksrt'),
|
||||
]);
|
||||
if (empty($payment_result['success'])) {
|
||||
return ['success' => false, 'message' => $payment_result['message']];
|
||||
}
|
||||
|
||||
try {
|
||||
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
||||
|
||||
$needs_attempt = !in_array($payment_method, ['balance', 'points'], true);
|
||||
if ($needs_attempt) {
|
||||
$attempt = $repo->allocate_payment_attempt($order_number);
|
||||
if ($attempt === null) {
|
||||
return ['success' => false, 'message' => __('无法生成支付单号,请稍后重试。', 'aksrt')];
|
||||
}
|
||||
$order = $repo->get_user_order($user_id, $order_number) ?: $order;
|
||||
$order['out_trade_no'] = (string) $attempt['out_trade_no'];
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
if (empty($payment_result['success'])) {
|
||||
return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')];
|
||||
}
|
||||
|
||||
$code_url = $payment_result['payment_data']['code_url'] ?? '';
|
||||
if (!empty($payment_result['payment_url']) || $code_url !== '') {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) $code_url,
|
||||
'order_number' => $order_number,
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$paid = $repo->update_status($order_number, 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')];
|
||||
}
|
||||
|
||||
if (!empty($payment_result['redirect'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order' => $this->get_order_detail_view($user_id, $order_number),
|
||||
'message' => $payment_result['message'] ?? __('支付成功。', 'aksrt'),
|
||||
'redirect' => true,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
||||
'order_number' => $order_number,
|
||||
'message' => $payment_result['message'],
|
||||
],
|
||||
];
|
||||
} finally {
|
||||
PaymentAttemptLock::release($order_number, $lock);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order' => $this->get_order_detail_view($user_id, $order_number),
|
||||
'message' => $payment_result['message'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function empty_stats(): array {
|
||||
|
||||
@@ -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