Files
wordpress-theme-aksrt/src/AKSRT/Admin/Native/UsersMenusVerify.php
T

347 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}