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:
@@ -546,9 +546,11 @@ class UsersMenus {
|
||||
if (!empty($apply['msg'])) {
|
||||
echo '<div class="description">' . esc_html((string) $apply['msg']) . '</div>';
|
||||
}
|
||||
foreach ((array) ($apply['img'] ?? []) as $image_url) {
|
||||
$evidence_src = class_exists('\AKSRT\User\AuthApply') ? \AKSRT\User\AuthApply::evidence_src((string) $image_url) : '';
|
||||
echo '<a href="' . esc_attr($evidence_src) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_attr($evidence_src) . '" alt="" style="width:56px;height:56px;object-fit:cover;margin:6px 6px 0 0;border-radius:4px;" /></a>';
|
||||
$evidence_items = class_exists('\AKSRT\User\AuthApply')
|
||||
? \AKSRT\User\AuthApply::get_evidence_items($apply)
|
||||
: [];
|
||||
foreach ($evidence_items as $item) {
|
||||
echo '<a href="' . esc_url((string) $item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url((string) $item['thumbnail_url']) . '" alt="" style="width:56px;height:56px;object-fit:cover;margin:6px 6px 0 0;border-radius:4px;" /></a>';
|
||||
}
|
||||
}
|
||||
if (!empty($row['verify_reason'])) {
|
||||
|
||||
@@ -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)),
|
||||
];
|
||||
|
||||
@@ -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')]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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')]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -449,6 +449,7 @@ class DomainModel {
|
||||
self::ORDER_STATUS_PAID,
|
||||
self::ORDER_STATUS_DELIVERED,
|
||||
self::ORDER_STATUS_COMPLETED,
|
||||
self::ORDER_STATUS_PENDING_REVIEW,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class Balance {
|
||||
* @param array<string,mixed> $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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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')]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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')]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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')]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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 必需)。
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+64
-22
@@ -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 {
|
||||
|
||||
+238
-78
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend identity verification apply (zibll user-auth page parity on aksrt_user_verify_* meta).
|
||||
* Frontend identity verification application.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -85,59 +85,89 @@ class AuthApply {
|
||||
return self::get_status($user_id) === 'verified';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
/** @return array<string,mixed> */
|
||||
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<string,mixed>
|
||||
*/
|
||||
/** @return array<string,mixed> */
|
||||
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<string,mixed> $application
|
||||
* @return array<int,array{id:int,url:string,thumbnail_url:string}>
|
||||
*/
|
||||
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<string,mixed> $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<int,int>|\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<int,array<string,mixed>>|\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<int,int|string> $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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user