-
-
+
+
diff --git a/inc/helpers/user-center-renderers/sidebar-orders.php b/inc/helpers/user-center-renderers/sidebar-orders.php
index 316e3aec..b6f898cc 100644
--- a/inc/helpers/user-center-renderers/sidebar-orders.php
+++ b/inc/helpers/user-center-renderers/sidebar-orders.php
@@ -12,9 +12,19 @@ if ($orders_base_url === '') {
return;
}
+$order_user_id = (int) ($user_id ?? 0);
$order_stats = class_exists('\AKSRT\Payment\Order')
- ? \AKSRT\Payment\Order::get_instance()->get_stats((int) ($user_id ?? 0))
+ ? \AKSRT\Payment\Order::get_instance()->get_stats($order_user_id)
: [];
+$order_repo = class_exists('\AKSRT\Payment\OrderRepository')
+ ? \AKSRT\Payment\OrderRepository::get_instance()
+ : null;
+$shop_type = class_exists('\AKSRT\Core\DomainModel')
+ ? \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP
+ : 'shop_product';
+$shop_paid = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PAID, 'product_type' => $shop_type]) : 0;
+$shop_delivered = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'product_type' => $shop_type]) : 0;
+$shop_review = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, 'product_type' => $shop_type]) : 0;
$order_items = [
[
'label' => __('待支付', 'aksrt'),
@@ -26,19 +36,19 @@ $order_items = [
'label' => __('待发货', 'aksrt'),
'icon' => 'gift',
'tab' => 'to-ship',
- 'count' => (int) ($order_stats['paid'] ?? 0),
+ 'count' => $shop_paid,
],
[
'label' => __('待收货', 'aksrt'),
'icon' => 'truck',
'tab' => 'shipped',
- 'count' => (int) ($order_stats['delivered'] ?? 0),
+ 'count' => $shop_delivered,
],
[
'label' => __('待评价', 'aksrt'),
'icon' => 'comment-dots',
'tab' => 'review',
- 'count' => (int) ($order_stats['pending_review'] ?? 0),
+ 'count' => $shop_review,
],
[
'label' => __('售后', 'aksrt'),
diff --git a/src/AKSRT/Admin/Native/UsersMenus.php b/src/AKSRT/Admin/Native/UsersMenus.php
index f3e5128a..ce62f3bc 100644
--- a/src/AKSRT/Admin/Native/UsersMenus.php
+++ b/src/AKSRT/Admin/Native/UsersMenus.php
@@ -546,9 +546,11 @@ class UsersMenus {
if (!empty($apply['msg'])) {
echo '
' . esc_html((string) $apply['msg']) . '
';
}
- foreach ((array) ($apply['img'] ?? []) as $image_url) {
- $evidence_src = class_exists('\AKSRT\User\AuthApply') ? \AKSRT\User\AuthApply::evidence_src((string) $image_url) : '';
- echo '
 . ')
';
+ $evidence_items = class_exists('\AKSRT\User\AuthApply')
+ ? \AKSRT\User\AuthApply::get_evidence_items($apply)
+ : [];
+ foreach ($evidence_items as $item) {
+ echo '
 $item['thumbnail_url']) . ')
';
}
}
if (!empty($row['verify_reason'])) {
diff --git a/src/AKSRT/Admin/UserControllerVerifyIntegrations.php b/src/AKSRT/Admin/UserControllerVerifyIntegrations.php
index 43a0bfc6..335bd42f 100644
--- a/src/AKSRT/Admin/UserControllerVerifyIntegrations.php
+++ b/src/AKSRT/Admin/UserControllerVerifyIntegrations.php
@@ -246,6 +246,9 @@ trait UserControllerVerifyIntegrations {
$email_verified = (bool) get_user_meta($user_id, 'aksrt_email_verified', true);
$application = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
$application = is_array($application) ? $application : [];
+ $application_view = class_exists('\AKSRT\User\AuthApply')
+ ? \AKSRT\User\AuthApply::application_view($application)
+ : [];
return [
'user_id' => $user_id,
@@ -256,7 +259,7 @@ trait UserControllerVerifyIntegrations {
'requested_at' => (string) ($application['time'] ?? $row['user_registered'] ?? ''),
'reviewed_at' => (string) ($row['reviewed_at'] ?? ''),
'email_verified' => $email_verified,
- 'application' => $application,
+ 'application' => $application_view,
'reviewer' => $this->build_admin_user_summary($reviewer_id, $reviewer),
'history_count' => count($this->get_verify_history_for_user($user_id)),
];
diff --git a/src/AKSRT/Content/PayOrders.php b/src/AKSRT/Content/PayOrders.php
index 84886113..b0012039 100644
--- a/src/AKSRT/Content/PayOrders.php
+++ b/src/AKSRT/Content/PayOrders.php
@@ -54,6 +54,13 @@ trait PayOrders {
// Consume coupon + write affiliate commission (cash content only; idempotent).
$this->finalize_content_order_side_effects($order_number, $paid_order);
+ if (class_exists('\AKSRT\Payment\OrderRepository')) {
+ \AKSRT\Payment\OrderRepository::get_instance()->update_status(
+ $order_number,
+ \AKSRT\Payment\Order::STATUS_COMPLETED,
+ ['note' => __('内容权益已发放。', 'aksrt')]
+ );
+ }
}
diff --git a/src/AKSRT/Content/PayRest.php b/src/AKSRT/Content/PayRest.php
index f5b30faa..8461383f 100644
--- a/src/AKSRT/Content/PayRest.php
+++ b/src/AKSRT/Content/PayRest.php
@@ -165,6 +165,9 @@ trait PayRest {
if (!$order) {
return new \WP_Error('aksrt_invalid', __('订单不存在', 'aksrt'), ['status' => 404]);
}
+ if (class_exists('\AKSRT\Payment\Gateway')) {
+ $order = \AKSRT\Payment\Gateway::get_instance()->reconcile_order($order_number, $order) ?: $order;
+ }
$status = (string) ($order['status'] ?? '');
$paid = class_exists('\AKSRT\Core\DomainModel') && \AKSRT\Core\DomainModel::is_paid_order_status($status);
if ($paid && $uid <= 0 && function_exists('aksrt_set_guest_pay_cookies')) {
diff --git a/src/AKSRT/Content/VipServicePurchase.php b/src/AKSRT/Content/VipServicePurchase.php
index fb097758..c0e32572 100644
--- a/src/AKSRT/Content/VipServicePurchase.php
+++ b/src/AKSRT/Content/VipServicePurchase.php
@@ -445,14 +445,38 @@ trait VipServicePurchase {
$level = 2;
}
+ $previous_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
+ $previous_expiry = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
+ $fulfilled = false;
// unit upgrade: raise level only, keep current expiry (zibll).
if ($upgrade_mode === 'unit') {
$this->activate_vip($user_id, $level, 0, true);
+ $fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= $level;
} elseif ($time > 0 || (isset($meta['time']) && (int) $meta['time'] === 0 && $days === 0)) {
// Prefer calendar time+unit when present (including permanent time=0).
$this->activate_vip_duration($user_id, $level, $time, $unit);
+ $new_expiry = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
+ $expected_base = max($previous_expiry, time());
+ $expected_expiry = $time <= 0
+ ? 0
+ : strtotime('+' . $time . ' ' . ($unit === 'month' ? 'months' : 'days'), $expected_base);
+ if ($expected_expiry === false) {
+ $expected_expiry = $expected_base + ($this->duration_to_days($time, $unit) * DAY_IN_SECONDS);
+ }
+ $fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= max($previous_level, $level)
+ && ($new_expiry === 0 || ($time <= 0 ? $new_expiry === 0 : $new_expiry >= $expected_expiry));
} else {
$this->activate_vip($user_id, $level, $days, false);
+ $new_expiry = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
+ $fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= max($previous_level, $level)
+ && ($new_expiry === 0 || $days <= 0 || $new_expiry >= max($previous_expiry, time()) + ($days * DAY_IN_SECONDS));
+ }
+ if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
+ \AKSRT\Payment\OrderRepository::get_instance()->update_status(
+ $order_number,
+ \AKSRT\Payment\Order::STATUS_COMPLETED,
+ ['note' => __('会员权益已发放。', 'aksrt')]
+ );
}
}
diff --git a/src/AKSRT/Core/DomainModel.php b/src/AKSRT/Core/DomainModel.php
index 09c1a9c0..0f65b62b 100644
--- a/src/AKSRT/Core/DomainModel.php
+++ b/src/AKSRT/Core/DomainModel.php
@@ -449,6 +449,7 @@ class DomainModel {
self::ORDER_STATUS_PAID,
self::ORDER_STATUS_DELIVERED,
self::ORDER_STATUS_COMPLETED,
+ self::ORDER_STATUS_PENDING_REVIEW,
];
}
diff --git a/src/AKSRT/Credits/Balance.php b/src/AKSRT/Credits/Balance.php
index a3bee1d4..ece322ea 100644
--- a/src/AKSRT/Credits/Balance.php
+++ b/src/AKSRT/Credits/Balance.php
@@ -32,7 +32,7 @@ class Balance {
* @param array
$ref
* @return float|\WP_Error New balance.
*/
- public static function add(int $user_id, float $delta, string $reason, array $ref = [], string $remark = '') {
+ public static function add(int $user_id, float $delta, string $reason, array $ref = [], string $remark = '', bool $dispatch_hooks = true) {
if ($user_id <= 0) {
return new \WP_Error('aksrt_balance_user', __('用户无效', 'aksrt'));
}
@@ -46,7 +46,9 @@ class Balance {
return new \WP_Error('aksrt_balance_db', __('余额写入失败', 'aksrt'));
}
self::log($user_id, $delta, $new, $reason, $ref, $remark);
- do_action('aksrt_balance_added', $user_id, $delta, $reason, $new);
+ if ($dispatch_hooks) {
+ do_action('aksrt_balance_added', $user_id, $delta, $reason, $new);
+ }
return $new;
}
diff --git a/src/AKSRT/Credits/BalanceServiceCharge.php b/src/AKSRT/Credits/BalanceServiceCharge.php
index 3eaf123b..30c2e8ac 100644
--- a/src/AKSRT/Credits/BalanceServiceCharge.php
+++ b/src/AKSRT/Credits/BalanceServiceCharge.php
@@ -52,26 +52,49 @@ trait BalanceServiceCharge {
if ($points <= 0) {
return;
}
+ global $wpdb;
+ if ($wpdb->query('START TRANSACTION') === false) {
+ return;
+ }
$add = Credits::add(
$user_id,
$points,
'points_buy',
['type' => 'order', 'order' => $order_number],
- sprintf(__('购买积分订单 %s', 'aksrt'), $order_number)
+ sprintf(__('购买积分订单 %s', 'aksrt'), $order_number),
+ false
);
if (is_wp_error($add)) {
+ $wpdb->query('ROLLBACK');
return;
}
$note['points_credited'] = 1;
$note['credited_points'] = $points;
- global $wpdb;
- $wpdb->update(
+ $updated = $wpdb->update(
$wpdb->prefix . 'aksrt_order',
['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)],
['order_number' => $order_number],
['%s'],
['%s']
);
+ if ($updated === false) {
+ $wpdb->query('ROLLBACK');
+ wp_cache_delete($user_id, 'user_meta');
+ return;
+ }
+ if ($wpdb->query('COMMIT') === false) {
+ $wpdb->query('ROLLBACK');
+ wp_cache_delete($user_id, 'user_meta');
+ return;
+ }
+ do_action('aksrt_credits_added', $user_id, $points, 'points_buy', (int) $add);
+ if (class_exists('\AKSRT\Payment\OrderRepository')) {
+ \AKSRT\Payment\OrderRepository::get_instance()->update_status(
+ $order_number,
+ \AKSRT\Payment\Order::STATUS_COMPLETED,
+ ['note' => __('购买积分已到账。', 'aksrt')]
+ );
+ }
return;
}
@@ -101,26 +124,49 @@ trait BalanceServiceCharge {
if ($credit_amount <= 0) {
return;
}
+ global $wpdb;
+ if ($wpdb->query('START TRANSACTION') === false) {
+ return;
+ }
$result = Balance::add(
$user_id,
$credit_amount,
'recharge',
['type' => 'order', 'order' => $order_number],
- sprintf(__('余额充值订单 %s', 'aksrt'), $order_number)
+ sprintf(__('余额充值订单 %s', 'aksrt'), $order_number),
+ false
);
if (is_wp_error($result)) {
+ $wpdb->query('ROLLBACK');
return;
}
$note['balance_credited'] = 1;
$note['credited_amount'] = $credit_amount;
- global $wpdb;
- $wpdb->update(
+ $updated = $wpdb->update(
$wpdb->prefix . 'aksrt_order',
['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)],
['order_number' => $order_number],
['%s'],
['%s']
);
+ if ($updated === false) {
+ $wpdb->query('ROLLBACK');
+ wp_cache_delete($user_id, 'user_meta');
+ return;
+ }
+ if ($wpdb->query('COMMIT') === false) {
+ $wpdb->query('ROLLBACK');
+ wp_cache_delete($user_id, 'user_meta');
+ return;
+ }
+ do_action('aksrt_balance_added', $user_id, $credit_amount, 'recharge', (float) $result);
+ if (class_exists('\AKSRT\Payment\OrderRepository')) {
+ \AKSRT\Payment\OrderRepository::get_instance()->update_status(
+ $order_number,
+ \AKSRT\Payment\Order::STATUS_COMPLETED,
+ ['note' => __('充值金额已到账。', 'aksrt')]
+ );
+ }
}
diff --git a/src/AKSRT/Credits/Credits.php b/src/AKSRT/Credits/Credits.php
index 6638135d..1348fd4f 100644
--- a/src/AKSRT/Credits/Credits.php
+++ b/src/AKSRT/Credits/Credits.php
@@ -37,7 +37,7 @@ class Credits {
* @param array $ref ['type' => 'post|comment|user|invite', 'id' => int]
* @return int|\WP_Error 新余额或错误
*/
- public static function add(int $user_id, int $delta, string $reason, array $ref = [], string $remark = '') {
+ public static function add(int $user_id, int $delta, string $reason, array $ref = [], string $remark = '', bool $dispatch_hooks = true) {
if ($user_id <= 0) return new \WP_Error('aksrt_credits_user', __('用户无效', 'aksrt'));
if ($delta <= 0) return new \WP_Error('aksrt_credits_delta', __('增加积分必须为正数', 'aksrt'));
@@ -47,7 +47,9 @@ class Credits {
}
self::log($user_id, $delta, $new_balance, $reason, $ref, $remark);
- do_action('aksrt_credits_added', $user_id, $delta, $reason, $new_balance);
+ if ($dispatch_hooks) {
+ do_action('aksrt_credits_added', $user_id, $delta, $reason, $new_balance);
+ }
return $new_balance;
}
diff --git a/src/AKSRT/Forum/PlateFollowPay.php b/src/AKSRT/Forum/PlateFollowPay.php
index 05724a0e..ef50b956 100644
--- a/src/AKSRT/Forum/PlateFollowPay.php
+++ b/src/AKSRT/Forum/PlateFollowPay.php
@@ -510,8 +510,12 @@ class PlateFollowPay {
}
}
+ $fulfilled = $user_id <= 0;
if ($user_id > 0) {
$this->mark_followed($user_id, $plate_id);
+ $fulfilled = class_exists('AKSRT\\Forum\\Forum')
+ ? Forum::get_instance()->is_user_following_plate($user_id, $plate_id)
+ : in_array($plate_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_follow_plate', true)), true);
} elseif (function_exists('aksrt_set_guest_pay_cookies')) {
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
@@ -522,6 +526,13 @@ class PlateFollowPay {
aksrt_set_guest_pay_cookies($paid_order);
}
$this->finalize_plate_order_side_effects($order_number, $paid_order);
+ if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
+ \AKSRT\Payment\OrderRepository::get_instance()->update_status(
+ $order_number,
+ \AKSRT\Payment\Order::STATUS_COMPLETED,
+ ['note' => __('付费关注权益已发放。', 'aksrt')]
+ );
+ }
}
/**
diff --git a/src/AKSRT/Forum/TopicPay.php b/src/AKSRT/Forum/TopicPay.php
index 60c68879..ed89f0ae 100644
--- a/src/AKSRT/Forum/TopicPay.php
+++ b/src/AKSRT/Forum/TopicPay.php
@@ -514,8 +514,12 @@ class TopicPay {
}
}
+ $fulfilled = $user_id <= 0;
if ($user_id > 0) {
$this->mark_unlocked($user_id, $topic_id);
+ $fulfilled = class_exists('AKSRT\\Forum\\Forum')
+ ? Forum::get_instance()->has_user_unlocked_topic($user_id, $topic_id)
+ : in_array($topic_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_unlocked_topics', true)), true);
} elseif (function_exists('aksrt_set_guest_pay_cookies')) {
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
@@ -526,6 +530,13 @@ class TopicPay {
aksrt_set_guest_pay_cookies($paid_order);
}
$this->finalize_topic_order_side_effects($order_number, $paid_order);
+ if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
+ \AKSRT\Payment\OrderRepository::get_instance()->update_status(
+ $order_number,
+ \AKSRT\Payment\Order::STATUS_COMPLETED,
+ ['note' => __('付费主题权益已发放。', 'aksrt')]
+ );
+ }
}
/**
diff --git a/src/AKSRT/Payment/Gateway.php b/src/AKSRT/Payment/Gateway.php
index 721d2ca3..d2e28e9e 100644
--- a/src/AKSRT/Payment/Gateway.php
+++ b/src/AKSRT/Payment/Gateway.php
@@ -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|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
));
diff --git a/src/AKSRT/Payment/Gateways/AlipayGateway.php b/src/AKSRT/Payment/Gateways/AlipayGateway.php
index d5d092bf..ed6f0ce4 100644
--- a/src/AKSRT/Payment/Gateways/AlipayGateway.php
+++ b/src/AKSRT/Payment/Gateways/AlipayGateway.php
@@ -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,
];
diff --git a/src/AKSRT/Payment/Gateways/EpayGateway.php b/src/AKSRT/Payment/Gateways/EpayGateway.php
index 84b437ad..370fc833 100644
--- a/src/AKSRT/Payment/Gateways/EpayGateway.php
+++ b/src/AKSRT/Payment/Gateways/EpayGateway.php
@@ -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',
];
diff --git a/src/AKSRT/Payment/Gateways/PayPalGateway.php b/src/AKSRT/Payment/Gateways/PayPalGateway.php
index 2d7814ed..a8f78e7b 100644
--- a/src/AKSRT/Payment/Gateways/PayPalGateway.php
+++ b/src/AKSRT/Payment/Gateways/PayPalGateway.php
@@ -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 $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 必需)。
*/
diff --git a/src/AKSRT/Payment/Gateways/StripeGateway.php b/src/AKSRT/Payment/Gateways/StripeGateway.php
index d84c30e2..c8a88c43 100644
--- a/src/AKSRT/Payment/Gateways/StripeGateway.php
+++ b/src/AKSRT/Payment/Gateways/StripeGateway.php
@@ -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,
];
diff --git a/src/AKSRT/Payment/Gateways/WechatPayGateway.php b/src/AKSRT/Payment/Gateways/WechatPayGateway.php
index 94903a81..5dcf0689 100644
--- a/src/AKSRT/Payment/Gateways/WechatPayGateway.php
+++ b/src/AKSRT/Payment/Gateways/WechatPayGateway.php
@@ -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,
];
diff --git a/src/AKSRT/Payment/UserOrders.php b/src/AKSRT/Payment/UserOrders.php
index d73085ac..231e0925 100644
--- a/src/AKSRT/Payment/UserOrders.php
+++ b/src/AKSRT/Payment/UserOrders.php
@@ -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);
}
diff --git a/src/AKSRT/Shop/Part3.php b/src/AKSRT/Shop/Part3.php
index ef7f2010..cbc0be77 100644
--- a/src/AKSRT/Shop/Part3.php
+++ b/src/AKSRT/Shop/Part3.php
@@ -65,7 +65,7 @@ trait Part3 {
];
}
- public function execute_auto_delivery(int $product_id, string $options_key, int $qty, int $user_id, string $order_number): string {
+ public function execute_auto_delivery(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id = 0): string {
$config = $this->get_auto_delivery_config($product_id);
$type = $config['type'];
@@ -77,7 +77,7 @@ trait Part3 {
return (string) ($config['opts'][$key] ?? $config['content']);
}
if ($type === 'card_pass') {
- return $this->consume_card_keys($product_id, $options_key, $qty, $user_id, $order_number);
+ return $this->consume_card_keys($product_id, $options_key, $qty, $user_id, $order_number, $order_item_id);
}
if ($type === 'invit_code') {
return $this->create_invite_codes($qty, $user_id, $order_number);
@@ -85,10 +85,20 @@ trait Part3 {
return '';
}
- private function consume_card_keys(int $product_id, string $options_key, int $qty, int $user_id, string $order_number): string {
+ private function auto_delivery_succeeded(int $product_id, string $content, int $qty): bool {
+ $config = $this->get_auto_delivery_config($product_id);
+ if (in_array($config['type'], ['card_pass', 'invit_code'], true)) {
+ $lines = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $content) ?: [])));
+ return count($lines) === max(1, $qty)
+ && !in_array($content, [__('卡密已售罄', 'aksrt'), __('邀请系统不可用', 'aksrt')], true);
+ }
+ return trim($content) !== '';
+ }
+
+ private function consume_card_keys(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id): string {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('card_key');
- if ($table === '') return '';
+ if ($table === '' || $order_item_id <= 0 || $wpdb->query('START TRANSACTION') === false) return '';
$where_opt = '';
$params = [$product_id];
@@ -101,28 +111,39 @@ trait Part3 {
$params[] = $qty;
$keys = $wpdb->get_results($wpdb->prepare(
- "SELECT id, card_code FROM {$table} WHERE product_id = %d AND status = 'available'{$where_opt} ORDER BY id ASC LIMIT %d",
+ "SELECT id, card_code FROM {$table} WHERE product_id = %d AND status = 'available'{$where_opt} ORDER BY id ASC LIMIT %d FOR UPDATE",
...$params
), ARRAY_A);
-
- if (empty($keys)) {
+ if (count((array) $keys) !== $qty) {
+ $wpdb->query('ROLLBACK');
return __('卡密已售罄', 'aksrt');
}
$codes = [];
$ids = [];
- foreach ($keys as $k) {
- $codes[] = $k['card_code'];
- $ids[] = (int) $k['id'];
+ foreach ($keys as $key) {
+ $codes[] = (string) $key['card_code'];
+ $ids[] = (int) $key['id'];
}
- if (!empty($ids)) {
- $placeholders = implode(',', array_fill(0, count($ids), '%d'));
- $wpdb->query($wpdb->prepare(
- "UPDATE {$table} SET status = 'used', order_number = %s, used_by = %d, used_at = %s WHERE id IN ({$placeholders})",
- array_merge([$order_number, $user_id, current_time('mysql')], $ids)
- ));
+ $placeholders = implode(',', array_fill(0, count($ids), '%d'));
+ $updated = $wpdb->query($wpdb->prepare(
+ "UPDATE {$table} SET status = 'used', order_number = %s, used_by = %d, used_at = %s WHERE status = 'available' AND id IN ({$placeholders})",
+ array_merge([$order_number, $user_id, current_time('mysql')], $ids)
+ ));
+ $content = implode("\n", $codes);
+ $item_table = $this->order_item_table();
+ $persisted = $item_table !== '' ? $wpdb->update(
+ $item_table,
+ ['auto_delivery_content' => $content],
+ ['id' => $order_item_id, 'order_number' => $order_number],
+ ['%s'],
+ ['%d', '%s']
+ ) : false;
+ if ($updated !== $qty || $persisted === false || $wpdb->query('COMMIT') === false) {
+ $wpdb->query('ROLLBACK');
+ return '';
}
- return implode("\n", $codes);
+ return $content;
}
private function create_invite_codes(int $qty, int $user_id, string $order_number): string {
@@ -221,6 +242,7 @@ trait Part3 {
$user_id = (int) ($order['user_id'] ?? 0);
$auto_delivery_parts = [];
+ $auto_delivery_successes = 0;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
@@ -235,10 +257,16 @@ trait Part3 {
$shipping_type = (string) get_post_meta($pid, '_aksrt_shipping_type', true);
if ($shipping_type === 'auto') {
- $content = $this->execute_auto_delivery($pid, $opt_key, $qty, $user_id, $order_number);
+ $content = $this->execute_auto_delivery($pid, $opt_key, $qty, $user_id, $order_number, (int) $item['id']);
if ($content !== '') {
$auto_delivery_parts[] = $content;
- $this->update_order_item_delivery($item['id'], $content);
+ $config = $this->get_auto_delivery_config($pid);
+ $persisted = $config['type'] === 'card_pass'
+ ? true
+ : $this->update_order_item_delivery($item['id'], $content);
+ if ($persisted && $this->auto_delivery_succeeded($pid, $content, $qty)) {
+ $auto_delivery_successes++;
+ }
}
}
}
@@ -250,14 +278,28 @@ trait Part3 {
$this->send_auto_delivery_notification($user_id, $order_number, $auto_delivery_parts);
}
+ if ($auto_delivery_successes === count($items) && class_exists('\AKSRT\Payment\OrderRepository')) {
+ \AKSRT\Payment\OrderRepository::get_instance()->update_status(
+ $order_number,
+ \AKSRT\Payment\Order::STATUS_COMPLETED,
+ ['note' => __('自动发货订单已完成。', 'aksrt')]
+ );
+ }
+
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
}
- private function update_order_item_delivery(int $item_id, string $content): void {
+ private function update_order_item_delivery(int $item_id, string $content): bool {
global $wpdb;
$table = $this->order_item_table();
- if ($table === '') return;
- $wpdb->update($table, ['auto_delivery_content' => $content], ['id' => $item_id], ['%s'], ['%d']);
+ if ($table === '') return false;
+ $updated = $wpdb->update($table, ['auto_delivery_content' => $content], ['id' => $item_id], ['%s'], ['%d']);
+ if ($updated === false) return false;
+ if ($updated === 0) {
+ $stored = $wpdb->get_var($wpdb->prepare("SELECT auto_delivery_content FROM {$table} WHERE id = %d", $item_id));
+ return is_string($stored) && hash_equals($stored, $content);
+ }
+ return true;
}
private function send_auto_delivery_notification(int $user_id, string $order_number, array $parts): void {
diff --git a/src/AKSRT/User/AuthApply.php b/src/AKSRT/User/AuthApply.php
index 57ed853b..1eeadcb3 100644
--- a/src/AKSRT/User/AuthApply.php
+++ b/src/AKSRT/User/AuthApply.php
@@ -1,6 +1,6 @@
- */
+ /** @return array */
public static function get_auth_info(int $user_id): array {
$info = get_user_meta($user_id, self::INFO_META, true);
return is_array($info) ? $info : [];
}
- /**
- * @return array
- */
+ /** @return array */
public static function get_apply_payload(int $user_id): array {
$apply = get_user_meta($user_id, self::APPLY_META, true);
return is_array($apply) ? $apply : [];
}
- public static function evidence_src(string $value): string {
- if (preg_match('#^data:image/(?:jpeg|png|gif|webp);base64,[A-Za-z0-9+/=]+$#', $value)) {
- return $value;
+ /**
+ * Build the only public representation of certification evidence.
+ *
+ * @param array $application
+ * @return array
+ */
+ public static function get_evidence_items(array $application): array {
+ $items = [];
+ foreach ((array) ($application['evidence_attachment_ids'] ?? []) as $attachment_id) {
+ $attachment_id = (int) $attachment_id;
+ if ($attachment_id <= 0 || get_post_type($attachment_id) !== 'attachment') {
+ continue;
+ }
+ $url = wp_get_attachment_url($attachment_id);
+ if (!is_string($url) || $url === '') {
+ continue;
+ }
+ $thumb = wp_get_attachment_image_url($attachment_id, 'thumbnail');
+ $items[] = [
+ 'id' => $attachment_id,
+ 'url' => esc_url_raw($url),
+ 'thumbnail_url' => esc_url_raw(is_string($thumb) && $thumb !== '' ? $thumb : $url),
+ ];
}
- return esc_url_raw($value);
+ return $items;
+ }
+
+ /** @param array $application */
+ public static function application_view(array $application): array {
+ return [
+ 'name' => (string) ($application['name'] ?? ''),
+ 'desc' => (string) ($application['desc'] ?? ''),
+ 'msg' => (string) ($application['msg'] ?? ''),
+ 'type' => sanitize_key((string) ($application['type'] ?? '')),
+ 'time' => (string) ($application['time'] ?? ''),
+ 'evidence' => self::get_evidence_items($application),
+ ];
}
public function ajax_apply(): void {
+ if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
+ wp_send_json_error(['message' => __('请求方式无效', 'aksrt')], 405);
+ }
if (!is_user_logged_in()) {
- wp_send_json(['error' => 1, 'msg' => __('请先登录', 'aksrt')]);
+ wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
}
if (!check_ajax_referer('aksrt_user_auth_apply', '_wpnonce', false)) {
- wp_send_json(['error' => 1, 'msg' => __('安全校验失败', 'aksrt')]);
+ wp_send_json_error(['message' => __('安全校验失败', 'aksrt')], 403);
}
$user_id = get_current_user_id();
$s = self::settings();
if (!$s['enabled'] || !$s['allow_apply']) {
- wp_send_json(['error' => 1, 'msg' => __('暂不能申请身份认证', 'aksrt')]);
+ wp_send_json_error(['message' => __('暂不能申请身份认证', 'aksrt')], 403);
}
if (self::is_verified($user_id)) {
- wp_send_json(['error' => 1, 'msg' => __('您已是认证用户', 'aksrt')]);
+ wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409);
}
if (self::get_status($user_id) === 'pending') {
- wp_send_json(['error' => 1, 'msg' => __('您的申请正在审核中', 'aksrt')]);
+ wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409);
}
if ($s['limit_level'] > 0 && class_exists('\AKSRT\User\Level')) {
$lv = (int) Level::get_level($user_id);
if ($lv < $s['limit_level']) {
- wp_send_json([
- 'error' => 1,
- 'msg' => sprintf(
+ wp_send_json_error([
+ 'message' => sprintf(
/* translators: %d: required level */
__('等级不足,需达到 LV.%d 后申请', 'aksrt'),
$s['limit_level']
),
- ]);
+ ], 403);
}
}
@@ -146,77 +176,207 @@ class AuthApply {
$msg = isset($_POST['msg']) ? sanitize_textarea_field(wp_unslash((string) $_POST['msg'])) : '';
$type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : '';
$allowed_types = array_column((array) $s['types'], 'slug');
- $name_length = function_exists('mb_strlen') ? mb_strlen($name) : strlen($name);
- if ($name_length < 4 || $name_length > 16) {
- wp_send_json(['error' => 1, 'msg' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')]);
+ if ($this->text_length($name) < 4 || $this->text_length($name) > 16) {
+ wp_send_json_error(['message' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')], 422);
+ }
+ if ($this->text_length($desc) > 300 || $this->text_length($msg) > 500) {
+ wp_send_json_error(['message' => __('认证说明内容过长', 'aksrt')], 422);
}
if ($type === '' || !in_array($type, $allowed_types, true)) {
- wp_send_json(['error' => 1, 'msg' => __('请选择有效的认证类型', 'aksrt')]);
+ wp_send_json_error(['message' => __('请选择有效的认证类型', 'aksrt')], 422);
}
- $images = [];
- if (!empty($_FILES['image']['name'])) {
- $files = $_FILES['image'];
- $names = is_array($files['name']) ? $files['name'] : [$files['name']];
- if (!is_array($files['name'])) {
- $files = [
- 'name' => $names,
- 'type' => [$files['type']],
- 'tmp_name' => [$files['tmp_name']],
- 'error' => [$files['error']],
- 'size' => [$files['size']],
- ];
+ $lock_key = 'aksrt_auth_apply_lock_' . $user_id;
+ $lock_token = wp_generate_uuid4();
+ $lock_payload = ['token' => $lock_token, 'expires' => time() + 300];
+ if (!add_option($lock_key, $lock_payload, '', false)) {
+ $current_lock = get_option($lock_key, []);
+ if (is_array($current_lock) && (int) ($current_lock['expires'] ?? 0) < time()) {
+ delete_option($lock_key);
}
- if (count($files['name']) > 3) {
- wp_send_json(['error' => 1, 'msg' => __('证明材料最多上传 3 张', 'aksrt')]);
- }
- foreach ($files['name'] as $index => $filename) {
- if ($filename === '') {
- continue;
- }
- if ((int) ($files['error'][$index] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
- wp_send_json(['error' => 1, 'msg' => __('证明材料上传失败,请重新选择', 'aksrt')]);
- }
- $size = (int) ($files['size'][$index] ?? 0);
- $tmp_name = (string) ($files['tmp_name'][$index] ?? '');
- if ($size <= 0 || $size > 2 * MB_IN_BYTES || !is_uploaded_file($tmp_name)) {
- wp_send_json(['error' => 1, 'msg' => __('单张证明图片不能超过 2MB', 'aksrt')]);
- }
- $file_check = wp_check_filetype_and_ext($tmp_name, (string) $filename);
- $mime = (string) ($file_check['type'] ?? '');
- if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
- wp_send_json(['error' => 1, 'msg' => __('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt')]);
- }
- $bytes = file_get_contents($tmp_name);
- if (!is_string($bytes) || $bytes === '') {
- wp_send_json(['error' => 1, 'msg' => __('证明材料读取失败', 'aksrt')]);
- }
- $images[] = 'data:' . $mime . ';base64,' . base64_encode($bytes);
+ if (!add_option($lock_key, $lock_payload, '', false)) {
+ wp_send_json_error(['message' => __('认证申请正在提交,请勿重复操作', 'aksrt')], 409);
}
}
+ $uploaded = [];
+ if (self::is_verified($user_id)) {
+ $this->release_submission_lock($lock_key, $lock_token);
+ wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409);
+ }
+ if (self::get_status($user_id) === 'pending') {
+ $this->release_submission_lock($lock_key, $lock_token);
+ wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409);
+ }
+
+ $uploaded = $this->upload_evidence($user_id);
+ if (is_wp_error($uploaded)) {
+ $this->release_submission_lock($lock_key, $lock_token);
+ wp_send_json_error(['message' => $uploaded->get_error_message()], 422);
+ }
+
+ if (!$this->owns_submission_lock($lock_key, $lock_token)) {
+ $this->delete_attachments($uploaded);
+ wp_send_json_error(['message' => __('认证申请提交超时,请重新提交', 'aksrt')], 409);
+ }
+ $this->renew_submission_lock($lock_key, $lock_token);
+
+ $previous_payload = self::get_apply_payload($user_id);
$payload = [
- 'name' => $name,
- 'desc' => $desc,
- 'msg' => $msg,
- 'type' => $type,
- 'img' => $images,
- 'time' => current_time('Y-m-d H:i'),
+ 'name' => $name,
+ 'desc' => $desc,
+ 'msg' => $msg,
+ 'type' => $type,
+ 'evidence_attachment_ids' => $uploaded,
+ 'time' => current_time('Y-m-d H:i'),
];
- update_user_meta($user_id, self::APPLY_META, $payload);
- update_user_meta($user_id, self::STATUS_META, 'pending');
+
+ $payload_saved = update_user_meta($user_id, self::APPLY_META, $payload);
+ if ($payload_saved === false && get_user_meta($user_id, self::APPLY_META, true) !== $payload) {
+ $this->delete_attachments($uploaded);
+ $this->release_submission_lock($lock_key, $lock_token);
+ wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
+ }
+ $status_saved = update_user_meta($user_id, self::STATUS_META, 'pending');
+ if ($status_saved === false && self::get_status($user_id) !== 'pending') {
+ $this->delete_attachments($uploaded);
+ $previous_payload === []
+ ? delete_user_meta($user_id, self::APPLY_META)
+ : update_user_meta($user_id, self::APPLY_META, $previous_payload);
+ $this->release_submission_lock($lock_key, $lock_token);
+ wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
+ }
+
+ if (!$this->owns_submission_lock($lock_key, $lock_token)) {
+ $this->release_submission_lock($lock_key, $lock_token);
+ wp_send_json_error(['message' => __('认证申请已提交,请刷新查看最新状态', 'aksrt')], 409);
+ }
+ $this->renew_submission_lock($lock_key, $lock_token);
delete_user_meta($user_id, self::REASON_META);
+ $this->delete_attachments(array_column(self::get_evidence_items($previous_payload), 'id'));
- /**
- * @param int $user_id
- * @param array $payload
- */
+ /** @param int $user_id @param array $payload */
do_action('aksrt_user_auth_apply_submitted', $user_id, $payload);
+ $this->release_submission_lock($lock_key, $lock_token);
- wp_send_json([
- 'error' => 0,
- 'msg' => __('提交成功,请等待管理员审核', 'aksrt'),
- 'reload' => true,
- ]);
+ wp_send_json_success([
+ 'message' => __('提交成功,请等待管理员审核', 'aksrt'),
+ 'status' => 'pending',
+ ], 201);
+ }
+
+ /** @return array|\WP_Error */
+ private function upload_evidence(int $user_id) {
+ $files = $this->normalize_uploads($_FILES['image'] ?? null);
+ if (is_wp_error($files)) {
+ return $files;
+ }
+ if ($files === []) {
+ return [];
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+
+ $attachment_ids = [];
+ foreach ($files as $file) {
+ $filename = sanitize_file_name((string) ($file['name'] ?? ''));
+ $tmp_name = (string) ($file['tmp_name'] ?? '');
+ $size = (int) ($file['size'] ?? 0);
+ $error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
+ if ($error !== UPLOAD_ERR_OK || $filename === '' || $size <= 0 || $size > 2 * MB_IN_BYTES || !is_uploaded_file($tmp_name)) {
+ $this->delete_attachments($attachment_ids);
+ return new \WP_Error('aksrt_auth_upload_failed', __('证明材料上传失败,单张图片不能超过 2MB', 'aksrt'));
+ }
+ $file_check = wp_check_filetype_and_ext($tmp_name, $filename);
+ $mime = (string) ($file_check['type'] ?? '');
+ if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
+ $this->delete_attachments($attachment_ids);
+ return new \WP_Error('aksrt_auth_upload_type', __('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt'));
+ }
+
+ $attachment_id = media_handle_sideload([
+ 'name' => $filename,
+ 'tmp_name' => $tmp_name,
+ 'type' => $mime,
+ 'error' => UPLOAD_ERR_OK,
+ 'size' => $size,
+ ], 0, __('官方认证证明材料', 'aksrt'));
+ if (is_wp_error($attachment_id)) {
+ $this->delete_attachments($attachment_ids);
+ return new \WP_Error('aksrt_auth_upload_failed', __('证明材料写入媒体库失败', 'aksrt'));
+ }
+ $attachment_id = (int) $attachment_id;
+ $stored_mime = (string) get_post_mime_type($attachment_id);
+ if (!in_array($stored_mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
+ wp_delete_attachment($attachment_id, true);
+ $this->delete_attachments($attachment_ids);
+ return new \WP_Error('aksrt_auth_upload_type', __('证明材料类型校验失败', 'aksrt'));
+ }
+ wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
+ update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id);
+ $attachment_ids[] = $attachment_id;
+ }
+ return $attachment_ids;
+ }
+
+ /** @return array>|\WP_Error */
+ private function normalize_uploads($input) {
+ if (!is_array($input) || empty($input['name'])) {
+ return [];
+ }
+ $names = is_array($input['name']) ? $input['name'] : [$input['name']];
+ if (count($names) > 3) {
+ return new \WP_Error('aksrt_auth_upload_limit', __('证明材料最多上传 3 张', 'aksrt'));
+ }
+ $files = [];
+ foreach ($names as $index => $name) {
+ if ((string) $name === '') {
+ continue;
+ }
+ $files[] = [
+ 'name' => $name,
+ 'type' => is_array($input['type'] ?? null) ? ($input['type'][$index] ?? '') : ($input['type'] ?? ''),
+ 'tmp_name' => is_array($input['tmp_name'] ?? null) ? ($input['tmp_name'][$index] ?? '') : ($input['tmp_name'] ?? ''),
+ 'error' => is_array($input['error'] ?? null) ? ($input['error'][$index] ?? UPLOAD_ERR_NO_FILE) : ($input['error'] ?? UPLOAD_ERR_NO_FILE),
+ 'size' => is_array($input['size'] ?? null) ? ($input['size'][$index] ?? 0) : ($input['size'] ?? 0),
+ ];
+ }
+ return $files;
+ }
+
+ /** @param array $attachment_ids */
+ private function delete_attachments(array $attachment_ids): void {
+ foreach ($attachment_ids as $attachment_id) {
+ $attachment_id = (int) $attachment_id;
+ if ($attachment_id > 0 && get_post_type($attachment_id) === 'attachment') {
+ wp_delete_attachment($attachment_id, true);
+ }
+ }
+ }
+
+ private function owns_submission_lock(string $lock_key, string $lock_token): bool {
+ $current_lock = get_option($lock_key, []);
+ return is_array($current_lock)
+ && (int) ($current_lock['expires'] ?? 0) >= time()
+ && hash_equals((string) ($current_lock['token'] ?? ''), $lock_token);
+ }
+
+ private function renew_submission_lock(string $lock_key, string $lock_token): void {
+ if ($this->owns_submission_lock($lock_key, $lock_token)) {
+ update_option($lock_key, ['token' => $lock_token, 'expires' => time() + 300], false);
+ }
+ }
+
+ private function release_submission_lock(string $lock_key, string $lock_token): void {
+ $current_lock = get_option($lock_key, []);
+ if (is_array($current_lock) && hash_equals((string) ($current_lock['token'] ?? ''), $lock_token)) {
+ delete_option($lock_key);
+ }
+ }
+
+ private function text_length(string $value): int {
+ return function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
}
}