diff --git a/assets/admin/src/api/index.js b/assets/admin/src/api/index.js
index 3f2e33ae..9f0fdc15 100644
--- a/assets/admin/src/api/index.js
+++ b/assets/admin/src/api/index.js
@@ -96,7 +96,7 @@ class ApiClient {
// Dual-signature support:
// request('/forum/overview')
// request({ url: '/aksrt/v1/forum/topics', method: 'GET', params: { limit: 20 } })
- // request({ url: '/aksrt/v1/verify/review', method: 'POST', data: { user_id: 1 } })
+ // request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' })
let endpoint, method, headers, params, body, rest
if (typeof endpointOrConfig === 'object' && endpointOrConfig !== null) {
diff --git a/assets/admin/src/constants/sections/users.js b/assets/admin/src/constants/sections/users.js
index de46f7cc..5496a38e 100644
--- a/assets/admin/src/constants/sections/users.js
+++ b/assets/admin/src/constants/sections/users.js
@@ -63,15 +63,13 @@ export const usersSection = {
canonicalDomain: 'users',
section: 'credits',
}),
- customPage({
+ schemaPage({
key: 'users-verify',
name: 'UsersVerify',
label: '身份认证',
route: '/users/verify',
- component: 'UsersVerify',
- status: 'available',
title: '身份认证',
- desc: '认证展示、申请门槛与审核策略(审核队列在 用户 → 身份认证)。',
+ desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。',
canonicalDomain: 'users',
section: 'verify',
}),
diff --git a/assets/admin/src/pages/users/UsersVerify.vue b/assets/admin/src/pages/users/UsersVerify.vue
deleted file mode 100644
index f4e0cb45..00000000
--- a/assets/admin/src/pages/users/UsersVerify.vue
+++ /dev/null
@@ -1,387 +0,0 @@
-
-
-
-
-
-
-
-
-
- 认证结构:{{ form.user_verify_enabled ? '已启用' : '已关闭' }}
-
-
- 邮箱验证:{{ authSummary.emailVerificationEnabled ? '已启用' : '未启用' }}
-
-
- 待审核:{{ counts.pending }}
-
-
- 已通过:{{ counts.verified }}
-
-
- 已驳回:{{ counts.rejected }}
-
-
-
-
-
-
-
-
- 官方认证必须由用户提交申请并经审核通过;邮箱验证仅用于账户安全,不授予官方认证徽标。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 保存配置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 筛选记录
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue b/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue
deleted file mode 100644
index cba1a0ea..00000000
--- a/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue
+++ /dev/null
@@ -1,210 +0,0 @@
-
-
-
-
-
-
- {{ detail.request.display_name || '-' }}
- {{ detail.request.email || '-' }}
- {{ detail.request.status || '-' }}
-
- {{ detail.request.email_verified ? '已完成' : '未完成' }}
-
- {{ detail.request.requested_at || '-' }}
- {{ detail.request.reviewed_at || '-' }}
-
-
-
-
申请资料
-
- {{ detail.request.application?.type || '-' }}
- {{ detail.request.application?.name || '-' }}
- {{ detail.request.application?.desc || '-' }}
- {{ detail.request.application?.msg || '-' }}
-
-
-
-
-
-
当前备注
-
- {{ detail.request.reason || '暂无备注' }}
-
-
-
-
-
审核历史
-
-
-
- {{ entry.status || '-' }}
- {{ entry.time || '-' }}
-
-
{{ entry.reason || '无备注' }}
-
{{ entry.reviewer_name || '系统' }}
-
-
-
-
-
-
-
执行审核
-
-
-
-
- 提交审核
- 刷新详情
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/assets/admin/src/router/component-loaders/users.js b/assets/admin/src/router/component-loaders/users.js
index f5e91037..9f850d99 100644
--- a/assets/admin/src/router/component-loaders/users.js
+++ b/assets/admin/src/router/component-loaders/users.js
@@ -1,7 +1,6 @@
export const usersPageLoaders = {
UsersFeatures: () => import('@/pages/users/UsersFeatures.vue'),
UsersSignin: () => import('@/pages/users/UsersSignin.vue'),
- UsersVerify: () => import('@/pages/users/UsersVerify.vue'),
UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
UsersWechatTemplate: () => import('@/pages/users/UsersWechatTemplate.vue'),
diff --git a/assets/admin/src/schema/sections/users/verify.js b/assets/admin/src/schema/sections/users/verify.js
index d7f27add..7317aaa8 100644
--- a/assets/admin/src/schema/sections/users/verify.js
+++ b/assets/admin/src/schema/sections/users/verify.js
@@ -2,12 +2,11 @@ export const verifySchema = {
key: 'users.verify',
title: '身份认证',
icon: 'fas fa-id-card',
- description: '配置公开认证徽标、申请门槛与审核策略(审核队列在 用户 → 身份认证 原生菜单)。',
+ description: '配置公开认证徽标、申请类型与申请门槛;审核、驳回和取消认证请前往 WordPress 用户 → 身份认证。',
defaults: {
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_badge_text: '已认证',
- user_verify_require_manual_review: false,
user_verify_allow_apply: true,
user_verify_disable_desc:
'由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
@@ -44,13 +43,6 @@ export const verifySchema = {
title: '审核流程',
icon: 'fas fa-tasks',
fields: [
- {
- key: 'user_verify_require_manual_review',
- label: '保留人工审核位',
- type: 'switch',
- default: false,
- dependsOn: { key: 'user_verify_enabled', value: true },
- },
{
key: 'user_verify_apply_option',
label: '申请类型选项',
diff --git a/inc/helpers/user-center-renderers/profile-auth.php b/inc/helpers/user-center-renderers/profile-auth.php
index 4fc2ed82..14da9724 100644
--- a/inc/helpers/user-center-renderers/profile-auth.php
+++ b/inc/helpers/user-center-renderers/profile-auth.php
@@ -17,7 +17,8 @@ $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) : [];
+$reason = $user_id > 0 && class_exists(AuthApply::class) ? (string) get_user_meta($user_id, AuthApply::REASON_META, true) : '';
+$evidence = class_exists(AuthApply::class) ? AuthApply::get_evidence_items($apply, $user_id) : [];
$type_labels = [];
foreach ((array) ($settings['types'] ?? []) as $type) {
$type_labels[(string) ($type['slug'] ?? '')] = (string) ($type['label'] ?? '');
@@ -54,7 +55,13 @@ $can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) &&
-
+
+
+
+
+
+
+
diff --git a/src/AKSRT/Admin/ControllerBase.php b/src/AKSRT/Admin/ControllerBase.php
index 1cdd46de..c6188678 100644
--- a/src/AKSRT/Admin/ControllerBase.php
+++ b/src/AKSRT/Admin/ControllerBase.php
@@ -68,6 +68,9 @@ abstract class ControllerBase {
case 'string':
$out[$key] = sanitize_text_field($val);
break;
+ case 'textarea':
+ $out[$key] = sanitize_textarea_field($val);
+ break;
case 'html':
$out[$key] = wp_kses_post($val);
break;
diff --git a/src/AKSRT/Admin/Native/UsersMenus.php b/src/AKSRT/Admin/Native/UsersMenus.php
index ce62f3bc..b4071d24 100644
--- a/src/AKSRT/Admin/Native/UsersMenus.php
+++ b/src/AKSRT/Admin/Native/UsersMenus.php
@@ -9,6 +9,8 @@ defined('ABSPATH') || exit;
class UsersMenus {
+ use UsersMenusVerify;
+
public const CAPABILITY = 'manage_options';
private static $instance = null;
@@ -159,7 +161,11 @@ class UsersMenus {
return;
}
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
- echo '' . esc_html($map[$code]) . '
';
+ $message = $map[$code];
+ if ($code === 'verify_failed' && isset($_GET['aksrt_verify_error'])) {
+ $message = sanitize_text_field((string) wp_unslash($_GET['aksrt_verify_error']));
+ }
+ echo '' . esc_html($message) . '
';
}
private function redirect(string $page, string $msg, array $extra = []): void {
@@ -452,200 +458,6 @@ class UsersMenus {
$this->redirect('aksrt_invite_codes', $ok ? 'invite_disabled' : 'invite_failed');
}
- /* ------------------------------------------------------------------
- * Verify
- * ----------------------------------------------------------------*/
-
- private function count_verify_status(string $status): int {
- global $wpdb;
- return (int) $wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_user_verify_status' AND meta_value = %s",
- $status
- ));
- }
-
- public function render_verify_page(): void {
- $this->ensure_cap();
- echo '' . esc_html__('身份认证', 'aksrt') . '
';
- $this->flash();
-
- $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
- $page_num = max(1, (int) ($_GET['paged'] ?? 1));
- $per_page = 20;
- $offset = ($page_num - 1) * $per_page;
-
- echo '
';
-
- global $wpdb;
- $where = "INNER JOIN {$wpdb->usermeta} status ON status.user_id = u.ID AND status.meta_key = 'aksrt_user_verify_status'";
- $params = [];
- $sql_where = '';
- if ($status !== '') {
- $sql_where = ' WHERE status.meta_value = %s';
- $params[] = $status;
- }
-
- $count_sql = "SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u {$where}{$sql_where}";
- $total = $params
- ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
- : (int) $wpdb->get_var($count_sql);
-
- $sql = "SELECT u.ID, u.display_name, u.user_email, u.user_registered, status.meta_value AS verify_status,
- reason.meta_value AS verify_reason
- FROM {$wpdb->users} u
- {$where}
- LEFT JOIN {$wpdb->usermeta} reason ON reason.user_id = u.ID AND reason.meta_key = 'aksrt_user_verify_reason'
- {$sql_where}
- ORDER BY u.user_registered DESC
- LIMIT %d OFFSET %d";
- $params[] = $per_page;
- $params[] = $offset;
- $items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
- $pages = max(1, (int) ceil(max(1, $total) / $per_page));
-
- echo '
';
- echo '| ID | ' . esc_html__('用户', 'aksrt') . ' | ' . esc_html__('邮箱', 'aksrt') . ' | ' . esc_html__('状态', 'aksrt') . ' | ' . esc_html__('备注', 'aksrt') . ' | ' . esc_html__('操作', 'aksrt') . ' | ';
- echo '
';
- if (!$items) {
- echo '| ' . esc_html__('暂无认证申请', 'aksrt') . ' |
';
- }
- foreach ((array) $items as $row) {
- $uid = (int) ($row['ID'] ?? 0);
- $st = (string) ($row['verify_status'] ?? '');
- echo '';
- echo '| ' . esc_html((string) $uid) . ' | ';
- echo '' . esc_html((string) ($row['display_name'] ?? '')) . ' | ';
- echo '' . esc_html((string) ($row['user_email'] ?? '')) . ' | ';
- echo '' . esc_html($st) . ' | ';
- $apply = get_user_meta($uid, 'aksrt_user_verify_apply', true);
- $apply = is_array($apply) ? $apply : [];
- $type_labels = [];
- if (class_exists('\AKSRT\User\AuthApply')) {
- foreach ((array) \AKSRT\User\AuthApply::settings()['types'] as $type) {
- $type_labels[(string) ($type['slug'] ?? '')] = (string) ($type['label'] ?? '');
- }
- }
- echo '';
- if ($apply) {
- $type_key = sanitize_key((string) ($apply['type'] ?? ''));
- echo '' . esc_html((string) ($apply['name'] ?? '')) . '';
- if ($type_key !== '') {
- echo ' ' . esc_html($type_labels[$type_key] ?? $type_key) . ' ';
- }
- if (!empty($apply['desc'])) {
- echo '' . esc_html((string) $apply['desc']) . ' ';
- }
- if (!empty($apply['msg'])) {
- echo '' . esc_html((string) $apply['msg']) . ' ';
- }
- $evidence_items = class_exists('\AKSRT\User\AuthApply')
- ? \AKSRT\User\AuthApply::get_evidence_items($apply)
- : [];
- foreach ($evidence_items as $item) {
- echo ' ';
- }
- }
- if (!empty($row['verify_reason'])) {
- echo '' . esc_html((string) $row['verify_reason']) . ' ';
- }
- echo ' | ';
- echo '';
- if ($st === 'pending') {
- foreach (['verified' => __('通过', 'aksrt'), 'rejected' => __('拒绝', 'aksrt')] as $action => $label) {
- echo '';
- }
- } else {
- echo '—';
- }
- echo ' |
';
- }
- echo '
';
- $this->pagination($pages, $page_num);
- echo '
';
- }
-
- public function handle_review_verify(): void {
- if (!current_user_can(self::CAPABILITY)) {
- wp_die(esc_html__('权限不足', 'aksrt'));
- }
- $user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
- $status = isset($_POST['status']) ? sanitize_key((string) wp_unslash($_POST['status'])) : '';
- $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
- check_admin_referer('aksrt_users_review_verify_' . $user_id);
-
- $ok = false;
- $current_status = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true));
- $application = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
- if (
- $user_id > 0
- && in_array($status, ['verified', 'rejected'], true)
- && get_userdata($user_id)
- && $current_status === 'pending'
- && is_array($application)
- && !empty($application['name'])
- && !empty($application['type'])
- ) {
- update_user_meta($user_id, 'aksrt_user_verify_status', $status);
- update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql'));
- update_user_meta($user_id, 'aksrt_user_verify_reviewer', get_current_user_id());
- if ($note !== '') {
- update_user_meta($user_id, 'aksrt_user_verify_reason', $note);
- } elseif ($status === 'verified') {
- delete_user_meta($user_id, 'aksrt_user_verify_reason');
- }
- // Promote apply payload to public auth info when approved.
- if ($status === 'verified') {
- $apply = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
- if (is_array($apply)) {
- update_user_meta($user_id, 'aksrt_user_verify_info', [
- 'name' => (string) ($apply['name'] ?? ''),
- 'desc' => (string) ($apply['desc'] ?? ''),
- 'type' => (string) ($apply['type'] ?? ''),
- 'time' => current_time('Y-m-d H:i'),
- ]);
- }
- }
- $log = get_option('aksrt_verify_review_log', []);
- $log = is_array($log) ? $log : [];
- $log[] = [
- 'user_id' => $user_id,
- 'status' => $status,
- 'reason' => $note,
- 'reviewer' => get_current_user_id(),
- 'time' => current_time('mysql'),
- ];
- update_option('aksrt_verify_review_log', array_slice($log, -500), false);
- if (class_exists('\AKSRT\Messages\Message')) {
- $message = $status === 'verified'
- ? __('您的官方认证申请已通过审核', 'aksrt')
- : sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note !== '' ? $note : __('请修改资料后重新申请', 'aksrt'));
- \AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
- 'title' => __('官方认证审核结果', 'aksrt'),
- 'target_url' => aksrt_get_user_center_tab_url('auth'),
- ]);
- }
- $ok = true;
- }
- $this->redirect('aksrt_user_auth', $ok ? 'verify_updated' : 'verify_failed', ['status' => 'pending']);
- }
-
/* ------------------------------------------------------------------
* Ban / reports
* ----------------------------------------------------------------*/
diff --git a/src/AKSRT/Admin/Native/UsersMenusVerify.php b/src/AKSRT/Admin/Native/UsersMenusVerify.php
new file mode 100644
index 00000000..67cbd88f
--- /dev/null
+++ b/src/AKSRT/Admin/Native/UsersMenusVerify.php
@@ -0,0 +1,346 @@
+get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_user_verify_status' AND meta_value = %s",
+ $status
+ ));
+ }
+
+ public function render_verify_page(): void {
+ $this->ensure_cap();
+ $filters = $this->get_verify_filters();
+ $page_num = max(1, (int) ($_GET['paged'] ?? 1));
+ $per_page = 20;
+ $result = $this->query_verify_requests($filters, $page_num, $per_page);
+ $counts = $this->get_verify_status_counts();
+
+ echo '' . esc_html__('身份认证', 'aksrt') . '
';
+ $this->flash();
+ echo '
' . esc_html(sprintf(
+ /* translators: 1 pending, 2 verified, 3 rejected, 4 cancelled */
+ __('待审核 %1$d · 已通过 %2$d · 已驳回 %3$d · 已取消 %4$d', 'aksrt'),
+ $counts['pending'],
+ $counts['verified'],
+ $counts['rejected'],
+ $counts['cancelled']
+ )) . '
';
+ $this->render_verify_filters($filters);
+ $this->render_verify_table($result['items']);
+ $this->pagination($result['pages'], $page_num);
+ echo '
';
+ }
+
+ /** @return array{status:string,keyword:string,date_from:string,date_to:string} */
+ private function get_verify_filters(): array {
+ $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
+ if (!in_array($status, ['pending', 'verified', 'rejected', 'cancelled', ''], true)) {
+ $status = 'pending';
+ }
+
+ return [
+ 'status' => $status,
+ 'keyword' => isset($_GET['keyword']) ? sanitize_text_field((string) wp_unslash($_GET['keyword'])) : '',
+ 'date_from' => $this->sanitize_verify_date($_GET['date_from'] ?? ''),
+ 'date_to' => $this->sanitize_verify_date($_GET['date_to'] ?? ''),
+ ];
+ }
+
+ private function sanitize_verify_date($value): string {
+ $value = sanitize_text_field((string) wp_unslash($value));
+ return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) ? $value : '';
+ }
+
+ /** @return array{pending:int,verified:int,rejected:int,cancelled:int,total:int} */
+ private function get_verify_status_counts(): array {
+ global $wpdb;
+ $counts = ['pending' => 0, 'verified' => 0, 'rejected' => 0, 'cancelled' => 0, 'total' => 0];
+ $rows = $wpdb->get_results(
+ "SELECT meta_value AS status, COUNT(*) AS total
+ FROM {$wpdb->usermeta}
+ WHERE meta_key = 'aksrt_user_verify_status'
+ GROUP BY meta_value",
+ ARRAY_A
+ );
+ foreach ((array) $rows as $row) {
+ $status = sanitize_key((string) ($row['status'] ?? ''));
+ $total = (int) ($row['total'] ?? 0);
+ if (isset($counts[$status])) {
+ $counts[$status] = $total;
+ }
+ $counts['total'] += $total;
+ }
+ return $counts;
+ }
+
+ /**
+ * @param array{status:string,keyword:string,date_from:string,date_to:string} $filters
+ * @return array{items:array>,pages:int,total:int}
+ */
+ private function query_verify_requests(array $filters, int $page_num, int $per_page): array {
+ global $wpdb;
+ $where = ["status.meta_value IN ('pending','verified','rejected','cancelled')"];
+ $params = [];
+
+ if ($filters['status'] !== '') {
+ $where[] = 'status.meta_value = %s';
+ $params[] = $filters['status'];
+ }
+ if ($filters['keyword'] !== '') {
+ $like = '%' . $wpdb->esc_like($filters['keyword']) . '%';
+ $where[] = '(u.display_name LIKE %s OR u.user_login LIKE %s OR u.user_email LIKE %s OR apply.meta_value LIKE %s)';
+ array_push($params, $like, $like, $like, $like);
+ }
+ if ($filters['date_from'] !== '') {
+ $where[] = 'COALESCE(requested_at.meta_value, u.user_registered) >= %s';
+ $params[] = $filters['date_from'] . ' 00:00';
+ }
+ if ($filters['date_to'] !== '') {
+ $where[] = 'COALESCE(requested_at.meta_value, u.user_registered) <= %s';
+ $params[] = $filters['date_to'] . ' 23:59';
+ }
+
+ $joins = "INNER JOIN {$wpdb->usermeta} status
+ ON status.user_id = u.ID AND status.meta_key = 'aksrt_user_verify_status'
+ INNER JOIN {$wpdb->usermeta} apply
+ ON apply.user_id = u.ID AND apply.meta_key = 'aksrt_user_verify_apply'
+ LEFT JOIN {$wpdb->usermeta} requested_at
+ ON requested_at.user_id = u.ID AND requested_at.meta_key = 'aksrt_user_verify_requested_at'
+ LEFT JOIN {$wpdb->usermeta} reason
+ ON reason.user_id = u.ID AND reason.meta_key = 'aksrt_user_verify_reason'
+ LEFT JOIN {$wpdb->usermeta} reviewed_at
+ ON reviewed_at.user_id = u.ID AND reviewed_at.meta_key = 'aksrt_user_verify_reviewed_at'
+ LEFT JOIN {$wpdb->usermeta} reviewer
+ ON reviewer.user_id = u.ID AND reviewer.meta_key = 'aksrt_user_verify_reviewer'";
+ $where_sql = ' WHERE ' . implode(' AND ', $where);
+ $count_sql = "SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u {$joins}{$where_sql}";
+ $total = $params
+ ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
+ : (int) $wpdb->get_var($count_sql);
+
+ $query_params = $params;
+ $query_params[] = $per_page;
+ $query_params[] = ($page_num - 1) * $per_page;
+ $sql = "SELECT DISTINCT u.ID, u.display_name, u.user_login, u.user_email,
+ status.meta_value AS verify_status, apply.meta_value AS verify_apply,
+ requested_at.meta_value AS requested_at, reason.meta_value AS verify_reason,
+ reviewed_at.meta_value AS reviewed_at, reviewer.meta_value AS reviewer_id
+ FROM {$wpdb->users} u {$joins}{$where_sql}
+ ORDER BY COALESCE(requested_at.meta_value, u.user_registered) DESC, u.ID DESC
+ LIMIT %d OFFSET %d";
+ $items = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
+
+ return [
+ 'items' => is_array($items) ? $items : [],
+ 'pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
+ 'total' => $total,
+ ];
+ }
+
+ /** @param array{status:string,keyword:string,date_from:string,date_to:string} $filters */
+ private function render_verify_filters(array $filters): void {
+ echo '';
+ }
+
+ /** @param array> $items */
+ private function render_verify_table(array $items): void {
+ $type_labels = $this->get_verify_type_labels();
+ echo '';
+ echo '| ' . esc_html__('用户', 'aksrt') . ' | ';
+ echo '' . esc_html__('状态', 'aksrt') . ' | ';
+ echo '' . esc_html__('申请资料', 'aksrt') . ' | ';
+ echo '' . esc_html__('审核记录', 'aksrt') . ' | ';
+ echo '' . esc_html__('操作', 'aksrt') . ' | ';
+ echo '
';
+ if ($items === []) {
+ echo '| ' . esc_html__('暂无认证申请', 'aksrt') . ' |
';
+ }
+ foreach ($items as $row) {
+ $this->render_verify_row($row, $type_labels);
+ }
+ echo '
';
+ }
+
+ /** @return array */
+ private function get_verify_type_labels(): array {
+ $labels = [];
+ if (class_exists('\AKSRT\User\AuthApply')) {
+ foreach ((array) \AKSRT\User\AuthApply::settings()['types'] as $type) {
+ $labels[(string) ($type['slug'] ?? '')] = (string) ($type['label'] ?? '');
+ }
+ }
+ return $labels;
+ }
+
+ /** @param array $row @param array $type_labels */
+ private function render_verify_row(array $row, array $type_labels): void {
+ $user_id = (int) ($row['ID'] ?? 0);
+ $status = sanitize_key((string) ($row['verify_status'] ?? ''));
+ $apply = maybe_unserialize($row['verify_apply'] ?? []);
+ $apply = is_array($apply) ? $apply : [];
+ $history = $this->get_verify_history($user_id);
+ $reviewer_id = (int) ($row['reviewer_id'] ?? 0);
+ $reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null;
+
+ echo '';
+ echo '| ' . esc_html((string) ($row['display_name'] ?? '')) . '';
+ echo ' @' . esc_html((string) ($row['user_login'] ?? '')) . ' · UID ' . esc_html((string) $user_id) . ' ';
+ echo ' | ';
+ echo '' . $this->verify_status_badge($status) . ' | ';
+ echo '';
+ $type_key = sanitize_key((string) ($apply['type'] ?? ''));
+ echo '' . esc_html((string) ($apply['name'] ?? __('未填写认证身份', 'aksrt'))) . '';
+ echo ' ' . esc_html__('类型:', 'aksrt') . esc_html($type_labels[$type_key] ?? $type_key ?: '—') . ' ';
+ if (!empty($apply['desc'])) {
+ echo '' . esc_html((string) $apply['desc']) . ' ';
+ }
+ if (!empty($apply['msg'])) {
+ echo '' . esc_html__('申请说明:', 'aksrt') . esc_html((string) $apply['msg']) . ' ';
+ }
+ echo '' . esc_html__('申请时间:', 'aksrt') . esc_html((string) ($row['requested_at'] ?: ($apply['time'] ?? '—'))) . ' ';
+ $evidence = class_exists('\AKSRT\User\AuthApply') ? \AKSRT\User\AuthApply::get_evidence_items($apply, $user_id, true) : [];
+ foreach ($evidence as $item) {
+ echo ' ';
+ }
+ echo ' | ';
+ echo '';
+ if (!empty($row['reviewed_at'])) {
+ echo ' ' . esc_html((string) $row['reviewed_at']) . ' ';
+ }
+ if ($reviewer) {
+ echo '' . esc_html__('审核人:', 'aksrt') . esc_html($reviewer->display_name) . ' ';
+ }
+ if (!empty($row['verify_reason'])) {
+ echo '' . esc_html((string) $row['verify_reason']) . ' ';
+ }
+ if ($history !== []) {
+ echo '' . esc_html(sprintf(__('历史记录(%d)', 'aksrt'), count($history))) . '';
+ foreach ($history as $entry) {
+ echo '- ' . wp_kses_post($this->format_verify_history_entry($entry)) . '
';
+ }
+ echo ' ';
+ }
+ echo ' | ';
+ $this->render_verify_actions($user_id, $status);
+ echo ' |
';
+ }
+
+ private function verify_status_badge(string $status): string {
+ $labels = [
+ 'pending' => __('待审核', 'aksrt'),
+ 'verified' => __('已通过', 'aksrt'),
+ 'rejected' => __('已驳回', 'aksrt'),
+ 'cancelled' => __('已取消', 'aksrt'),
+ ];
+ $colors = ['pending' => '#996800', 'verified' => '#008a20', 'rejected' => '#b32d2e', 'cancelled' => '#646970'];
+ return '' . esc_html($labels[$status] ?? $status) . '';
+ }
+
+ /** @param array $entry */
+ private function format_verify_history_entry(array $entry): string {
+ $reviewer_id = (int) ($entry['reviewer'] ?? 0);
+ $reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null;
+ $parts = [
+ esc_html((string) ($entry['time'] ?? '')),
+ $this->verify_status_badge(sanitize_key((string) ($entry['status'] ?? ''))),
+ ];
+ if ($reviewer) {
+ $parts[] = esc_html($reviewer->display_name);
+ }
+ if (!empty($entry['reason'])) {
+ $parts[] = esc_html((string) $entry['reason']);
+ }
+ return implode(' · ', $parts);
+ }
+
+ private function render_verify_actions(int $user_id, string $status): void {
+ if ($status === 'pending') {
+ echo '';
+ echo '';
+ return;
+ }
+
+ if ($status === 'verified') {
+ echo '';
+ return;
+ }
+
+ echo '' . esc_html__('用户可在前台重新提交申请', 'aksrt') . '';
+ }
+
+ public function handle_review_verify(): void {
+ if (!current_user_can(self::CAPABILITY)) {
+ wp_die(esc_html__('权限不足', 'aksrt'));
+ }
+ $user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
+ $status = isset($_POST['status']) ? sanitize_key((string) wp_unslash($_POST['status'])) : '';
+ $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
+ check_admin_referer('aksrt_users_review_verify_' . $user_id);
+
+ $result = class_exists('\AKSRT\User\AuthApply')
+ ? \AKSRT\User\AuthApply::review($user_id, $status, $note, get_current_user_id())
+ : new \WP_Error('aksrt_verify_unavailable', __('认证服务不可用', 'aksrt'));
+ $redirect_status = $status === 'cancelled' ? 'cancelled' : 'pending';
+ $extra = ['status' => $redirect_status];
+ if (is_wp_error($result)) {
+ $extra['aksrt_verify_error'] = $result->get_error_message();
+ }
+ $this->redirect(
+ 'aksrt_user_auth',
+ is_wp_error($result) ? 'verify_failed' : 'verify_updated',
+ $extra
+ );
+ }
+
+ /** @return array> */
+ private function get_verify_history(int $user_id): array {
+ if (!class_exists('\AKSRT\User\AuthApply')) {
+ return [];
+ }
+ return \AKSRT\User\AuthApply::get_review_history($user_id);
+ }
+}
diff --git a/src/AKSRT/Admin/Schema/SchemaUsers.php b/src/AKSRT/Admin/Schema/SchemaUsers.php
index 4ca861a4..2029db1f 100644
--- a/src/AKSRT/Admin/Schema/SchemaUsers.php
+++ b/src/AKSRT/Admin/Schema/SchemaUsers.php
@@ -599,8 +599,7 @@ class SchemaUsers {
['key' => 'user_verify_public_display', 'label' => '公开页显示认证徽标', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_badge_text', 'label' => '认证徽标文案', 'type' => 'input', 'default' => '已认证'],
]],
- ['key' => 'workflow', 'title' => '审核流程', 'fields' => [
- ['key' => 'user_verify_require_manual_review', 'label' => '保留人工审核位', 'type' => 'switch', 'default' => false],
+ ['key' => 'workflow', 'title' => '申请类型', 'fields' => [
['key' => 'user_verify_apply_option', 'label' => '申请类型选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证", 'desc' => '每行:slug|显示名称'],
]],
['key' => 'apply', 'title' => '申请设置', 'fields' => [
diff --git a/src/AKSRT/Admin/SchemaProvider.php b/src/AKSRT/Admin/SchemaProvider.php
index 72ba7f42..ebc0cd68 100644
--- a/src/AKSRT/Admin/SchemaProvider.php
+++ b/src/AKSRT/Admin/SchemaProvider.php
@@ -394,6 +394,8 @@ class SchemaProvider {
switch ($type) {
case 'html':
return wp_kses_post((string) $value);
+ case 'textarea':
+ return sanitize_textarea_field((string) $value);
case 'code':
return is_string($value) ? wp_unslash($value) : '';
case 'url':
@@ -433,7 +435,7 @@ class SchemaProvider {
'switch' => 'bool',
'input' => 'string',
'text' => 'string',
- 'textarea' => 'string',
+ 'textarea' => 'textarea',
'number' => 'int',
'slider' => 'int',
'select' => 'string',
diff --git a/src/AKSRT/Admin/UserController.php b/src/AKSRT/Admin/UserController.php
index 15b602d4..b35fbd82 100644
--- a/src/AKSRT/Admin/UserController.php
+++ b/src/AKSRT/Admin/UserController.php
@@ -253,35 +253,6 @@ class UserController extends ControllerBase {
'permission_callback' => function () { return is_user_logged_in(); },
]);
- // Verify: list pending verification requests
- register_rest_route(self::API_NAMESPACE, '/verify/pending', [
- 'methods' => 'GET',
- 'callback' => [$this, 'get_verify_pending'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
- register_rest_route(self::API_NAMESPACE, '/verify/requests', [
- 'methods' => 'GET',
- 'callback' => [$this, 'get_verify_requests'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
- register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P\d+)', [
- 'methods' => 'GET',
- 'callback' => [$this, 'get_verify_request_detail'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
-
- // Verify: approve or reject a verification request
- register_rest_route(self::API_NAMESPACE, '/verify/review', [
- 'methods' => 'POST',
- 'callback' => [$this, 'review_verify_request'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
- register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P\d+)/review', [
- 'methods' => 'POST',
- 'callback' => [$this, 'review_verify_request_by_id'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
-
// SMS provider status
register_rest_route(self::API_NAMESPACE, '/sms/status', [
'methods' => 'GET',
diff --git a/src/AKSRT/Admin/UserControllerVerifyIntegrations.php b/src/AKSRT/Admin/UserControllerVerifyIntegrations.php
index 335bd42f..363f6e92 100644
--- a/src/AKSRT/Admin/UserControllerVerifyIntegrations.php
+++ b/src/AKSRT/Admin/UserControllerVerifyIntegrations.php
@@ -9,311 +9,6 @@ defined('ABSPATH') || exit;
trait UserControllerVerifyIntegrations {
- public function get_verify_pending(): \WP_REST_Response {
- $response = $this->get_verify_requests(new \WP_REST_Request('GET', '/'));
- $data = $response->get_data();
- $items = isset($data['data']['items']) && is_array($data['data']['items']) ? $data['data']['items'] : [];
- $pending = array_values(array_filter($items, static function ($item) {
- return ($item['status'] ?? '') === 'pending';
- }));
-
- return $this->success([
- 'data' => [
- 'requests' => array_slice($pending, 0, 50),
- 'total' => count($pending),
- ],
- ]);
- }
-
- public function get_verify_requests(\WP_REST_Request $request): \WP_REST_Response {
- global $wpdb;
-
- $page = max(1, (int) ($request->get_param('page') ?: 1));
- $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
- $keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
- $status = sanitize_key((string) $request->get_param('status'));
- $date_from = sanitize_text_field((string) $request->get_param('date_from'));
- $date_to = sanitize_text_field((string) $request->get_param('date_to'));
-
- $sql = "
- SELECT u.ID, u.display_name, u.user_email, u.user_registered,
- status.meta_value AS verify_status,
- reason.meta_value AS verify_reason,
- reviewed_at.meta_value AS reviewed_at,
- reviewer.meta_value AS reviewer_id
- FROM {$wpdb->users} u
- INNER JOIN {$wpdb->usermeta} status
- ON status.user_id = u.ID AND status.meta_key = 'aksrt_user_verify_status'
- LEFT JOIN {$wpdb->usermeta} reason
- ON reason.user_id = u.ID AND reason.meta_key = 'aksrt_user_verify_reason'
- LEFT JOIN {$wpdb->usermeta} reviewed_at
- ON reviewed_at.user_id = u.ID AND reviewed_at.meta_key = 'aksrt_user_verify_reviewed_at'
- LEFT JOIN {$wpdb->usermeta} reviewer
- ON reviewer.user_id = u.ID AND reviewer.meta_key = 'aksrt_user_verify_reviewer'
- ORDER BY u.user_registered DESC
- LIMIT 500
- ";
-
- $rows = $wpdb->get_results($sql, ARRAY_A);
-
- $items = [];
- $counts = ['pending' => 0, 'verified' => 0, 'rejected' => 0, 'total' => 0];
-
- foreach ((array) $rows as $row) {
- $item = $this->build_verify_request_item($row);
- $item_status = $item['status'];
- if (isset($counts[$item_status])) {
- $counts[$item_status]++;
- }
- $counts['total']++;
-
- if ($status !== '' && $item_status !== $status) {
- continue;
- }
-
- if ($date_from !== '' && strcmp((string) $item['requested_at'], $date_from . ' 00:00:00') < 0) {
- continue;
- }
- if ($date_to !== '' && strcmp((string) $item['requested_at'], $date_to . ' 23:59:59') > 0) {
- continue;
- }
- if ($keyword !== '' && mb_stripos(implode(' ', [
- (string) ($item['display_name'] ?? ''),
- (string) ($item['email'] ?? ''),
- (string) ($item['reason'] ?? ''),
- ]), $keyword) === false) {
- continue;
- }
-
- $items[] = $item;
- }
-
- $total = count($items);
- $offset = ($page - 1) * $per_page;
-
- return $this->success([
- 'data' => [
- 'items' => array_values(array_slice($items, $offset, $per_page)),
- 'total' => $total,
- 'page' => $page,
- 'per_page' => $per_page,
- 'counts' => $counts,
- 'options' => [
- 'statuses' => [
- ['label' => __('待审核', 'aksrt'), 'value' => 'pending'],
- ['label' => __('已通过', 'aksrt'), 'value' => 'verified'],
- ['label' => __('已驳回', 'aksrt'), 'value' => 'rejected'],
- ],
- ],
- ],
- ]);
- }
-
- public function get_verify_request_detail(\WP_REST_Request $request): \WP_REST_Response {
- $user_id = (int) $request->get_param('id');
- if ($user_id <= 0) {
- return $this->error(__('认证申请不存在', 'aksrt'), 404);
- }
-
- $user = get_userdata($user_id);
- if (!$user) {
- return $this->error(__('用户不存在', 'aksrt'), 404);
- }
-
- $detail = $this->build_verify_request_item([
- 'ID' => $user->ID,
- 'display_name' => $user->display_name,
- 'user_email' => $user->user_email,
- 'user_registered' => $user->user_registered,
- 'verify_status' => get_user_meta($user_id, 'aksrt_user_verify_status', true),
- 'verify_reason' => get_user_meta($user_id, 'aksrt_user_verify_reason', true),
- 'reviewed_at' => get_user_meta($user_id, 'aksrt_user_verify_reviewed_at', true),
- 'reviewer_id' => get_user_meta($user_id, 'aksrt_user_verify_reviewer', true),
- ]);
-
- return $this->success([
- 'data' => [
- 'request' => $detail,
- 'history' => $this->get_verify_history_for_user($user_id),
- 'options' => [
- 'statuses' => [
- ['label' => __('通过认证', 'aksrt'), 'value' => 'verified'],
- ['label' => __('驳回申请', 'aksrt'), 'value' => 'rejected'],
- ],
- ],
- ],
- ]);
- }
-
- /**
- * POST /verify/review — approve or reject a verification request.
- */
- public function review_verify_request(\WP_REST_Request $request): \WP_REST_Response {
- $params = $this->get_json_params($request);
- $user_id = (int) ($params['user_id'] ?? 0);
- if ($user_id <= 0) {
- return $this->error(__('参数无效', 'aksrt'));
- }
-
- return $this->perform_verify_review($user_id, $params);
- }
-
- public function review_verify_request_by_id(\WP_REST_Request $request): \WP_REST_Response {
- $user_id = (int) $request->get_param('id');
- if ($user_id <= 0) {
- return $this->error(__('参数无效', 'aksrt'));
- }
-
- return $this->perform_verify_review($user_id, $this->get_json_params($request));
- }
-
- private function perform_verify_review(int $user_id, array $payload): \WP_REST_Response {
- $status = sanitize_key((string) ($payload['status'] ?? ''));
- $note = sanitize_textarea_field((string) ($payload['note'] ?? ($payload['reason'] ?? '')));
-
- if (!in_array($status, ['verified', 'rejected'], true)) {
- return $this->error(__('审核状态无效', 'aksrt'));
- }
-
- $user = get_userdata($user_id);
- if (!$user) {
- return $this->error(__('用户不存在', 'aksrt'), 404);
- }
- $current_status = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true));
- $application = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
- if ($current_status !== 'pending' || !is_array($application) || empty($application['name']) || empty($application['type'])) {
- return $this->error(__('该用户没有可审核的认证申请', 'aksrt'), 409);
- }
-
- update_user_meta($user_id, 'aksrt_user_verify_status', $status);
- update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql'));
- update_user_meta($user_id, 'aksrt_user_verify_reviewer', get_current_user_id());
-
- if ($note !== '') {
- update_user_meta($user_id, 'aksrt_user_verify_reason', $note);
- } elseif ($status === 'verified') {
- delete_user_meta($user_id, 'aksrt_user_verify_reason');
- }
- if ($status === 'verified') {
- $apply = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
- if (is_array($apply)) {
- update_user_meta($user_id, 'aksrt_user_verify_info', [
- 'name' => (string) ($apply['name'] ?? ''),
- 'desc' => (string) ($apply['desc'] ?? ''),
- 'type' => (string) ($apply['type'] ?? ''),
- 'time' => current_time('Y-m-d H:i'),
- ]);
- }
- }
-
- $log = get_option('aksrt_verify_review_log', []);
- if (!is_array($log)) {
- $log = [];
- }
- $log[] = [
- 'user_id' => $user_id,
- 'reviewer' => get_current_user_id(),
- 'status' => $status,
- 'reason' => $note,
- 'time' => current_time('mysql'),
- ];
- if (count($log) > 500) {
- $log = array_slice($log, -500);
- }
- update_option('aksrt_verify_review_log', $log, false);
- if (class_exists('\AKSRT\Messages\Message')) {
- $message = $status === 'verified'
- ? __('您的官方认证申请已通过审核', 'aksrt')
- : sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note !== '' ? $note : __('请修改资料后重新申请', 'aksrt'));
- \AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
- 'title' => __('官方认证审核结果', 'aksrt'),
- 'target_url' => aksrt_get_user_center_tab_url('auth'),
- ]);
- }
-
- return $this->success([
- 'message' => $status === 'verified'
- ? __('已通过认证', 'aksrt')
- : __('已驳回申请', 'aksrt'),
- ]);
- }
-
- private function build_verify_request_item(array $row): array {
- $user_id = (int) ($row['ID'] ?? 0);
- $reviewer_id = (int) ($row['reviewer_id'] ?? 0);
- $reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null;
- $status = sanitize_key((string) ($row['verify_status'] ?? 'pending'));
- $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,
- 'display_name' => (string) ($row['display_name'] ?? ''),
- 'email' => (string) ($row['user_email'] ?? ''),
- 'status' => $status,
- 'reason' => (string) ($row['verify_reason'] ?? ''),
- 'requested_at' => (string) ($application['time'] ?? $row['user_registered'] ?? ''),
- 'reviewed_at' => (string) ($row['reviewed_at'] ?? ''),
- 'email_verified' => $email_verified,
- 'application' => $application_view,
- 'reviewer' => $this->build_admin_user_summary($reviewer_id, $reviewer),
- 'history_count' => count($this->get_verify_history_for_user($user_id)),
- ];
- }
-
- private function get_verify_history_for_user(int $user_id): array {
- $log = get_option('aksrt_verify_review_log', []);
- if (!is_array($log)) {
- return [];
- }
-
- $history = [];
- foreach ($log as $entry) {
- if ((int) ($entry['user_id'] ?? 0) !== $user_id) {
- continue;
- }
-
- $reviewer_id = (int) ($entry['reviewer'] ?? 0);
- $history[] = [
- 'status' => sanitize_key((string) ($entry['status'] ?? '')),
- 'reason' => (string) ($entry['reason'] ?? ''),
- 'time' => (string) ($entry['time'] ?? ''),
- 'reviewer' => $reviewer_id,
- 'reviewer_name' => $reviewer_id > 0 && ($user = get_userdata($reviewer_id)) ? $user->display_name : '',
- ];
- }
-
- usort($history, static function ($left, $right) {
- return strcmp((string) ($right['time'] ?? ''), (string) ($left['time'] ?? ''));
- });
-
- return $history;
- }
-
- private function build_admin_user_summary(int $user_id, $user = null): ?array {
- if ($user_id <= 0 && !$user instanceof \WP_User) {
- return null;
- }
-
- if (!$user instanceof \WP_User) {
- $user = get_userdata($user_id);
- }
- if (!$user instanceof \WP_User) {
- return null;
- }
-
- return [
- 'id' => (int) $user->ID,
- 'display_name' => (string) $user->display_name,
- 'email' => (string) $user->user_email,
- 'avatar_url' => get_avatar_url((int) $user->ID, ['size' => 64]),
- ];
- }
-
/**
* GET /sms/status — SMS provider connection status.
*/
diff --git a/src/AKSRT/Settings/UserRuntime.php b/src/AKSRT/Settings/UserRuntime.php
index 46b9b14a..06bd7a9d 100644
--- a/src/AKSRT/Settings/UserRuntime.php
+++ b/src/AKSRT/Settings/UserRuntime.php
@@ -142,7 +142,7 @@ class UserRuntime {
* User identity, level, medal, and report governance settings.
*
* @return array{
- * verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,require_manual_review:bool,allow_apply:bool,disable_desc:string,limit_level:int,desc:string,apply_option:string},
+ * verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,allow_apply:bool,disable_desc:string,limit_level:int,desc:string,apply_option:string},
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array},
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array}>,items_by_slug:array},
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array},
@@ -162,7 +162,6 @@ class UserRuntime {
'public_display' => !isset($settings['user_verify_public_display']) || !empty($settings['user_verify_public_display']),
'badge_text' => $verify_badge_text !== '' ? $verify_badge_text : __('已认证', 'aksrt'),
'accept_email_verified' => false,
- 'require_manual_review' => !empty($settings['user_verify_require_manual_review']),
'allow_apply' => !isset($settings['user_verify_allow_apply']) || !empty($settings['user_verify_allow_apply']),
'disable_desc' => (string) ($settings['user_verify_disable_desc'] ?? ''),
'limit_level' => max(0, (int) ($settings['user_verify_limit_level'] ?? 0)),
diff --git a/src/AKSRT/User/AuthApply.php b/src/AKSRT/User/AuthApply.php
index 1eeadcb3..40869395 100644
--- a/src/AKSRT/User/AuthApply.php
+++ b/src/AKSRT/User/AuthApply.php
@@ -11,10 +11,14 @@ defined('ABSPATH') || exit;
class AuthApply {
- public const STATUS_META = 'aksrt_user_verify_status';
- public const REASON_META = 'aksrt_user_verify_reason';
- public const APPLY_META = 'aksrt_user_verify_apply';
- public const INFO_META = 'aksrt_user_verify_info';
+ public const STATUS_META = 'aksrt_user_verify_status';
+ public const REASON_META = 'aksrt_user_verify_reason';
+ public const APPLY_META = 'aksrt_user_verify_apply';
+ public const INFO_META = 'aksrt_user_verify_info';
+ public const REQUESTED_AT_META = 'aksrt_user_verify_requested_at';
+ public const REVIEWED_AT_META = 'aksrt_user_verify_reviewed_at';
+ public const REVIEWER_META = 'aksrt_user_verify_reviewer';
+ public const HISTORY_META = 'aksrt_user_verify_history';
private static $instance = null;
@@ -27,6 +31,7 @@ class AuthApply {
public function init(): void {
add_action('wp_ajax_aksrt_user_auth_apply', [$this, 'ajax_apply']);
+ add_action('admin_post_aksrt_verify_evidence', [$this, 'serve_evidence']);
}
/**
@@ -98,44 +103,209 @@ class AuthApply {
}
/**
- * Build the only public representation of certification evidence.
+ * Return certification evidence only to its owner or an administrator.
*
* @param array $application
* @return array
*/
- public static function get_evidence_items(array $application): array {
+ public static function get_evidence_items(array $application, int $owner_id = 0, bool $admin_context = false): array {
+ if ($owner_id <= 0) {
+ return [];
+ }
+ $can_view = $admin_context
+ ? current_user_can('manage_options')
+ : get_current_user_id() === $owner_id;
+ if (!$can_view) {
+ return [];
+ }
+
$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') {
+ if (
+ $attachment_id <= 0
+ || get_post_type($attachment_id) !== 'attachment'
+ || (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true) !== $owner_id
+ ) {
continue;
}
- $url = wp_get_attachment_url($attachment_id);
- if (!is_string($url) || $url === '') {
- continue;
- }
- $thumb = wp_get_attachment_image_url($attachment_id, 'thumbnail');
+ $url = add_query_arg([
+ 'action' => 'aksrt_verify_evidence',
+ 'attachment' => $attachment_id,
+ '_wpnonce' => wp_create_nonce('aksrt_verify_evidence_' . $attachment_id),
+ ], admin_url('admin-post.php'));
$items[] = [
'id' => $attachment_id,
'url' => esc_url_raw($url),
- 'thumbnail_url' => esc_url_raw(is_string($thumb) && $thumb !== '' ? $thumb : $url),
+ 'thumbnail_url' => esc_url_raw($url),
];
}
return $items;
}
+ public function serve_evidence(): void {
+ if (!is_user_logged_in()) {
+ wp_die(esc_html__('请先登录', 'aksrt'), '', ['response' => 401]);
+ }
+ $attachment_id = isset($_GET['attachment']) ? (int) $_GET['attachment'] : 0;
+ if ($attachment_id <= 0 || !check_admin_referer('aksrt_verify_evidence_' . $attachment_id)) {
+ wp_die(esc_html__('安全校验失败', 'aksrt'), '', ['response' => 403]);
+ }
+ $owner_id = (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true);
+ if ($owner_id <= 0 || (get_current_user_id() !== $owner_id && !current_user_can('manage_options'))) {
+ wp_die(esc_html__('您没有权限查看该证明材料', 'aksrt'), '', ['response' => 403]);
+ }
+ $file_path = get_attached_file($attachment_id);
+ if (!is_string($file_path) || $file_path === '' || !is_file($file_path) || !is_readable($file_path)) {
+ wp_die(esc_html__('证明材料不存在', 'aksrt'), '', ['response' => 404]);
+ }
+ $mime = (string) get_post_mime_type($attachment_id);
+ if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
+ wp_die(esc_html__('证明材料类型无效', 'aksrt'), '', ['response' => 415]);
+ }
+
+ nocache_headers();
+ header('Content-Type: ' . $mime);
+ header('Content-Disposition: inline; filename="' . rawurlencode(basename($file_path)) . '"');
+ header('Content-Length: ' . (string) filesize($file_path));
+ readfile($file_path);
+ exit;
+ }
+
/** @param array $application */
- public static function application_view(array $application): array {
+ public static function application_view(array $application, int $owner_id = 0, bool $admin_context = false): 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),
+ 'evidence' => self::get_evidence_items($application, $owner_id, $admin_context),
];
}
+ /** @return array> */
+ public static function get_review_history(int $user_id): array {
+ $history = get_user_meta($user_id, self::HISTORY_META, true);
+ $history = is_array($history) ? array_values($history) : [];
+ usort($history, static fn(array $left, array $right): int => strcmp(
+ (string) ($right['time'] ?? ''),
+ (string) ($left['time'] ?? '')
+ ));
+ return $history;
+ }
+
+ /**
+ * Apply one authoritative certification status transition.
+ *
+ * @return array|\WP_Error
+ */
+ public static function review(int $user_id, string $status, string $note, int $reviewer_id) {
+ $status = sanitize_key($status);
+ $note = sanitize_textarea_field($note);
+ if (!in_array($status, ['verified', 'rejected', 'cancelled'], true)) {
+ return new \WP_Error('aksrt_verify_status', __('审核状态无效', 'aksrt'));
+ }
+ if ($user_id <= 0 || !get_userdata($user_id) || $reviewer_id <= 0) {
+ return new \WP_Error('aksrt_verify_user', __('用户不存在', 'aksrt'));
+ }
+ if ($status === 'cancelled' && $note === '') {
+ return new \WP_Error('aksrt_verify_note', __('取消认证时必须填写原因', 'aksrt'));
+ }
+ if ($status === 'rejected' && $note === '') {
+ return new \WP_Error('aksrt_verify_note', __('驳回申请时必须填写原因', 'aksrt'));
+ }
+
+ $current = self::get_status($user_id);
+ $allowed = $status === 'cancelled'
+ ? $current === 'verified'
+ : $current === 'pending';
+ $application = self::get_apply_payload($user_id);
+ if (!$allowed || $application === [] || empty($application['name']) || empty($application['type'])) {
+ return new \WP_Error('aksrt_verify_transition', __('该认证记录当前不可执行此操作', 'aksrt'));
+ }
+
+ global $wpdb;
+ $rows = $wpdb->get_results($wpdb->prepare(
+ "SELECT umeta_id, meta_value
+ FROM {$wpdb->usermeta}
+ WHERE user_id = %d AND meta_key = %s
+ ORDER BY umeta_id ASC",
+ $user_id,
+ self::STATUS_META
+ ), ARRAY_A);
+ if (count((array) $rows) !== 1 || sanitize_key((string) ($rows[0]['meta_value'] ?? '')) !== $current) {
+ return new \WP_Error('aksrt_verify_state', __('认证状态数据异常,请修复后重试', 'aksrt'));
+ }
+ $updated = $wpdb->query($wpdb->prepare(
+ "UPDATE {$wpdb->usermeta}
+ SET meta_value = %s
+ WHERE umeta_id = %d AND meta_value = %s",
+ $status,
+ (int) $rows[0]['umeta_id'],
+ $current
+ ));
+ if ($updated !== 1) {
+ return new \WP_Error('aksrt_verify_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'));
+ }
+
+ $reviewed_at = current_time('mysql');
+ update_user_meta($user_id, self::REVIEWED_AT_META, $reviewed_at);
+ update_user_meta($user_id, self::REVIEWER_META, $reviewer_id);
+ if ($note !== '') {
+ update_user_meta($user_id, self::REASON_META, $note);
+ } else {
+ delete_user_meta($user_id, self::REASON_META);
+ }
+
+ if ($status === 'verified') {
+ update_user_meta($user_id, self::INFO_META, [
+ 'name' => (string) ($application['name'] ?? ''),
+ 'desc' => (string) ($application['desc'] ?? ''),
+ 'type' => sanitize_key((string) ($application['type'] ?? '')),
+ 'time' => current_time('Y-m-d H:i'),
+ ]);
+ } elseif ($status === 'cancelled') {
+ delete_user_meta($user_id, self::INFO_META);
+ }
+
+ $entry = [
+ 'status' => $status,
+ 'reason' => $note,
+ 'reviewer' => $reviewer_id,
+ 'time' => $reviewed_at,
+ 'application' => self::application_view($application),
+ ];
+ $history = self::get_review_history($user_id);
+ array_unshift($history, $entry);
+ update_user_meta($user_id, self::HISTORY_META, array_slice($history, 0, 50));
+
+ self::send_review_notification($user_id, $status, $note);
+ do_action('aksrt_user_auth_reviewed', $user_id, $status, $note, $reviewer_id);
+
+ return ['status' => $status, 'reviewed_at' => $reviewed_at];
+ }
+
+ private static function send_review_notification(int $user_id, string $status, string $note): void {
+ if (!class_exists('\\AKSRT\\Messages\\Message')) {
+ return;
+ }
+ if ($status === 'verified') {
+ $message = __('您的官方认证申请已通过审核', 'aksrt');
+ $title = __('官方认证审核结果', 'aksrt');
+ } elseif ($status === 'cancelled') {
+ $message = sprintf(__('您的官方认证已被取消:%s', 'aksrt'), $note);
+ $title = __('官方认证已取消', 'aksrt');
+ } else {
+ $message = sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note);
+ $title = __('官方认证审核结果', 'aksrt');
+ }
+ \AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
+ 'title' => $title,
+ 'target_url' => aksrt_get_user_center_tab_url('auth'),
+ ]);
+ }
+
public function ajax_apply(): void {
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
wp_send_json_error(['message' => __('请求方式无效', 'aksrt')], 405);
@@ -222,13 +392,14 @@ class AuthApply {
$this->renew_submission_lock($lock_key, $lock_token);
$previous_payload = self::get_apply_payload($user_id);
+ $requested_at = current_time('Y-m-d H:i');
$payload = [
'name' => $name,
'desc' => $desc,
'msg' => $msg,
'type' => $type,
'evidence_attachment_ids' => $uploaded,
- 'time' => current_time('Y-m-d H:i'),
+ 'time' => $requested_at,
];
$payload_saved = update_user_meta($user_id, self::APPLY_META, $payload);
@@ -246,14 +417,17 @@ class AuthApply {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
+ update_user_meta($user_id, self::REQUESTED_AT_META, $requested_at);
+ delete_user_meta($user_id, self::REVIEWED_AT_META);
+ delete_user_meta($user_id, self::REVIEWER_META);
+ delete_user_meta($user_id, self::REASON_META);
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'));
+ $this->delete_attachments(array_column(self::get_evidence_items($previous_payload, $user_id), 'id'));
/** @param int $user_id @param array $payload */
do_action('aksrt_user_auth_apply_submitted', $user_id, $payload);
@@ -314,7 +488,11 @@ class AuthApply {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_type', __('证明材料类型校验失败', 'aksrt'));
}
- wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
+ wp_update_post([
+ 'ID' => $attachment_id,
+ 'post_author' => $user_id,
+ 'post_status' => 'private',
+ ]);
update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id);
$attachment_ids[] = $attachment_id;
}