fix(identity): complete native certification review flow

This commit is contained in:
2026-07-29 12:25:58 +08:00
parent 32658be87d
commit 2dfaa10798
16 changed files with 571 additions and 1167 deletions
+3
View File
@@ -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;
+7 -195
View File
@@ -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 '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
$message = $map[$code];
if ($code === 'verify_failed' && isset($_GET['aksrt_verify_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['aksrt_verify_error']));
}
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
}
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 '<div class="wrap"><h1>' . esc_html__('身份认证', 'aksrt') . '</h1>';
$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 '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_user_auth" />';
echo '<select name="status">';
foreach (['pending' => __('待审核', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已拒绝', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
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 '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('邮箱', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('备注', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="6">' . esc_html__('暂无认证申请', 'aksrt') . '</td></tr>';
}
foreach ((array) $items as $row) {
$uid = (int) ($row['ID'] ?? 0);
$st = (string) ($row['verify_status'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $uid) . '</td>';
echo '<td>' . esc_html((string) ($row['display_name'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($row['user_email'] ?? '')) . '</td>';
echo '<td>' . esc_html($st) . '</td>';
$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 '<td>';
if ($apply) {
$type_key = sanitize_key((string) ($apply['type'] ?? ''));
echo '<strong>' . esc_html((string) ($apply['name'] ?? '')) . '</strong>';
if ($type_key !== '') {
echo '<div><span class="dashicons dashicons-id" aria-hidden="true"></span> ' . esc_html($type_labels[$type_key] ?? $type_key) . '</div>';
}
if (!empty($apply['desc'])) {
echo '<div>' . esc_html((string) $apply['desc']) . '</div>';
}
if (!empty($apply['msg'])) {
echo '<div class="description">' . esc_html((string) $apply['msg']) . '</div>';
}
$evidence_items = class_exists('\AKSRT\User\AuthApply')
? \AKSRT\User\AuthApply::get_evidence_items($apply)
: [];
foreach ($evidence_items as $item) {
echo '<a href="' . esc_url((string) $item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url((string) $item['thumbnail_url']) . '" alt="" style="width:56px;height:56px;object-fit:cover;margin:6px 6px 0 0;border-radius:4px;" /></a>';
}
}
if (!empty($row['verify_reason'])) {
echo '<div class="description">' . esc_html((string) $row['verify_reason']) . '</div>';
}
echo '</td>';
echo '<td>';
if ($st === 'pending') {
foreach (['verified' => __('通过', 'aksrt'), 'rejected' => __('拒绝', 'aksrt')] as $action => $label) {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;margin-right:4px;">';
echo '<input type="hidden" name="action" value="aksrt_users_review_verify" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $uid) . '" />';
echo '<input type="hidden" name="status" value="' . esc_attr($action) . '" />';
wp_nonce_field('aksrt_users_review_verify_' . $uid);
if ($action === 'rejected') {
echo '<input type="text" name="note" placeholder="' . esc_attr__('拒绝原因', 'aksrt') . '" /> ';
}
submit_button($label, 'small', '', false);
echo '</form>';
}
} else {
echo '—';
}
echo '</td></tr>';
}
echo '</tbody></table>';
$this->pagination($pages, $page_num);
echo '</div>';
}
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
* ----------------------------------------------------------------*/
+346
View File
@@ -0,0 +1,346 @@
<?php
/**
* Native WordPress identity certification operations.
*
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
trait UsersMenusVerify {
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();
$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 '<div class="wrap"><h1>' . esc_html__('身份认证', 'aksrt') . '</h1>';
$this->flash();
echo '<p>' . 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']
)) . '</p>';
$this->render_verify_filters($filters);
$this->render_verify_table($result['items']);
$this->pagination($result['pages'], $page_num);
echo '</div>';
}
/** @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<int,array<string,mixed>>,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 '<form method="get" style="margin:12px 0;display:flex;gap:8px;align-items:end;flex-wrap:wrap;">';
echo '<input type="hidden" name="page" value="aksrt_user_auth" />';
echo '<label>' . esc_html__('状态', 'aksrt') . '<br><select name="status">';
foreach (['pending' => __('待审核', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['status'], $key, false), esc_html($label));
}
echo '</select></label>';
echo '<label>' . esc_html__('搜索', 'aksrt') . '<br><input type="search" name="keyword" value="' . esc_attr($filters['keyword']) . '" placeholder="' . esc_attr__('用户、邮箱、认证资料', 'aksrt') . '" /></label>';
echo '<label>' . esc_html__('申请起始日期', 'aksrt') . '<br><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '" /></label>';
echo '<label>' . esc_html__('申请结束日期', 'aksrt') . '<br><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '" /></label>';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
}
/** @param array<int,array<string,mixed>> $items */
private function render_verify_table(array $items): void {
$type_labels = $this->get_verify_type_labels();
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th style="width:18%">' . esc_html__('用户', 'aksrt') . '</th>';
echo '<th style="width:9%">' . esc_html__('状态', 'aksrt') . '</th>';
echo '<th style="width:33%">' . esc_html__('申请资料', 'aksrt') . '</th>';
echo '<th style="width:18%">' . esc_html__('审核记录', 'aksrt') . '</th>';
echo '<th style="width:22%">' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if ($items === []) {
echo '<tr><td colspan="5">' . esc_html__('暂无认证申请', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$this->render_verify_row($row, $type_labels);
}
echo '</tbody></table>';
}
/** @return array<string,string> */
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<string,mixed> $row @param array<string,string> $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 '<tr>';
echo '<td><strong><a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html((string) ($row['display_name'] ?? '')) . '</a></strong>';
echo '<div class="description">@' . esc_html((string) ($row['user_login'] ?? '')) . ' · UID ' . esc_html((string) $user_id) . '</div>';
echo '<div><a href="mailto:' . esc_attr((string) ($row['user_email'] ?? '')) . '">' . esc_html((string) ($row['user_email'] ?? '')) . '</a></div></td>';
echo '<td>' . $this->verify_status_badge($status) . '</td>';
echo '<td>';
$type_key = sanitize_key((string) ($apply['type'] ?? ''));
echo '<strong>' . esc_html((string) ($apply['name'] ?? __('未填写认证身份', 'aksrt'))) . '</strong>';
echo '<div>' . esc_html__('类型:', 'aksrt') . esc_html($type_labels[$type_key] ?? $type_key ?: '—') . '</div>';
if (!empty($apply['desc'])) {
echo '<div>' . esc_html((string) $apply['desc']) . '</div>';
}
if (!empty($apply['msg'])) {
echo '<div class="description">' . esc_html__('申请说明:', 'aksrt') . esc_html((string) $apply['msg']) . '</div>';
}
echo '<div class="description">' . esc_html__('申请时间:', 'aksrt') . esc_html((string) ($row['requested_at'] ?: ($apply['time'] ?? '—'))) . '</div>';
$evidence = class_exists('\AKSRT\User\AuthApply') ? \AKSRT\User\AuthApply::get_evidence_items($apply, $user_id, true) : [];
foreach ($evidence as $item) {
echo '<a href="' . esc_url((string) $item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url((string) $item['thumbnail_url']) . '" alt="' . esc_attr__('认证证明材料', 'aksrt') . '" style="width:64px;height:64px;object-fit:cover;margin:8px 8px 0 0;border-radius:4px;" /></a>';
}
echo '</td>';
echo '<td>';
if (!empty($row['reviewed_at'])) {
echo '<div>' . esc_html((string) $row['reviewed_at']) . '</div>';
}
if ($reviewer) {
echo '<div class="description">' . esc_html__('审核人:', 'aksrt') . esc_html($reviewer->display_name) . '</div>';
}
if (!empty($row['verify_reason'])) {
echo '<div>' . esc_html((string) $row['verify_reason']) . '</div>';
}
if ($history !== []) {
echo '<details><summary>' . esc_html(sprintf(__('历史记录(%d)', 'aksrt'), count($history))) . '</summary><ol style="margin-left:18px;">';
foreach ($history as $entry) {
echo '<li>' . wp_kses_post($this->format_verify_history_entry($entry)) . '</li>';
}
echo '</ol></details>';
}
echo '</td><td>';
$this->render_verify_actions($user_id, $status);
echo '</td></tr>';
}
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 '<span style="display:inline-block;padding:2px 8px;border:1px solid currentColor;border-radius:999px;color:'
. esc_attr($colors[$status] ?? '#646970') . '">' . esc_html($labels[$status] ?? $status) . '</span>';
}
/** @param array<string,mixed> $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 '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:grid;gap:6px;">';
echo '<input type="hidden" name="action" value="aksrt_users_review_verify" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $user_id) . '" />';
wp_nonce_field('aksrt_users_review_verify_' . $user_id);
echo '<textarea name="note" rows="2" placeholder="' . esc_attr__('通过时可填备注', 'aksrt') . '"></textarea>';
echo '<button type="submit" name="status" value="verified" class="button button-primary">' . esc_html__('通过认证', 'aksrt') . '</button>';
echo '</form>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:grid;gap:6px;margin-top:10px;">';
echo '<input type="hidden" name="action" value="aksrt_users_review_verify" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $user_id) . '" />';
echo '<input type="hidden" name="status" value="rejected" />';
wp_nonce_field('aksrt_users_review_verify_' . $user_id);
echo '<textarea name="note" rows="2" required placeholder="' . esc_attr__('填写驳回原因', 'aksrt') . '"></textarea>';
echo '<button type="submit" class="button">' . esc_html__('驳回申请', 'aksrt') . '</button>';
echo '</form>';
return;
}
if ($status === 'verified') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:grid;gap:6px;">';
echo '<input type="hidden" name="action" value="aksrt_users_review_verify" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $user_id) . '" />';
echo '<input type="hidden" name="status" value="cancelled" />';
wp_nonce_field('aksrt_users_review_verify_' . $user_id);
echo '<textarea name="note" rows="2" required placeholder="' . esc_attr__('填写取消认证原因', 'aksrt') . '"></textarea>';
echo '<button type="submit" class="button button-link-delete" onclick="return confirm(\'' . esc_js(__('确认取消该用户的官方认证?', 'aksrt')) . '\');">' . esc_html__('取消认证', 'aksrt') . '</button>';
echo '</form>';
return;
}
echo '<span class="description">' . esc_html__('用户可在前台重新提交申请', 'aksrt') . '</span>';
}
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<int,array<string,mixed>> */
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);
}
}
+1 -2
View File
@@ -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' => [
+3 -1
View File
@@ -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',
-29
View File
@@ -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<id>\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<id>\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',
@@ -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.
*/
+1 -2
View File
@@ -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<int,int>},
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,icon_url?:string}>},
@@ -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)),
+197 -19
View File
@@ -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<string,mixed> $application
* @return array<int,array{id:int,url:string,thumbnail_url:string}>
*/
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<string,mixed> $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<int,array<string,mixed>> */
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<string,mixed>|\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;
}