feat(admin): redesign identity review with dialogs
Replace inline identity review forms with focused detail and review dialogs, improve list filters and caching, and align evidence permissions and deletion feedback. Add contract and runtime regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5732be847e
commit
bc26d62602
9 files changed
+2222
-116
No files matched your search
@@ -46,6 +46,7 @@ class UsersMenus {
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_ticket_assets']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_message_assets']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_invite_assets']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_verify_assets']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_vip_assets']);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,169 +3,218 @@
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
use AKSRT\Identity\ApplicationRepository;
|
||||
use AKSRT\Identity\EvidenceService;
|
||||
use AKSRT\Identity\EventRepository;
|
||||
use AKSRT\User\AuthApply;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusVerify {
|
||||
use UsersMenusVerifyDialogs;
|
||||
|
||||
private const VERIFY_PER_PAGE = 20;
|
||||
private const VERIFY_STATUSES = ['pending', 'approved', 'rejected', ''];
|
||||
|
||||
public function enqueue_verify_assets(string $hook): void {
|
||||
if ($hook !== 'users_page_aksrt_user_auth') return;
|
||||
$css = '/assets/css/admin/user-auth.css';
|
||||
$js = '/assets/js/admin-user-auth.js';
|
||||
wp_enqueue_style('aksrt-admin-user-auth', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
|
||||
wp_enqueue_script('aksrt-admin-user-auth', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
|
||||
}
|
||||
|
||||
private function count_verify_status(string $status): int {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||||
$stored = $status === 'pending' ? 'manual_pending' : $status;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE type = %s AND channel = %s AND status = %s",
|
||||
'general',
|
||||
'manual',
|
||||
$stored
|
||||
'general', 'manual', $status === 'pending' ? 'manual_pending' : $status
|
||||
));
|
||||
}
|
||||
|
||||
public function render_verify_page(): void {
|
||||
$this->ensure_cap();
|
||||
$filters = $this->get_verify_filters();
|
||||
$page = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$data = $this->query_verify_requests($filters, $page, 20);
|
||||
$counts = $this->get_verify_status_counts();
|
||||
NativeUi::page_open();
|
||||
NativeUi::header(__('身份认证', 'aksrt'), '', 'dashicons-id');
|
||||
$filters = $this->get_verify_filters($_GET);
|
||||
$data = $this->query_verify_requests($filters, max(1, (int) ($_GET['paged'] ?? 1)));
|
||||
$counts = $this->get_verify_status_counts($filters);
|
||||
NativeUi::page_open('aksrt-verify-page');
|
||||
NativeUi::header(
|
||||
__('身份认证', 'aksrt'),
|
||||
__('查看申请资料与证明材料,在审核弹窗中确认处理结果。', 'aksrt'),
|
||||
'dashicons-id',
|
||||
[['label' => __('认证设置', 'aksrt'), 'url' => admin_url('admin.php?page=aksrt-settings#/users/verify'), 'icon' => 'dashicons-admin-settings']]
|
||||
);
|
||||
$this->flash();
|
||||
NativeUi::stats([
|
||||
['label' => __('待审核', 'aksrt'), 'value' => $counts['pending'], 'icon' => 'dashicons-clock', 'tone' => 'warning'],
|
||||
['label' => __('已通过', 'aksrt'), 'value' => $counts['approved'], 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
|
||||
['label' => __('已拒绝', 'aksrt'), 'value' => $counts['rejected'], 'icon' => 'dashicons-dismiss', 'tone' => 'danger'],
|
||||
]);
|
||||
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
|
||||
$this->render_verify_filters($filters);
|
||||
NativeUi::card_close();
|
||||
NativeUi::table_card_open(__('认证申请', 'aksrt'));
|
||||
$this->render_verify_table($data['items']);
|
||||
NativeUi::card_open('', '', true, 'aksrt-verify-list-card');
|
||||
$this->render_verify_filters($filters, $counts);
|
||||
echo '<div class="aksrt-native-table-wrap">';
|
||||
$this->render_verify_table($data['items'], $filters, $data['page']);
|
||||
NativeUi::table_wrap_close();
|
||||
$this->pagination($data['pages'], $page);
|
||||
AdminListPagination::render('aksrt_user_auth', $data['total'], $data['page'], self::VERIFY_PER_PAGE, $filters, 'users.php');
|
||||
NativeUi::card_close();
|
||||
echo '<dialog id="aksrt-verify-dialog" class="aksrt-verify-dialog" aria-labelledby="aksrt-verify-dialog-title" aria-describedby="aksrt-verify-dialog-description"></dialog>';
|
||||
echo '<noscript><p class="aksrt-native-callout">' . esc_html__('请启用 JavaScript 后查看申请资料和处理审核。', 'aksrt') . '</p></noscript>';
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
/** @return array{status:string,keyword:string} */
|
||||
private function get_verify_filters(): array {
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
|
||||
if (!in_array($status, ['pending', 'approved', 'rejected', ''], true)) $status = 'pending';
|
||||
private function get_verify_filters(array $input): array {
|
||||
$status = isset($input['status']) ? sanitize_key((string) wp_unslash($input['status'])) : 'pending';
|
||||
return [
|
||||
'status' => $status,
|
||||
'keyword' => isset($_GET['keyword']) ? sanitize_text_field((string) wp_unslash($_GET['keyword'])) : '',
|
||||
'status' => in_array($status, self::VERIFY_STATUSES, true) ? $status : 'pending',
|
||||
'keyword' => sanitize_text_field((string) wp_unslash($input['keyword'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{pending:int,approved:int,rejected:int} */
|
||||
private function get_verify_status_counts(): array {
|
||||
return [
|
||||
'pending' => $this->count_verify_status('pending'),
|
||||
'approved' => $this->count_verify_status('verified'),
|
||||
'rejected' => $this->count_verify_status('rejected'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{items:array<int,array<string,mixed>>,pages:int} */
|
||||
private function query_verify_requests(array $filters, int $page, int $per_page): array {
|
||||
/** @return array{0:string,1:array} */
|
||||
private function verify_query_base(array $filters): array {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||||
$where = ["a.type = 'general'", "a.channel = 'manual'"];
|
||||
$params = [];
|
||||
$where = ['a.type = %s', 'a.channel = %s'];
|
||||
$params = ['general', 'manual'];
|
||||
if ($filters['status'] !== '') {
|
||||
$where[] = 'a.status = %s';
|
||||
$params[] = $filters['status'] === 'pending' ? 'manual_pending' : ($filters['status'] === 'approved' ? 'verified' : $filters['status']);
|
||||
$params[] = ['pending' => 'manual_pending', 'approved' => 'verified', 'rejected' => 'rejected'][$filters['status']];
|
||||
}
|
||||
if ($filters['keyword'] !== '') {
|
||||
$like = '%' . $wpdb->esc_like($filters['keyword']) . '%';
|
||||
$where[] = '(CAST(u.ID AS CHAR) LIKE %s OR u.display_name LIKE %s OR u.user_login LIKE %s OR u.user_email LIKE %s OR a.public_name LIKE %s OR a.provider_result_summary LIKE %s)';
|
||||
array_push($params, $like, $like, $like, $like, $like, $like);
|
||||
}
|
||||
$condition = implode(' AND ', $where);
|
||||
$base = "FROM {$table} a INNER JOIN {$wpdb->users} u ON u.ID = a.user_id WHERE {$condition}";
|
||||
$count_sql = "SELECT COUNT(*) {$base}";
|
||||
$total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql);
|
||||
$limit = max(1, min(100, $per_page));
|
||||
$query_params = array_merge($params, [$limit, (max(1, $page) - 1) * $limit]);
|
||||
$sql = "SELECT a.id, a.application_uuid, a.user_id, a.type, a.channel, a.status, a.public_name, a.provider_result_summary AS description, a.manual_reason, a.review_note, a.evidence_ids, a.created_at, u.display_name, u.user_login, u.user_email {$base} ORDER BY a.created_at DESC, a.id DESC LIMIT %d OFFSET %d";
|
||||
return ["FROM {$table} a INNER JOIN {$wpdb->users} u ON u.ID = a.user_id WHERE " . implode(' AND ', $where), $params];
|
||||
}
|
||||
|
||||
/** @return array{pending:int,approved:int,rejected:int,all:int} */
|
||||
private function get_verify_status_counts(array $filters): array {
|
||||
global $wpdb;
|
||||
$filters['status'] = '';
|
||||
[$base, $params] = $this->verify_query_base($filters);
|
||||
$rows = $wpdb->get_results($wpdb->prepare("SELECT a.status, COUNT(*) AS total {$base} GROUP BY a.status", $params), ARRAY_A);
|
||||
$counts = ['pending' => 0, 'approved' => 0, 'rejected' => 0, 'all' => 0];
|
||||
$keys = ['manual_pending' => 'pending', 'verified' => 'approved', 'rejected' => 'rejected'];
|
||||
foreach ((array) $rows as $row) {
|
||||
$total = (int) $row['total'];
|
||||
$counts['all'] += $total;
|
||||
if (isset($keys[$row['status']])) $counts[$keys[$row['status']]] = $total;
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/** @return array{items:array,page:int,total:int} */
|
||||
private function query_verify_requests(array $filters, int $page): array {
|
||||
global $wpdb;
|
||||
[$base, $params] = $this->verify_query_base($filters);
|
||||
$total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) {$base}", $params));
|
||||
$page = min(max(1, $page), max(1, (int) ceil($total / self::VERIFY_PER_PAGE)));
|
||||
$query_params = array_merge($params, [self::VERIFY_PER_PAGE, ($page - 1) * self::VERIFY_PER_PAGE]);
|
||||
$sql = "SELECT a.id, a.application_uuid, a.user_id, a.status, a.public_name, a.provider_result_summary AS description, a.manual_reason, a.review_note, a.reviewer_id, a.evidence_ids, a.created_at, a.completed_at, u.display_name, u.user_login, u.user_email {$base} ORDER BY a.created_at DESC, a.id DESC LIMIT %d OFFSET %d";
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
|
||||
return ['items' => is_array($rows) ? $rows : [], 'pages' => max(1, (int) ceil(max(1, $total) / $limit))];
|
||||
$items = is_array($rows) ? $rows : [];
|
||||
$users = [];
|
||||
$evidence = [];
|
||||
foreach ($items as &$row) {
|
||||
$ids = json_decode((string) $row['evidence_ids'], true);
|
||||
$row['evidence_ids'] = is_array($ids) ? array_values(array_filter(array_map('absint', $ids))) : [];
|
||||
array_push($evidence, ...$row['evidence_ids']);
|
||||
$users[] = (int) $row['user_id'];
|
||||
$users[] = (int) $row['reviewer_id'];
|
||||
}
|
||||
unset($row);
|
||||
$users = array_values(array_unique(array_filter($users)));
|
||||
if ($users !== []) cache_users($users);
|
||||
if ($evidence !== []) update_meta_cache('post', array_values(array_unique($evidence)));
|
||||
return ['items' => $items, 'page' => $page, 'total' => $total];
|
||||
}
|
||||
|
||||
private function render_verify_filters(array $filters): void {
|
||||
echo '<form method="get" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_auth" />';
|
||||
echo '<label>' . esc_html__('状态', 'aksrt') . '<select name="status">';
|
||||
private function verify_page_url(array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => 'aksrt_user_auth'], $args), admin_url('users.php'));
|
||||
}
|
||||
|
||||
private function render_verify_filters(array $filters, array $counts): void {
|
||||
echo '<div class="aksrt-verify-toolbar"><div class="aksrt-verify-toolbar__views">';
|
||||
echo '<h2 class="aksrt-native-card__title">' . esc_html__('认证申请', 'aksrt') . '</h2>';
|
||||
echo '<ul class="subsubsub" aria-label="' . esc_attr__('认证状态筛选', 'aksrt') . '">';
|
||||
foreach (['pending' => __('待审核', 'aksrt'), 'approved' => __('已通过', 'aksrt'), 'rejected' => __('已拒绝', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['status'], $key, false), esc_html($label));
|
||||
$url = $this->verify_page_url(['status' => $key, 'keyword' => $filters['keyword']]);
|
||||
echo '<li><a href="' . esc_url($url) . '"' . ($key === $filters['status'] ? ' class="current" aria-current="page"' : '') . '>' . esc_html($label)
|
||||
. ' <span class="count">' . esc_html(number_format_i18n($counts[$key === '' ? 'all' : $key])) . '</span></a></li>';
|
||||
}
|
||||
echo '</select></label><label>' . esc_html__('搜索', 'aksrt') . '<input type="search" name="keyword" value="' . esc_attr($filters['keyword']) . '" /></label>';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
echo '</ul></div><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_auth"><input type="hidden" name="status" value="' . esc_attr($filters['status']) . '">';
|
||||
echo '<label class="screen-reader-text" for="aksrt-verify-search">' . esc_html__('搜索用户或认证资料', 'aksrt') . '</label>';
|
||||
echo '<input type="search" id="aksrt-verify-search" name="keyword" value="' . esc_attr($filters['keyword']) . '" placeholder="' . esc_attr__('用户 ID / 昵称 / 邮箱 / 认证名称', 'aksrt') . '">';
|
||||
echo '<button type="submit" class="button">' . esc_html__('搜索', 'aksrt') . '</button>';
|
||||
if ($filters['keyword'] !== '') {
|
||||
echo '<a class="button-link" href="' . esc_url($this->verify_page_url(['status' => $filters['status']])) . '">' . esc_html__('清除搜索', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form></div>';
|
||||
}
|
||||
|
||||
private function render_verify_table(array $items): void {
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th>用户</th><th>状态</th><th>认证名称</th><th>认证简介 / 申请说明</th><th>证明材料</th><th>审核</th></tr></thead><tbody>';
|
||||
if ($items === []) {
|
||||
echo NativeUi::empty_row(6, __('暂无认证申请', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
private function render_verify_table(array $items, array $filters, int $page): void {
|
||||
echo '<table class="widefat fixed striped aksrt-verify-table"><thead><tr>';
|
||||
foreach (['user' => __('申请用户', 'aksrt'), 'profile' => __('认证资料', 'aksrt'), 'evidence' => __('证明材料', 'aksrt'), 'status' => __('状态', 'aksrt'), 'time' => __('申请时间', 'aksrt'), 'actions' => __('操作', 'aksrt')] as $key => $label) {
|
||||
echo '<th scope="col" class="column-' . esc_attr($key) . '">' . esc_html($label) . '</th>';
|
||||
}
|
||||
foreach ($items as $row) $this->render_verify_row($row);
|
||||
echo '</tr></thead><tbody>';
|
||||
if ($items === []) {
|
||||
$title = $filters['status'] === 'pending' && $filters['keyword'] === '' ? __('暂无待审核申请', 'aksrt') : __('没有符合条件的认证申请', 'aksrt');
|
||||
$hint = $filters['keyword'] !== '' ? __('请调整搜索关键词或切换状态。', 'aksrt') : __('可切换状态查看其他申请,新申请将在这里显示。', 'aksrt');
|
||||
echo NativeUi::empty_row(6, $title, $hint); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
}
|
||||
foreach ($items as $row) $this->render_verify_row($row, $filters, $page);
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
private function render_verify_row(array $row): void {
|
||||
$user_id = (int) $row['user_id'];
|
||||
$status = (string) $row['status'];
|
||||
$application = (new \AKSRT\Identity\ApplicationRepository())->find_by_id((int) $row['id']) ?: $row;
|
||||
echo '<tr><td>' . NativeUi::user_cell($user_id, (string) $row['display_name']) . '<div class="aksrt-native-muted">' . esc_html((string) $row['user_email']) . '</div></td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo '<td>' . NativeUi::status_badge($status, [
|
||||
private function render_verify_row(array $row, array $filters, int $page): void {
|
||||
$id = (int) $row['id'];
|
||||
$pending = $row['status'] === 'manual_pending';
|
||||
$evidence = (new EvidenceService())->items($row, true);
|
||||
echo '<tr><td class="column-user">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo '<div class="aksrt-verify-sub">' . esc_html((string) $row['user_email']) . '</div></td>';
|
||||
echo '<td class="column-profile"><strong>' . esc_html((string) $row['public_name']) . '</strong><div class="aksrt-verify-summary">' . esc_html((string) $row['description']) . '</div>';
|
||||
$this->render_verify_detail_template($row, $evidence, $filters, $page);
|
||||
$this->render_verify_delete_template($row, $filters, $page);
|
||||
echo '</td><td class="column-evidence">' . esc_html($evidence === [] ? __('未提供材料', 'aksrt') : sprintf(__('%d 份材料', 'aksrt'), count($evidence))) . '</td>';
|
||||
echo '<td class="column-status">' . $this->verify_status_badge((string) $row['status']) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo '<td class="column-time">' . esc_html($this->verify_time((string) $row['created_at'])) . '</td>';
|
||||
echo '<td class="column-actions"><div class="aksrt-verify-row-actions">';
|
||||
echo '<button type="button" class="button' . ($pending ? ' button-primary' : '') . '" data-aksrt-verify-view="' . $id . '" aria-haspopup="dialog" aria-controls="aksrt-verify-dialog">' . esc_html($pending ? __('审核', 'aksrt') : __('查看详情', 'aksrt')) . '</button>';
|
||||
echo '<button type="button" class="button-link button-link-delete" data-aksrt-verify-delete="' . $id . '" aria-haspopup="dialog" aria-controls="aksrt-verify-dialog">' . esc_html__('删除记录', 'aksrt') . '</button></div></td></tr>';
|
||||
}
|
||||
|
||||
private function verify_status_badge(string $status): string {
|
||||
return NativeUi::status_badge($status, [
|
||||
'manual_pending' => [__('待审核', 'aksrt'), 'warning'],
|
||||
'verified' => [__('已通过', 'aksrt'), 'success'],
|
||||
'rejected' => [__('已拒绝', 'aksrt'), 'danger'],
|
||||
]) . '<div class="aksrt-native-muted">' . esc_html((string) $row['created_at']) . '</div></td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo '<td><strong>' . esc_html((string) $row['public_name']) . '</strong></td>';
|
||||
echo '<td>' . esc_html((string) $row['description']) . '<p class="description">' . esc_html((string) $row['manual_reason']) . '</p>';
|
||||
if (!empty($row['review_note'])) echo '<p class="description">' . esc_html__('处理留言:', 'aksrt') . esc_html((string) $row['review_note']) . '</p>';
|
||||
echo '</td><td>';
|
||||
foreach ((new \AKSRT\Identity\EvidenceService())->items($application, true) as $item) {
|
||||
echo '<a href="' . esc_url($item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url($item['thumbnail_url']) . '" alt="" style="width:64px;height:64px;object-fit:cover;margin:4px;border-radius:4px;" /></a>';
|
||||
}
|
||||
echo '</td><td>';
|
||||
$this->render_verify_actions($row);
|
||||
echo '</td></tr>';
|
||||
]);
|
||||
}
|
||||
|
||||
private function render_verify_actions(array $row): void {
|
||||
$status = (string) $row['status'];
|
||||
if ($status === 'manual_pending') {
|
||||
foreach (['verified' => __('通过申请', 'aksrt'), 'rejected' => __('拒绝申请', 'aksrt')] as $next => $label) {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:grid;gap:5px;margin-bottom:10px;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_review_verify"><input type="hidden" name="user_id" value="' . (int) $row['user_id'] . '"><input type="hidden" name="application_uuid" value="' . esc_attr((string) $row['application_uuid']) . '"><input type="hidden" name="status" value="' . esc_attr($next) . '">';
|
||||
wp_nonce_field('aksrt_users_review_verify_' . (int) $row['user_id']);
|
||||
echo '<input name="name" type="text" maxlength="64" value="' . esc_attr((string) $row['public_name']) . '" placeholder="' . esc_attr__('认证名称', 'aksrt') . '" required>';
|
||||
echo '<textarea name="description" rows="2" placeholder="' . esc_attr__('认证简介', 'aksrt') . '">' . esc_textarea((string) $row['description']) . '</textarea>';
|
||||
echo '<textarea name="note" rows="2" ' . ($next === 'rejected' ? 'required' : '') . ' placeholder="' . esc_attr__('处理留言;拒绝时请填写原因', 'aksrt') . '"></textarea>';
|
||||
echo '<button class="button ' . ($next === 'verified' ? 'button-primary' : '') . '">' . esc_html($label) . '</button></form>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="description">' . esc_html__('已处理', 'aksrt') . '</span>';
|
||||
}
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" onsubmit="return confirm(\'' . esc_js(__('确定删除此认证申请记录及其证明材料?', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_delete_verify"><input type="hidden" name="application_uuid" value="' . esc_attr((string) $row['application_uuid']) . '">';
|
||||
wp_nonce_field('aksrt_users_delete_verify_' . (string) $row['application_uuid']);
|
||||
echo '<button class="button button-link-delete">' . esc_html__('删除记录', 'aksrt') . '</button></form>';
|
||||
private function verify_time(string $utc): string {
|
||||
return $utc !== '' && $utc !== '0000-00-00 00:00:00' ? get_date_from_gmt($utc, 'Y-m-d H:i') : '—';
|
||||
}
|
||||
|
||||
/** @return array{status:string,keyword:string,paged:int} */
|
||||
private function verify_return_args(): array {
|
||||
return array_merge($this->get_verify_filters(['status' => $_POST['filter_status'] ?? 'pending', 'keyword' => $_POST['keyword'] ?? '']), [
|
||||
'paged' => max(1, (int) ($_POST['paged'] ?? 1)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function handle_review_verify(): void {
|
||||
if (!current_user_can('aksrt_review_identity') && !current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
$user_id = absint($_POST['user_id'] ?? 0);
|
||||
check_admin_referer('aksrt_users_review_verify_' . $user_id);
|
||||
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
|
||||
check_admin_referer('aksrt_users_review_verify_' . $uuid);
|
||||
$user_id = absint($_POST['user_id'] ?? 0);
|
||||
$status = sanitize_key((string) wp_unslash($_POST['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? ''));
|
||||
$fields = [
|
||||
'name' => sanitize_text_field((string) wp_unslash($_POST['name'] ?? '')),
|
||||
'description' => sanitize_textarea_field((string) wp_unslash($_POST['description'] ?? '')),
|
||||
];
|
||||
$result = \AKSRT\User\AuthApply::review($user_id, $uuid, $status, $note, get_current_user_id(), $fields);
|
||||
$extra = ['status' => 'pending'];
|
||||
$result = AuthApply::review($user_id, $uuid, $status, $note, get_current_user_id(), $fields);
|
||||
$extra = $this->verify_return_args();
|
||||
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);
|
||||
}
|
||||
@@ -174,21 +223,19 @@ trait UsersMenusVerify {
|
||||
if (!current_user_can('aksrt_review_identity') && !current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
|
||||
check_admin_referer('aksrt_users_delete_verify_' . $uuid);
|
||||
$repo = new \AKSRT\Identity\ApplicationRepository();
|
||||
$extra = $this->verify_return_args();
|
||||
$repo = new ApplicationRepository();
|
||||
$application = $repo->find_by_uuid($uuid);
|
||||
if (
|
||||
!$application
|
||||
|| (string) ($application['type'] ?? '') !== 'general'
|
||||
|| (string) ($application['channel'] ?? '') !== 'manual'
|
||||
) {
|
||||
$this->redirect('aksrt_user_auth', 'verify_failed', ['aksrt_verify_error' => __('认证记录不存在', 'aksrt')]);
|
||||
if (!$application) {
|
||||
$this->redirect('aksrt_user_auth', 'verify_failed', array_merge($extra, ['aksrt_verify_error' => __('认证记录不存在', 'aksrt')]));
|
||||
}
|
||||
$deleted = $repo->delete((int) $application['id']);
|
||||
if (!$deleted) {
|
||||
$this->redirect('aksrt_user_auth', 'verify_failed', ['aksrt_verify_error' => __('认证记录删除失败', 'aksrt')]);
|
||||
if (!$repo->delete((int) $application['id'])) {
|
||||
$this->redirect('aksrt_user_auth', 'verify_failed', array_merge($extra, ['aksrt_verify_error' => __('认证记录删除失败', 'aksrt')]));
|
||||
}
|
||||
(new \AKSRT\Identity\EvidenceService())->delete((array) ($application['evidence_ids'] ?? []));
|
||||
(new \AKSRT\Identity\EventRepository())->delete_for_application((int) $application['id']);
|
||||
$this->redirect('aksrt_user_auth', 'verify_deleted');
|
||||
(new EvidenceService())->delete((array) $application['evidence_ids']);
|
||||
if (!(new EventRepository())->delete_for_application((int) $application['id'])) {
|
||||
$this->redirect('aksrt_user_auth', 'verify_failed', array_merge($extra, ['aksrt_verify_error' => __('认证申请记录已删除,但审核事件清理失败。', 'aksrt')]));
|
||||
}
|
||||
$this->redirect('aksrt_user_auth', 'verify_deleted', $extra);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/** Identity application detail, review and deletion dialogs. @package AKSRT */
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusVerifyDialogs {
|
||||
|
||||
private function render_verify_dialog_head(string $title, string $description): void {
|
||||
echo '<div class="aksrt-verify-dialog__head"><div><h2 id="aksrt-verify-dialog-title">' . esc_html($title) . '</h2>';
|
||||
echo '<p id="aksrt-verify-dialog-description">' . esc_html($description) . '</p></div>';
|
||||
echo '<button type="button" class="aksrt-verify-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭弹窗', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></div>';
|
||||
}
|
||||
|
||||
private function render_verify_context(array $row, array $filters, int $page, string $action): void {
|
||||
echo '<input type="hidden" name="action" value="' . esc_attr($action) . '">';
|
||||
echo '<input type="hidden" name="application_uuid" value="' . esc_attr((string) $row['application_uuid']) . '">';
|
||||
echo '<input type="hidden" name="filter_status" value="' . esc_attr($filters['status']) . '">';
|
||||
echo '<input type="hidden" name="keyword" value="' . esc_attr($filters['keyword']) . '">';
|
||||
echo '<input type="hidden" name="paged" value="' . esc_attr((string) $page) . '">';
|
||||
wp_nonce_field($action . '_' . (string) $row['application_uuid']);
|
||||
}
|
||||
|
||||
private function render_verify_detail_template(array $row, array $evidence, array $filters, int $page): void {
|
||||
$pending = $row['status'] === 'manual_pending';
|
||||
echo '<template id="aksrt-verify-detail-' . (int) $row['id'] . '">';
|
||||
if ($pending) {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-verify-dialog__frame" data-aksrt-verify-review>';
|
||||
$this->render_verify_context($row, $filters, $page, 'aksrt_users_review_verify');
|
||||
echo '<input type="hidden" name="user_id" value="' . (int) $row['user_id'] . '">';
|
||||
} else {
|
||||
echo '<div class="aksrt-verify-dialog__frame">';
|
||||
}
|
||||
$this->render_verify_dialog_head(
|
||||
$pending ? __('审核认证申请', 'aksrt') : __('认证申请详情', 'aksrt'),
|
||||
$pending ? __('先核对申请资料与证明材料,再选择审核结果。', 'aksrt') : __('该申请已处理,以下为申请资料与审核结果。', 'aksrt')
|
||||
);
|
||||
echo '<div class="aksrt-verify-dialog__body">';
|
||||
echo '<div class="aksrt-verify-applicant">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo $this->verify_status_badge((string) $row['status']) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
$meta = [
|
||||
__('用户账号', 'aksrt') => (string) $row['user_login'],
|
||||
__('邮箱', 'aksrt') => (string) $row['user_email'],
|
||||
__('申请时间', 'aksrt') => $this->verify_time((string) $row['created_at']),
|
||||
];
|
||||
if (!$pending) {
|
||||
$reviewer = get_userdata((int) $row['reviewer_id']);
|
||||
$meta[__('审核人', 'aksrt')] = $reviewer ? (string) $reviewer->display_name : '—';
|
||||
$meta[__('审核时间', 'aksrt')] = $this->verify_time((string) ($row['completed_at'] ?? ''));
|
||||
}
|
||||
echo '<dl class="aksrt-verify-meta">';
|
||||
foreach ($meta as $label => $value) {
|
||||
echo '<div><dt>' . esc_html($label) . '</dt><dd>' . esc_html($value !== '' ? $value : '—') . '</dd></div>';
|
||||
}
|
||||
echo '</dl>';
|
||||
$this->render_verify_text_section(__('认证名称', 'aksrt'), (string) $row['public_name']);
|
||||
$this->render_verify_text_section(__('认证简介', 'aksrt'), (string) $row['description']);
|
||||
$this->render_verify_text_section(__('申请说明', 'aksrt'), (string) $row['manual_reason']);
|
||||
$this->render_verify_evidence($evidence);
|
||||
if ($pending) {
|
||||
$this->render_verify_review_fields($row);
|
||||
} else {
|
||||
$this->render_verify_text_section($row['status'] === 'rejected' ? __('拒绝原因', 'aksrt') : __('处理留言', 'aksrt'), (string) $row['review_note']);
|
||||
}
|
||||
echo '</div><div class="aksrt-verify-dialog__foot">';
|
||||
echo '<button type="button" class="button" data-aksrt-dialog-close>' . esc_html($pending ? __('取消', 'aksrt') : __('关闭', 'aksrt')) . '</button>';
|
||||
if ($pending) {
|
||||
echo '<button type="submit" class="button button-primary" data-aksrt-verify-submit disabled'
|
||||
. ' data-idle-label="' . esc_attr__('请选择审核结果', 'aksrt') . '"'
|
||||
. ' data-approve-label="' . esc_attr__('确认通过', 'aksrt') . '"'
|
||||
. ' data-reject-label="' . esc_attr__('确认拒绝', 'aksrt') . '"'
|
||||
. ' data-busy-label="' . esc_attr__('正在提交…', 'aksrt') . '">' . esc_html__('请选择审核结果', 'aksrt') . '</button>';
|
||||
}
|
||||
echo '</div>' . ($pending ? '</form>' : '</div>') . '</template>';
|
||||
}
|
||||
|
||||
private function render_verify_text_section(string $title, string $text): void {
|
||||
echo '<section class="aksrt-verify-section"><h3>' . esc_html($title) . '</h3><div class="aksrt-verify-copy">' . esc_html($text !== '' ? $text : __('未填写', 'aksrt')) . '</div></section>';
|
||||
}
|
||||
|
||||
private function render_verify_evidence(array $evidence): void {
|
||||
echo '<section class="aksrt-verify-section"><h3>' . esc_html__('证明材料', 'aksrt') . '</h3>';
|
||||
if ($evidence === []) {
|
||||
echo '<p class="aksrt-verify-evidence-empty">' . esc_html__('未提供可查看的证明材料,请结合申请说明判断。', 'aksrt') . '</p>';
|
||||
} else {
|
||||
echo '<ul class="aksrt-verify-evidence">';
|
||||
foreach ($evidence as $index => $item) {
|
||||
$label = sprintf(__('材料 %d(新窗口查看原图)', 'aksrt'), $index + 1);
|
||||
echo '<li><a href="' . esc_url($item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url($item['thumbnail_url']) . '" alt="' . esc_attr(sprintf(__('证明材料 %d', 'aksrt'), $index + 1)) . '"><span>' . esc_html($label) . '</span></a></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
echo '</section>';
|
||||
}
|
||||
|
||||
private function render_verify_review_fields(array $row): void {
|
||||
echo '<section class="aksrt-verify-review"><h3>' . esc_html__('审核处理', 'aksrt') . '</h3>';
|
||||
echo '<fieldset class="aksrt-verify-decisions"><legend>' . esc_html__('审核结果(必选)', 'aksrt') . '</legend>';
|
||||
foreach (['verified' => __('通过申请', 'aksrt'), 'rejected' => __('拒绝申请', 'aksrt')] as $status => $label) {
|
||||
echo '<label><input type="radio" name="status" value="' . esc_attr($status) . '" required><span>' . esc_html($label) . '</span></label>';
|
||||
}
|
||||
echo '</fieldset><fieldset class="aksrt-verify-profile" data-aksrt-verify-profile hidden disabled><legend>' . esc_html__('通过后公开展示的认证资料', 'aksrt') . '</legend>';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('认证名称(必填)', 'aksrt') . '</span>';
|
||||
echo '<input type="text" name="name" maxlength="64" value="' . esc_attr((string) $row['public_name']) . '" required data-required-error="' . esc_attr__('请填写认证名称', 'aksrt') . '"></label>';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('认证简介', 'aksrt') . '</span><textarea name="description" rows="3">' . esc_textarea((string) $row['description']) . '</textarea></label>';
|
||||
echo '<p class="aksrt-native-field__hint">' . esc_html__('通过审核前可修订名称与简介,原申请说明和证明材料不会被修改。', 'aksrt') . '</p></fieldset>';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label" data-aksrt-verify-note-label data-optional="' . esc_attr__('处理留言(选填)', 'aksrt') . '" data-required="' . esc_attr__('拒绝原因(必填)', 'aksrt') . '">' . esc_html__('处理留言(选填)', 'aksrt') . '</span>';
|
||||
echo '<textarea name="note" rows="3" data-required-error="' . esc_attr__('请填写拒绝原因', 'aksrt') . '" aria-describedby="aksrt-verify-note-hint"></textarea></label>';
|
||||
echo '<p id="aksrt-verify-note-hint" class="aksrt-native-field__hint">' . esc_html__('审核结果会通知申请用户;拒绝时请说明原因,便于用户修改后重新申请。', 'aksrt') . '</p></section>';
|
||||
}
|
||||
|
||||
private function render_verify_delete_template(array $row, array $filters, int $page): void {
|
||||
echo '<template id="aksrt-verify-delete-' . (int) $row['id'] . '"><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-verify-dialog__frame" data-aksrt-verify-delete-form>';
|
||||
$this->render_verify_context($row, $filters, $page, 'aksrt_users_delete_verify');
|
||||
$this->render_verify_dialog_head(__('删除认证申请记录', 'aksrt'), __('请核对申请信息。此操作不可撤销。', 'aksrt'));
|
||||
echo '<div class="aksrt-verify-dialog__body"><div class="aksrt-verify-applicant">' . NativeUi::user_cell((int) $row['user_id'], (string) $row['display_name']); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo $this->verify_status_badge((string) $row['status']) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
$this->render_verify_text_section(__('认证名称', 'aksrt'), (string) $row['public_name']);
|
||||
echo '<p class="aksrt-native-callout is-danger">' . esc_html__('将永久删除该申请记录、证明材料及审核事件。删除记录不会撤销用户已获得的认证。', 'aksrt') . '</p>';
|
||||
echo '</div><div class="aksrt-verify-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close autofocus>' . esc_html__('取消', 'aksrt') . '</button>';
|
||||
echo '<button type="submit" class="button is-danger" data-aksrt-verify-submit data-idle-label="' . esc_attr__('确认删除', 'aksrt') . '" data-busy-label="' . esc_attr__('正在提交…', 'aksrt') . '">' . esc_html__('确认删除', 'aksrt') . '</button></div></form></template>';
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ final class EvidenceService {
|
||||
/** @return array<int,array{id:int,url:string,thumbnail_url:string}> */
|
||||
public function items(array $application, bool $admin = false): array {
|
||||
$owner_id = (int) ($application['user_id'] ?? 0);
|
||||
if ($owner_id <= 0 || ($admin ? !current_user_can('aksrt_review_identity') : get_current_user_id() !== $owner_id)) {
|
||||
if ($owner_id <= 0 || ($admin ? !$this->can_review() : get_current_user_id() !== $owner_id)) {
|
||||
return [];
|
||||
}
|
||||
$items = [];
|
||||
@@ -111,7 +111,7 @@ final class EvidenceService {
|
||||
wp_die(esc_html__('安全校验失败', 'aksrt'), '', ['response' => 403]);
|
||||
}
|
||||
$owner = (int) get_post_meta($id, self::OWNER_META, true);
|
||||
if ($owner <= 0 || (get_current_user_id() !== $owner && !current_user_can('aksrt_review_identity'))) {
|
||||
if ($owner <= 0 || (get_current_user_id() !== $owner && !$this->can_review())) {
|
||||
wp_die(esc_html__('您没有权限查看该证明材料', 'aksrt'), '', ['response' => 403]);
|
||||
}
|
||||
$path = get_attached_file($id);
|
||||
@@ -132,6 +132,10 @@ final class EvidenceService {
|
||||
exit;
|
||||
}
|
||||
|
||||
private function can_review(): bool {
|
||||
return current_user_can('aksrt_review_identity') || current_user_can('manage_options');
|
||||
}
|
||||
|
||||
/** @return array<int,array<string,mixed>>|\WP_Error */
|
||||
private function normalize($input) {
|
||||
if (!is_array($input) || empty($input['name'])) {
|
||||
|
||||
Reference in new issue
Block a user