347 lines
18 KiB
PHP
347 lines
18 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;
|
||
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);
|
||
}
|
||
}
|