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 @@ - - - - - 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 @@ - - - - - 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 '
'; - echo ''; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - 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 ''; - echo ''; - if (!$items) { - echo ''; - } - foreach ((array) $items as $row) { - $uid = (int) ($row['ID'] ?? 0); - $st = (string) ($row['verify_status'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - $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 ''; - echo ''; - } - echo '
ID' . esc_html__('用户', 'aksrt') . '' . esc_html__('邮箱', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('备注', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无认证申请', 'aksrt') . '
' . esc_html((string) $uid) . '' . esc_html((string) ($row['display_name'] ?? '')) . '' . esc_html((string) ($row['user_email'] ?? '')) . '' . esc_html($st) . ''; - 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 '
'; - if ($st === 'pending') { - foreach (['verified' => __('通过', 'aksrt'), 'rejected' => __('拒绝', 'aksrt')] as $action => $label) { - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_users_review_verify_' . $uid); - if ($action === 'rejected') { - echo ' '; - } - submit_button($label, 'small', '', false); - echo '
'; - } - } else { - 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 '
'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + submit_button(__('筛选', 'aksrt'), 'secondary', '', false); + echo '
'; + } + + /** @param array> $items */ + private function render_verify_table(array $items): void { + $type_labels = $this->get_verify_type_labels(); + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + if ($items === []) { + echo ''; + } + foreach ($items as $row) { + $this->render_verify_row($row, $type_labels); + } + echo '
' . esc_html__('用户', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('申请资料', 'aksrt') . '' . esc_html__('审核记录', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无认证申请', 'aksrt') . '
'; + } + + /** @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 '' . esc_attr__('认证证明材料', 'aksrt') . ''; + } + 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 '
  1. ' . wp_kses_post($this->format_verify_history_entry($entry)) . '
  2. '; + } + 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 ''; + echo ''; + wp_nonce_field('aksrt_users_review_verify_' . $user_id); + echo ''; + echo ''; + echo '
'; + echo '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_users_review_verify_' . $user_id); + echo ''; + echo ''; + echo '
'; + return; + } + + if ($status === 'verified') { + echo '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_users_review_verify_' . $user_id); + echo ''; + echo ''; + 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; }