225 lines
15 KiB
PHP
225 lines
15 KiB
PHP
<?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;
|
||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE status = %s", $status));
|
||
}
|
||
|
||
public function render_verify_page(): void {
|
||
$this->ensure_cap();
|
||
$filters = $this->get_verify_filters();
|
||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||
$result = $this->query_verify_requests($filters, $page_num, 20);
|
||
$counts = $this->get_verify_status_counts();
|
||
echo '<div class="wrap"><h1>' . esc_html__('身份认证', 'aksrt') . '</h1>';
|
||
$this->flash();
|
||
echo '<p>' . esc_html(sprintf(__('人工待审 %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;
|
||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||
$counts = ['pending' => 0, 'verified' => 0, 'rejected' => 0, 'cancelled' => 0, 'total' => 0];
|
||
$rows = $wpdb->get_results("SELECT status, COUNT(*) total FROM {$table} GROUP BY status", ARRAY_A);
|
||
foreach ((array) $rows as $row) {
|
||
$status = (string) ($row['status'] ?? '');
|
||
$total = (int) ($row['total'] ?? 0);
|
||
$public = \AKSRT\Identity\TransitionService::public_status($status);
|
||
if (in_array($status, ['manual_pending', 'provider_failed', 'provider_unsupported', 'provider_expired'], true)) {
|
||
$counts['pending'] += $total;
|
||
} elseif (isset($counts[$public])) {
|
||
$counts[$public] += $total;
|
||
}
|
||
$counts['total'] += $total;
|
||
}
|
||
return $counts;
|
||
}
|
||
|
||
/** @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;
|
||
$table = \AKSRT\Core\DBMigrator::table('identity_application');
|
||
$where = ['1=1'];
|
||
$params = [];
|
||
if ($filters['status'] === 'pending') {
|
||
$where[] = "a.status IN ('manual_pending','provider_failed','provider_unsupported','provider_expired')";
|
||
} elseif ($filters['status'] !== '') {
|
||
$where[] = 'a.status = %s';
|
||
$params[] = $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.application_uuid LIKE %s)';
|
||
array_push($params, $like, $like, $like, $like, $like, $like);
|
||
}
|
||
if ($filters['date_from'] !== '') {
|
||
$where[] = 'a.created_at >= %s';
|
||
$params[] = $filters['date_from'] . ' 00:00:00';
|
||
}
|
||
if ($filters['date_to'] !== '') {
|
||
$where[] = 'a.created_at <= %s';
|
||
$params[] = $filters['date_to'] . ' 23:59:59';
|
||
}
|
||
$where_sql = implode(' AND ', $where);
|
||
$total_sql = "SELECT COUNT(*) FROM {$table} a INNER JOIN {$wpdb->users} u ON u.ID = a.user_id WHERE {$where_sql}";
|
||
$total = $params ? (int) $wpdb->get_var($wpdb->prepare($total_sql, $params)) : (int) $wpdb->get_var($total_sql);
|
||
$query_params = array_merge($params, [$per_page, ($page_num - 1) * $per_page]);
|
||
$sql = "SELECT a.*, u.display_name, u.user_login, u.user_email FROM {$table} a INNER JOIN {$wpdb->users} u ON u.ID = a.user_id WHERE {$where_sql} ORDER BY a.created_at DESC, a.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];
|
||
}
|
||
|
||
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']) . '" /></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>';
|
||
}
|
||
|
||
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></tr></thead><tbody>';
|
||
if ($items === []) {
|
||
echo '<tr><td colspan="5">' . esc_html__('暂无认证申请', 'aksrt') . '</td></tr>';
|
||
}
|
||
foreach ($items as $row) {
|
||
$this->render_verify_row($row);
|
||
}
|
||
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;
|
||
$history = (new \AKSRT\Identity\EventRepository())->for_application((int) $row['id']);
|
||
echo '<tr><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 ' . $user_id . '</div><div>' . esc_html((string) $row['user_email']) . '</div></td>';
|
||
echo '<td>' . wp_kses_post($this->verify_status_badge($status)) . '<div class="description">' . esc_html((string) $row['provider']) . ' / ' . esc_html((string) $row['channel']) . '</div></td>';
|
||
echo '<td><strong>' . esc_html((string) $row['public_name']) . '</strong><div>' . esc_html((string) $row['type']) . '</div>';
|
||
echo '<div class="description">UUID: ' . esc_html((string) $row['application_uuid']) . '</div>';
|
||
if (!empty($row['provider_result_code'])) echo '<div>' . esc_html__('云结果:', 'aksrt') . esc_html((string) $row['provider_result_code']) . '</div>';
|
||
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:8px 8px 0 0;border-radius:4px;" /></a>';
|
||
}
|
||
echo '</td><td>';
|
||
if (!empty($row['review_note'])) echo '<div>' . esc_html((string) $row['review_note']) . '</div>';
|
||
if ($history !== []) echo '<details><summary>' . esc_html(sprintf(__('事件记录(%d)', 'aksrt'), count($history))) . '</summary><ol>';
|
||
foreach ($history as $event) echo '<li>' . esc_html((string) $event['created_at'] . ' · ' . (string) $event['event_type'] . ' · ' . (string) $event['to_status']) . '</li>';
|
||
if ($history !== []) echo '</ol></details>';
|
||
echo '</td><td>';
|
||
$this->render_verify_actions($row);
|
||
echo '</td></tr>';
|
||
}
|
||
|
||
private function verify_status_badge(string $status): string {
|
||
$labels = ['manual_pending' => __('人工待审', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已驳回', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), 'provider_pending' => __('等待核身', 'aksrt'), 'provider_processing' => __('云端处理中', 'aksrt'), 'provider_failed' => __('云认证失败', 'aksrt'), 'provider_unsupported' => __('云端不支持', 'aksrt'), 'provider_expired' => __('会话已过期', 'aksrt')];
|
||
return '<span style="display:inline-block;padding:2px 8px;border:1px solid currentColor;border-radius:999px;">' . esc_html($labels[$status] ?? $status) . '</span>';
|
||
}
|
||
|
||
private function render_verify_actions(array $row): void {
|
||
$status = (string) $row['status'];
|
||
if (in_array($status, ['manual_pending', 'provider_failed', 'provider_unsupported', 'provider_expired'], true)) {
|
||
foreach (['verified' => __('通过认证', 'aksrt'), 'rejected' => __('驳回申请', 'aksrt')] as $next => $label) {
|
||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:grid;gap:6px;margin-bottom:8px;">';
|
||
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="expected_status" value="' . esc_attr($status) . '" /><input type="hidden" name="status" value="' . esc_attr($next) . '" />';
|
||
wp_nonce_field('aksrt_users_review_verify_' . (int) $row['user_id']);
|
||
echo '<textarea name="note" rows="2" ' . ($next === 'rejected' ? 'required' : '') . '></textarea><button class="button ' . ($next === 'verified' ? 'button-primary' : '') . '">' . esc_html($label) . '</button></form>';
|
||
}
|
||
} elseif ($status === 'verified') {
|
||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '"><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="expected_status" value="verified" /><input type="hidden" name="status" value="cancelled" />';
|
||
wp_nonce_field('aksrt_users_review_verify_' . (int) $row['user_id']);
|
||
echo '<textarea name="note" required></textarea><button class="button button-link-delete">' . esc_html__('取消认证', 'aksrt') . '</button></form>';
|
||
} else {
|
||
echo '<span class="description">' . esc_html__('无需人工操作', 'aksrt') . '</span>';
|
||
}
|
||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-top:8px;" 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>';
|
||
}
|
||
|
||
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 = (int) ($_POST['user_id'] ?? 0);
|
||
check_admin_referer('aksrt_users_review_verify_' . $user_id);
|
||
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
|
||
$expected = sanitize_key((string) wp_unslash($_POST['expected_status'] ?? ''));
|
||
$status = sanitize_key((string) wp_unslash($_POST['status'] ?? ''));
|
||
$note = sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? ''));
|
||
$application = (new \AKSRT\Identity\ApplicationRepository())->find_by_uuid($uuid);
|
||
$result = (!$application || (int) $application['user_id'] !== $user_id || (string) $application['status'] !== $expected)
|
||
? new \WP_Error('aksrt_verify_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'))
|
||
: \AKSRT\User\AuthApply::review($user_id, $status, $note, get_current_user_id());
|
||
$extra = ['status' => $status === 'cancelled' ? 'cancelled' : 'pending'];
|
||
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);
|
||
}
|
||
|
||
public function handle_delete_verify(): void {
|
||
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();
|
||
$application = $repo->find_by_uuid($uuid);
|
||
if (!$application) {
|
||
$this->redirect('aksrt_user_auth', 'verify_failed', ['aksrt_verify_error' => __('认证记录不存在', 'aksrt')]);
|
||
}
|
||
(new \AKSRT\Identity\EvidenceService())->delete((array) ($application['evidence_ids'] ?? []));
|
||
(new \AKSRT\Identity\EventRepository())->delete_for_application((int) $application['id']);
|
||
$deleted = $repo->delete((int) $application['id']);
|
||
$this->redirect('aksrt_user_auth', $deleted ? 'verify_deleted' : 'verify_failed', $deleted ? [] : ['aksrt_verify_error' => __('认证记录删除失败', 'aksrt')]);
|
||
}
|
||
|
||
private function get_verify_history(int $user_id): array {
|
||
return \AKSRT\User\AuthApply::get_review_history($user_id);
|
||
}
|
||
}
|