fix(identity): complete native certification review flow

This commit is contained in:
kate522 committed 2026-07-29 12:25:58 +08:00
1 parent 32658be87d
commit 2dfaa10798
16 files changed
+571 -1167

No files matched your search

+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);
}
}