fix(user-center): complete certification and order flows

Fix modal scroll ownership and certification modal styling, store evidence as Media Library attachments, reconcile missed provider callbacks, validate currencies, and complete digital fulfillment only after durable entitlement delivery.
This commit is contained in:
kate522 committed 2026-07-29 10:29:59 +08:00
1 parent b945c6e835
commit 815e83bac0
26 files changed
+613 -148

No files matched your search

+62 -1
View File
@@ -243,6 +243,60 @@ class Gateway {
return sanitize_text_field((string) ($order['order_number'] ?? $provider_ref));
}
/**
* Reconcile a pending external order against the configured provider.
*
* @param array<string,mixed>|null $order
*/
public function reconcile_order(string $order_number, ?array $order = null): ?array {
if ($order_number === '' || !class_exists('AKSRT\\Payment\\OrderRepository')) {
return $order;
}
$repo = OrderRepository::get_instance();
$order = is_array($order) ? $order : $repo->get($order_number);
if (!$order || !in_array((string) ($order['status'] ?? ''), [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) {
return $order;
}
$gateway_id = sanitize_key((string) ($order['payment_method'] ?? ''));
if ($gateway_id === '' || in_array($gateway_id, ['balance', 'points', 'manual_review'], true)) {
return $order;
}
$gateway = $this->get_gateway($gateway_id);
if (!$gateway || !$gateway->is_configured()) {
return $order;
}
$throttle_key = 'aksrt_reconcile_' . hash('sha256', $order_number);
if (get_transient($throttle_key)) {
return $order;
}
set_transient($throttle_key, 1, 5);
$lock = PaymentAttemptLock::acquire('reconcile_' . $order_number);
if ($lock === false) {
return $repo->get($order_number) ?: $order;
}
try {
$provider_ref = OrderRepository::provider_out_trade_no($order);
$query = $gateway->query_status($provider_ref);
if (!empty($query['success']) && ($query['status'] ?? '') === 'paid') {
if (empty($query['order_number'])) {
$query['order_number'] = $provider_ref;
}
$this->complete_order($gateway_id, $query);
}
} catch (\Throwable $e) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf('[AKSRT Payment] reconcile failed order=%s gateway=%s: %s', $order_number, $gateway_id, $e->getMessage()));
}
} finally {
PaymentAttemptLock::release('reconcile_' . $order_number, $lock);
}
return $repo->get($order_number) ?: $order;
}
/**
* 幂等、带金额校验的订单完成。仅由验签通过的回调调用。
*
@@ -273,20 +327,27 @@ class Gateway {
}
// Provider-confirmed external payments must include an exact amount and transaction ID.
$expected_currency = strtoupper((string) ($order['currency'] ?? ''));
$paid_currency = strtoupper(sanitize_key((string) ($verify['currency'] ?? '')));
$expected = (float) ($order['amount'] ?? 0);
$paid_amount = (float) ($verify['amount'] ?? 0);
$transaction_id = sanitize_text_field((string) ($verify['transaction_id'] ?? ''));
if ($expected <= 0
|| $paid_amount <= 0
|| $transaction_id === ''
|| $expected_currency === ''
|| $paid_currency === ''
|| !hash_equals($expected_currency, $paid_currency)
|| abs($expected - $paid_amount) > 0.01) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf(
'[AKSRT Payment] invalid completion order=%s out_trade_no=%s expected=%.2f paid=%.2f transaction=%s gateway=%s',
'[AKSRT Payment] invalid completion order=%s out_trade_no=%s expected=%.2f %s paid=%.2f %s transaction=%s gateway=%s',
$order_number,
$provider_out_trade_no,
$expected,
$expected_currency,
$paid_amount,
$paid_currency,
$transaction_id !== '' ? 'present' : 'missing',
$gateway_id
));
@@ -119,6 +119,7 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
'success' => true,
'order_number' => $order_number,
'amount' => (float) ($data['total_amount'] ?? 0),
'currency' => 'CNY',
'transaction_id' => $trade_no,
'status' => $paid ? 'paid' : 'pending',
];
@@ -171,6 +172,7 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
// Keep provider out_trade_no for Gateway::complete_order resolve.
'order_number' => (string) ($data['out_trade_no'] ?? $out_trade_no),
'amount' => (float) ($data['total_amount'] ?? 0),
'currency' => 'CNY',
'transaction_id' => (string) ($data['trade_no'] ?? ''),
'status' => $status,
];
@@ -165,6 +165,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
'success' => true,
'order_number' => $order_number,
'amount' => $amount,
'currency' => 'CNY',
'transaction_id' => $transaction_id,
'status' => $paid ? 'paid' : 'pending',
];
@@ -215,6 +216,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
'success' => true,
'order_number' => (string) ($body['out_trade_no'] ?? $out_trade_no),
'amount' => $amount,
'currency' => 'CNY',
'transaction_id' => $tx,
'status' => $paid ? 'paid' : 'pending',
];
+54 -15
View File
@@ -42,7 +42,11 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
}
$amount = (float) ($order['amount'] ?? 0);
$subject = $order['subject'] ?? __('订单支付', 'aksrt');
$currency = $config['currency'] ?? 'USD';
$order_currency = strtoupper((string) ($order['currency'] ?? ''));
$currency = strtoupper((string) ($config['currency'] ?? 'USD'));
if ($order_currency === '' || !hash_equals($order_currency, $currency)) {
return ['success' => false, 'message' => __('PayPal 币种与订单币种不一致', 'aksrt')];
}
try {
$response = $this->api_request('POST', '/v2/checkout/orders', [
@@ -73,6 +77,15 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
}
}
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$persisted = \AKSRT\Payment\OrderRepository::get_instance()->merge_meta($order_number, [
'paypal_order_id' => (string) ($data['id'] ?? ''),
]);
if (!$persisted) {
return ['success' => false, 'message' => __('PayPal 支付单保存失败,请重试', 'aksrt')];
}
}
return [
'success' => true,
'payment_url' => $approveUrl,
@@ -102,11 +115,31 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
}
public function query_status(string $order_number): array {
return [
'success' => true,
'status' => 'pending',
'message' => __('PayPal 状态通过 Webhook 更新', 'aksrt'),
];
if (!$this->is_configured() || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'status' => 'pending', 'message' => __('PayPal 未配置', 'aksrt')];
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->resolve_by_out_trade_no($order_number);
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$paypal_order_id = sanitize_text_field((string) ($meta['paypal_order_id'] ?? ''));
if ($paypal_order_id === '') {
return ['success' => true, 'status' => 'pending', 'message' => __('PayPal 支付尚未建立', 'aksrt')];
}
try {
$response = $this->api_request('GET', '/v2/checkout/orders/' . rawurlencode($paypal_order_id));
$body = $response['body'];
if ($response['status'] !== 200 || !is_array($body)) {
return ['success' => false, 'status' => 'pending', 'message' => __('PayPal 状态查询失败', 'aksrt')];
}
if (($body['status'] ?? '') === 'APPROVED') {
return $this->capture_order($paypal_order_id);
}
if (($body['status'] ?? '') === 'COMPLETED') {
return $this->completed_order_evidence($body);
}
return ['success' => true, 'status' => 'pending', 'message' => __('PayPal 支付处理中', 'aksrt')];
} catch (\Throwable $e) {
return ['success' => false, 'status' => 'pending', 'message' => $e->getMessage()];
}
}
public function refund(string $order_number, float $amount, string $request_id = ''): array {
@@ -146,15 +179,7 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
$body = $response['body'];
if (in_array($response['status'], [200, 201], true) && ($body['status'] ?? '') === 'COMPLETED') {
$unit = $body['purchase_units'][0] ?? [];
$capture = $unit['payments']['captures'][0] ?? [];
return [
'success' => true,
'order_number' => $unit['reference_id'] ?? '',
'amount' => (float) ($capture['amount']['value'] ?? 0),
'transaction_id' => $capture['id'] ?? ($body['id'] ?? ''),
'status' => 'paid',
];
return $this->completed_order_evidence($body);
}
return ['success' => false, 'message' => $body['message'] ?? __('PayPal 捕获失败', 'aksrt')];
@@ -163,6 +188,20 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
}
}
/** @param array<string,mixed> $body */
private function completed_order_evidence(array $body): array {
$unit = $body['purchase_units'][0] ?? [];
$capture = $unit['payments']['captures'][0] ?? [];
return [
'success' => true,
'order_number' => (string) ($unit['reference_id'] ?? ''),
'amount' => (float) ($capture['amount']['value'] ?? 0),
'currency' => strtoupper((string) ($capture['amount']['currency_code'] ?? '')),
'transaction_id' => (string) ($capture['id'] ?? ($body['id'] ?? '')),
'status' => 'paid',
];
}
/**
* 获取 OAuth2 access token(v2 REST 必需)。
*/
+9 -1
View File
@@ -23,7 +23,9 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
return false;
}
$config = $this->get_config();
return !empty($config['secret_key']) && !empty($config['publishable_key']);
return !empty($config['secret_key'])
&& !empty($config['publishable_key'])
&& !empty($config['webhook_secret']);
}
public function create_payment(array $order): array {
@@ -42,7 +44,11 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
}
$amount = (float) ($order['amount'] ?? 0);
$subject = $order['subject'] ?? __('订单支付', 'aksrt');
$order_currency = strtoupper((string) ($order['currency'] ?? ''));
$currency = strtolower($config['currency'] ?? 'usd');
if ($order_currency === '' || !hash_equals($order_currency, strtoupper($currency))) {
return ['success' => false, 'message' => __('Stripe 币种与订单币种不一致', 'aksrt')];
}
try {
$stripe = $this->create_client($config);
@@ -145,6 +151,7 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
'success' => true,
'order_number' => $orderNumber,
'amount' => $amount,
'currency' => strtoupper($currency),
'transaction_id' => $session->payment_intent ?? $session->id,
'status' => $session->payment_status === 'paid' ? 'paid' : 'pending',
];
@@ -185,6 +192,7 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
(int) ($found->amount_total ?? 0),
strtolower((string) ($found->currency ?? $config['currency'] ?? 'usd'))
),
'currency' => strtoupper((string) ($found->currency ?? $config['currency'] ?? 'usd')),
'status' => $status,
'transaction_id' => $transaction_id,
];
@@ -179,6 +179,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
'success' => true,
'order_number' => $order_number,
'amount' => $total_fen > 0 ? ($total_fen / 100) : 0.0,
'currency' => strtoupper((string) YansongdaPayFactory::dig($data, 'amount.currency', 'CNY')),
'transaction_id' => $transaction_id,
'status' => $paid ? 'paid' : 'pending',
];
@@ -242,6 +243,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
// Keep provider out_trade_no so Gateway::complete_order can resolve the local order.
'order_number' => (string) ($data['out_trade_no'] ?? $out_trade_no),
'amount' => ((int) YansongdaPayFactory::dig($data, 'amount.total', 0)) / 100,
'currency' => strtoupper((string) YansongdaPayFactory::dig($data, 'amount.currency', 'CNY')),
'transaction_id' => (string) ($data['transaction_id'] ?? ''),
'status' => $status,
];
+6 -1
View File
@@ -52,7 +52,9 @@ class UserOrders {
if ($status !== '') {
$repo_args['status'] = $status;
}
if ($product_type !== '' && $product_type !== 'all') {
if (in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_PENDING_REVIEW], true)) {
$repo_args['product_type'] = \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP;
} elseif ($product_type !== '' && $product_type !== 'all') {
$repo_args['product_type'] = $product_type;
}
@@ -83,6 +85,9 @@ class UserOrders {
if (!$order) {
return null;
}
if (class_exists(Gateway::class)) {
$order = Gateway::get_instance()->reconcile_order($order_number, $order) ?: $order;
}
return $this->decorate_order_view($order, true);
}