} */ public static function settings(): array { $identity = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_user_identity_settings() : []; return is_array($identity['verify'] ?? null) ? $identity['verify'] : []; } /** @return array */ public static function type_settings(string $type): array { $types = self::settings()['types'] ?? []; return is_array($types[sanitize_key($type)] ?? null) ? $types[sanitize_key($type)] : []; } public static function application_count(int $user_id, string $type): int { $counts = get_user_meta($user_id, self::APPLICATION_COUNTS_META, true); $counts = is_array($counts) ? $counts : []; return max(0, (int) ($counts[sanitize_key($type)] ?? 0)); } private static function increment_application_count(int $user_id, string $type): void { $counts = get_user_meta($user_id, self::APPLICATION_COUNTS_META, true); $counts = is_array($counts) ? $counts : []; $type = sanitize_key($type); $counts[$type] = max(0, (int) ($counts[$type] ?? 0)) + 1; update_user_meta($user_id, self::APPLICATION_COUNTS_META, $counts); } public static function get_status(int $user_id): string { if ($user_id <= 0) { return ''; } return sanitize_key((string) get_user_meta($user_id, self::STATUS_META, true)); } public static function is_verified(int $user_id): bool { return self::get_status($user_id) === 'verified'; } public function add_admin_user_column(array $columns): array { $columns['aksrt_identity_status'] = __('身份认证', 'aksrt'); return $columns; } public function render_admin_user_column(string $output, string $column_name, int $user_id): string { if ($column_name !== 'aksrt_identity_status') { return $output; } $status = self::get_status($user_id); $labels = self::admin_status_labels(); if ($status === '' || !isset($labels[$status])) { return '' . esc_html__('未认证', 'aksrt') . ''; } $colors = ['pending' => '#996800', 'verified' => '#008a20', 'rejected' => '#b32d2e', 'cancelled' => '#646970']; $html = '' . esc_html($labels[$status]) . ''; $info = self::get_auth_info($user_id); if ($status === 'verified' && !empty($info['name'])) { $html .= '
' . esc_html((string) $info['name']) . '
'; } if ($status === 'pending') { $html .= '
' . esc_html__('查看申请', 'aksrt') . '
'; } return $html; } public function render_admin_profile_status(\WP_User $user): void { $status = self::get_status((int) $user->ID); $labels = self::admin_status_labels(); $apply = self::get_apply_payload((int) $user->ID); $info = self::get_auth_info((int) $user->ID); $reason = (string) get_user_meta((int) $user->ID, self::REASON_META, true); $reviewed_at = (string) get_user_meta((int) $user->ID, self::REVIEWED_AT_META, true); $reviewer_id = (int) get_user_meta((int) $user->ID, self::REVIEWER_META, true); $reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null; $display = $status !== '' && isset($labels[$status]) ? $labels[$status] : __('未认证', 'aksrt'); $identity_name = (string) ($info['name'] ?? $apply['name'] ?? ''); ?>

*/ private static function admin_status_labels(): array { return [ 'pending' => __('待审核', 'aksrt'), 'verified' => __('已认证', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), ]; } /** @return array */ public static function get_auth_info(int $user_id): array { $info = get_user_meta($user_id, self::INFO_META, true); return is_array($info) ? $info : []; } /** @return array */ public static function get_apply_payload(int $user_id): array { $apply = get_user_meta($user_id, self::APPLY_META, true); return is_array($apply) ? $apply : []; } /** * Return certification evidence only to its owner or an administrator. * * @param array $application * @return 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' || (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true) !== $owner_id ) { continue; } $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($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, int $owner_id = 0, bool $admin_context = false): array { return [ 'name' => (string) ($application['name'] ?? ''), 'real_name' => (string) ($application['real_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, $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); } if (!is_user_logged_in()) { wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401); } if (!check_ajax_referer('aksrt_user_auth_apply', '_wpnonce', false)) { wp_send_json_error(['message' => __('安全校验失败', 'aksrt')], 403); } $user_id = get_current_user_id(); $s = self::settings(); if (!$s['enabled'] || !$s['allow_apply']) { wp_send_json_error(['message' => __('暂不能申请身份认证', 'aksrt')], 403); } if (self::is_verified($user_id)) { wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409); } if (self::get_status($user_id) === 'pending') { wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409); } $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : ''; $real_name = isset($_POST['real_name']) ? sanitize_text_field(wp_unslash((string) $_POST['real_name'])) : ''; $desc = isset($_POST['desc']) ? sanitize_textarea_field(wp_unslash((string) $_POST['desc'])) : ''; $msg = isset($_POST['msg']) ? sanitize_textarea_field(wp_unslash((string) $_POST['msg'])) : ''; $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : ''; $type_settings = self::type_settings($type); if ($this->text_length($name) < 4 || $this->text_length($name) > 16) { wp_send_json_error(['message' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')], 422); } if ($this->text_length($desc) > 300 || $this->text_length($msg) > 500) { wp_send_json_error(['message' => __('认证说明内容过长', 'aksrt')], 422); } if ($type_settings === []) { wp_send_json_error(['message' => __('请选择有效的认证类型', 'aksrt')], 422); } if (!empty($type_settings['require_real_name']) && $real_name === '') { wp_send_json_error(['message' => __('请填写真实姓名', 'aksrt')], 422); } if ($this->text_length($real_name) > 50) { wp_send_json_error(['message' => __('真实姓名不能超过 50 个字符', 'aksrt')], 422); } $lock_key = 'aksrt_auth_apply_lock_' . $user_id; $lock_token = wp_generate_uuid4(); $lock_payload = ['token' => $lock_token, 'expires' => time() + 300]; if (!add_option($lock_key, $lock_payload, '', false)) { $current_lock = get_option($lock_key, []); if (is_array($current_lock) && (int) ($current_lock['expires'] ?? 0) < time()) { delete_option($lock_key); } if (!add_option($lock_key, $lock_payload, '', false)) { wp_send_json_error(['message' => __('认证申请正在提交,请勿重复操作', 'aksrt')], 409); } } $uploaded = []; if (self::is_verified($user_id)) { $this->release_submission_lock($lock_key, $lock_token); wp_send_json_error(['message' => __('您已是认证用户', 'aksrt')], 409); } if (self::get_status($user_id) === 'pending') { $this->release_submission_lock($lock_key, $lock_token); wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409); } $uploaded = $this->upload_evidence($user_id); if (is_wp_error($uploaded)) { $this->release_submission_lock($lock_key, $lock_token); wp_send_json_error(['message' => $uploaded->get_error_message()], 422); } if (!$this->owns_submission_lock($lock_key, $lock_token)) { $this->delete_attachments($uploaded); wp_send_json_error(['message' => __('认证申请提交超时,请重新提交', 'aksrt')], 409); } $this->renew_submission_lock($lock_key, $lock_token); $free_applications = max(0, (int) ($type_settings['free_applications'] ?? 0)); $application_count = self::application_count($user_id, $type); $fee = $application_count < $free_applications ? 0.0 : max(0, (float) ($type_settings['fee'] ?? 0)); if ($fee > 0) { $deducted = \AKSRT\Credits\Balance::deduct( $user_id, $fee, 'verify_application', ['type' => 'verify', 'id' => $user_id], sprintf(__('提交%s申请', 'aksrt'), (string) $type_settings['label']) ); if (is_wp_error($deducted)) { $this->delete_attachments($uploaded); $this->release_submission_lock($lock_key, $lock_token); wp_send_json_error(['message' => $deducted->get_error_message()], 422); } } $previous_payload = self::get_apply_payload($user_id); $requested_at = current_time('Y-m-d H:i'); $payload = [ 'name' => $name, 'real_name' => $real_name, 'desc' => $desc, 'msg' => $msg, 'type' => $type, 'evidence_attachment_ids' => $uploaded, 'time' => $requested_at, 'fee' => $fee, ]; $payload_saved = update_user_meta($user_id, self::APPLY_META, $payload); if ($payload_saved === false && get_user_meta($user_id, self::APPLY_META, true) !== $payload) { $this->delete_attachments($uploaded); if ($fee > 0) { \AKSRT\Credits\Balance::add($user_id, $fee, 'verify_application_refund', ['type' => 'verify', 'id' => $user_id], __('认证申请保存失败退款', 'aksrt')); } $this->release_submission_lock($lock_key, $lock_token); wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500); } $status_saved = update_user_meta($user_id, self::STATUS_META, 'pending'); if ($status_saved === false && self::get_status($user_id) !== 'pending') { $this->delete_attachments($uploaded); if ($fee > 0) { \AKSRT\Credits\Balance::add($user_id, $fee, 'verify_application_refund', ['type' => 'verify', 'id' => $user_id], __('认证申请保存失败退款', 'aksrt')); } $previous_payload === [] ? delete_user_meta($user_id, self::APPLY_META) : update_user_meta($user_id, self::APPLY_META, $previous_payload); $this->release_submission_lock($lock_key, $lock_token); wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500); } 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); self::increment_application_count($user_id, $type); 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); $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); $this->release_submission_lock($lock_key, $lock_token); wp_send_json_success([ 'message' => __('提交成功,请等待管理员审核', 'aksrt'), 'status' => 'pending', ], 201); } /** @return array|\WP_Error */ private function upload_evidence(int $user_id) { $files = $this->normalize_uploads($_FILES['image'] ?? null); if (is_wp_error($files)) { return $files; } if ($files === []) { return []; } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $attachment_ids = []; foreach ($files as $file) { $filename = sanitize_file_name((string) ($file['name'] ?? '')); $tmp_name = (string) ($file['tmp_name'] ?? ''); $size = (int) ($file['size'] ?? 0); $error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE); if ($error !== UPLOAD_ERR_OK || $filename === '' || $size <= 0 || $size > 2 * MB_IN_BYTES || !is_uploaded_file($tmp_name)) { $this->delete_attachments($attachment_ids); return new \WP_Error('aksrt_auth_upload_failed', __('证明材料上传失败,单张图片不能超过 2MB', 'aksrt')); } $file_check = wp_check_filetype_and_ext($tmp_name, $filename); $mime = (string) ($file_check['type'] ?? ''); if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) { $this->delete_attachments($attachment_ids); return new \WP_Error('aksrt_auth_upload_type', __('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt')); } $attachment_id = media_handle_sideload([ 'name' => $filename, 'tmp_name' => $tmp_name, 'type' => $mime, 'error' => UPLOAD_ERR_OK, 'size' => $size, ], 0, __('官方认证证明材料', 'aksrt')); if (is_wp_error($attachment_id)) { $this->delete_attachments($attachment_ids); return new \WP_Error('aksrt_auth_upload_failed', __('证明材料写入媒体库失败', 'aksrt')); } $attachment_id = (int) $attachment_id; $stored_mime = (string) get_post_mime_type($attachment_id); if (!in_array($stored_mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) { wp_delete_attachment($attachment_id, true); $this->delete_attachments($attachment_ids); return new \WP_Error('aksrt_auth_upload_type', __('证明材料类型校验失败', 'aksrt')); } wp_update_post([ 'ID' => $attachment_id, 'post_author' => $user_id, 'post_status' => 'private', ]); update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id); $attachment_ids[] = $attachment_id; } return $attachment_ids; } /** @return array>|\WP_Error */ private function normalize_uploads($input) { if (!is_array($input) || empty($input['name'])) { return []; } $names = is_array($input['name']) ? $input['name'] : [$input['name']]; if (count($names) > 3) { return new \WP_Error('aksrt_auth_upload_limit', __('证明材料最多上传 3 张', 'aksrt')); } $files = []; foreach ($names as $index => $name) { if ((string) $name === '') { continue; } $files[] = [ 'name' => $name, 'type' => is_array($input['type'] ?? null) ? ($input['type'][$index] ?? '') : ($input['type'] ?? ''), 'tmp_name' => is_array($input['tmp_name'] ?? null) ? ($input['tmp_name'][$index] ?? '') : ($input['tmp_name'] ?? ''), 'error' => is_array($input['error'] ?? null) ? ($input['error'][$index] ?? UPLOAD_ERR_NO_FILE) : ($input['error'] ?? UPLOAD_ERR_NO_FILE), 'size' => is_array($input['size'] ?? null) ? ($input['size'][$index] ?? 0) : ($input['size'] ?? 0), ]; } return $files; } /** @param array $attachment_ids */ private function delete_attachments(array $attachment_ids): void { foreach ($attachment_ids as $attachment_id) { $attachment_id = (int) $attachment_id; if ($attachment_id > 0 && get_post_type($attachment_id) === 'attachment') { wp_delete_attachment($attachment_id, true); } } } private function owns_submission_lock(string $lock_key, string $lock_token): bool { $current_lock = get_option($lock_key, []); return is_array($current_lock) && (int) ($current_lock['expires'] ?? 0) >= time() && hash_equals((string) ($current_lock['token'] ?? ''), $lock_token); } private function renew_submission_lock(string $lock_key, string $lock_token): void { if ($this->owns_submission_lock($lock_key, $lock_token)) { update_option($lock_key, ['token' => $lock_token, 'expires' => time() + 300], false); } } private function release_submission_lock(string $lock_key, string $lock_token): void { $current_lock = get_option($lock_key, []); if (is_array($current_lock) && hash_equals((string) ($current_lock['token'] ?? ''), $lock_token)) { delete_option($lock_key); } } private function text_length(string $value): int { return function_exists('mb_strlen') ? mb_strlen($value) : strlen($value); } }