fix(identity): complete native certification review flow
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user