Merge pull request 'fix(user-center): complete certification and order flows' (#320) from worktree-fix-user-center-order-certification-flow into main
Reviewed-on: CYL/wordpress-theme-aksrt#320
This commit is contained in:
@@ -22,9 +22,9 @@
|
||||
<n-descriptions-item label="身份简介">{{ detail.request.application?.desc || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="申请说明">{{ detail.request.application?.msg || '-' }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<div v-if="detail.request.application?.img?.length" class="aksrt-verify-drawer__proofs">
|
||||
<a v-for="image in detail.request.application.img" :key="image" :href="image" target="_blank" rel="noopener noreferrer">
|
||||
<img :src="image" alt="认证证明材料" />
|
||||
<div v-if="detail.request.application?.evidence?.length" class="aksrt-verify-drawer__proofs">
|
||||
<a v-for="item in detail.request.application.evidence" :key="item.id" :href="item.url" target="_blank" rel="noopener noreferrer">
|
||||
<img :src="item.thumbnail_url || item.url" alt="认证证明材料" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -522,8 +522,15 @@
|
||||
}
|
||||
|
||||
/* Order detail modal content */
|
||||
html.aksrt-modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aksrt-order-detail-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(520px, 100%);
|
||||
max-height: min(85vh, 760px);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-1, #fff);
|
||||
@@ -557,8 +564,12 @@
|
||||
}
|
||||
|
||||
.aksrt-order-detail-dialog .aksrt-modal__body {
|
||||
min-height: 0;
|
||||
padding: 8px 18px 16px;
|
||||
gap: 14px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* Hero: status + amount */
|
||||
@@ -1413,6 +1424,10 @@ html.dark .aksrt-order-detail-dialog .aksrt-modal__footer {
|
||||
border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 70%, transparent);
|
||||
}
|
||||
|
||||
.aksrt-modal__dialog.aksrt-order-detail-dialog {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aksrt-modal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -399,7 +399,7 @@
|
||||
.uc-auth-card--verified .uc-auth-card__art { background: linear-gradient(150deg, #20a77d, #3287df); }
|
||||
|
||||
.uc-auth-modal { z-index: 620; }
|
||||
.uc-auth-modal .aksrt-modal__dialog { width: min(620px, calc(100vw - 28px)); max-height: min(760px, calc(100vh - 40px)); overflow: auto; }
|
||||
.uc-auth-modal__dialog { width: min(620px, calc(100vw - 28px)); max-width: 620px; max-height: min(760px, calc(100vh - 40px)); overflow: auto; background: var(--surface-1, #fff); }
|
||||
.uc-auth-modal__header > div { display: flex; align-items: center; gap: 10px; }
|
||||
.uc-auth-modal__header > div > span { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 11px; background: var(--color-primary-soft); color: var(--color-primary); }
|
||||
.uc-auth-modal__body { padding: 22px; }
|
||||
@@ -433,5 +433,5 @@
|
||||
.uc-auth-card__art { font-size: 34px; }
|
||||
.uc-auth-card__content { padding: 20px 17px; }
|
||||
.uc-auth-card__content h2 { font-size: 20px; }
|
||||
.uc-auth-modal .aksrt-modal__dialog { position: fixed; inset: auto 0 0; width: 100%; max-height: 88vh; margin: 0; border-radius: 18px 18px 0 0; }
|
||||
.uc-auth-modal__dialog { position: fixed; inset: auto 0 0; width: 100%; max-width: none; max-height: 88vh; margin: 0; border-radius: 18px 18px 0 0; }
|
||||
}
|
||||
|
||||
@@ -262,10 +262,16 @@
|
||||
}
|
||||
if (message) message.textContent = '';
|
||||
fetch(ajaxUrl, { method: 'POST', body: new FormData(form), credentials: 'same-origin' })
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(response) {
|
||||
if (!response || response.error) throw new Error((response && response.msg) || '提交失败');
|
||||
showToast(response.msg || '提交成功');
|
||||
return response.json().then(function(payload) {
|
||||
if (!response.ok || !payload || !payload.success) {
|
||||
throw new Error((payload && payload.data && payload.data.message) || '提交失败');
|
||||
}
|
||||
return payload.data || {};
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
showToast(data.message || '提交成功');
|
||||
window.setTimeout(function() { window.location.assign(tabUrls.auth || '?tab=auth'); }, 500);
|
||||
})
|
||||
.catch(function(error) {
|
||||
|
||||
@@ -17,6 +17,7 @@ $settings = class_exists(AuthApply::class) ? AuthApply::settings() : ['enabled'
|
||||
$status = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_status($user_id) : '';
|
||||
$info = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_auth_info($user_id) : [];
|
||||
$apply = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_apply_payload($user_id) : [];
|
||||
$evidence = class_exists(AuthApply::class) ? AuthApply::get_evidence_items($apply) : [];
|
||||
$type_labels = [];
|
||||
foreach ((array) ($settings['types'] ?? []) as $type) {
|
||||
$type_labels[(string) ($type['slug'] ?? '')] = (string) ($type['label'] ?? '');
|
||||
@@ -64,8 +65,8 @@ $can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) &&
|
||||
|
||||
<?php if ($can_apply) : ?>
|
||||
<div class="aksrt-modal uc-auth-modal" id="uc-auth-modal" aria-hidden="true">
|
||||
<div class="aksrt-modal__backdrop" data-auth-modal-close></div>
|
||||
<div class="aksrt-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="uc-auth-modal-title">
|
||||
<div class="aksrt-modal__overlay" data-auth-modal-close></div>
|
||||
<div class="aksrt-modal__box uc-auth-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="uc-auth-modal-title">
|
||||
<div class="aksrt-modal__header uc-auth-modal__header">
|
||||
<div><span><i class="fas fa-id-badge" aria-hidden="true"></i></span><strong id="uc-auth-modal-title"><?php esc_html_e('申请身份认证', 'aksrt'); ?></strong></div>
|
||||
<button type="button" class="aksrt-modal__close" data-auth-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fas fa-times" aria-hidden="true"></i></button>
|
||||
@@ -84,8 +85,8 @@ $can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) &&
|
||||
<?php if (!empty($apply['msg'])) : ?><dl><dt><?php esc_html_e('申请说明', 'aksrt'); ?></dt><dd><?php echo esc_html((string) $apply['msg']); ?></dd></dl><?php endif; ?>
|
||||
<?php if (!empty($apply['time'])) : ?><dl><dt><?php esc_html_e('申请时间', 'aksrt'); ?></dt><dd><?php echo esc_html((string) $apply['time']); ?></dd></dl><?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($apply['img'])) : ?>
|
||||
<div class="aksrt-user-auth-panel__imgs uc-auth-proof-list"><?php foreach ((array) $apply['img'] as $img_url) : ?><div class="preview-item"><img src="<?php echo esc_attr(AuthApply::evidence_src((string) $img_url)); ?>" alt="" loading="lazy"></div><?php endforeach; ?></div>
|
||||
<?php if ($evidence !== []) : ?>
|
||||
<div class="aksrt-user-auth-panel__imgs uc-auth-proof-list"><?php foreach ($evidence as $item) : ?><div class="preview-item"><img src="<?php echo esc_url((string) $item['thumbnail_url']); ?>" alt="" loading="lazy"></div><?php endforeach; ?></div>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn-primary uc-auth-modal__confirm" data-auth-modal-close><?php esc_html_e('确定', 'aksrt'); ?></button>
|
||||
<?php else : ?>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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