feat(identity): align auth flow with reference theme

This commit is contained in:
2026-10-04 21:10:12 +08:00
parent 5a17b399a0
commit 59901b9e10
34 changed files with 753 additions and 3871 deletions
+163 -204
View File
@@ -1,9 +1,5 @@
<?php
/**
* Native WordPress identity certification operations.
*
* @package AKSRT
*/
/** Native WordPress identity application review. @package AKSRT */
namespace AKSRT\Admin\Native;
@@ -11,214 +7,177 @@ 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));
}
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
));
}
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>';
}
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();
echo '<div class="wrap"><h1>' . esc_html__('身份认证', 'aksrt') . '</h1>';
$this->flash();
printf('<p>%s</p>', esc_html(sprintf(__('待审核 %1$d · 已通过 %2$d · 已拒绝 %3$d', 'aksrt'), $counts['pending'], $counts['approved'], $counts['rejected'])));
$this->render_verify_filters($filters);
$this->render_verify_table($data['items']);
$this->pagination($data['pages'], $page);
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'] ?? ''),
];
}
/** @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';
return [
'status' => $status,
'keyword' => isset($_GET['keyword']) ? sanitize_text_field((string) wp_unslash($_GET['keyword'])) : '',
];
}
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,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{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} */
private function query_verify_requests(array $filters, int $page, int $per_page): array {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('identity_application');
$where = ["a.type = 'general'", "a.channel = 'manual'"];
$params = [];
if ($filters['status'] !== '') {
$where[] = 'a.status = %s';
$params[] = $filters['status'] === 'pending' ? 'manual_pending' : ($filters['status'] === 'approved' ? 'verified' : $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";
$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))];
}
/** @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'), '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));
}
echo '</select></label><label>' . esc_html__('搜索', 'aksrt') . '<br><input type="search" name="keyword" value="' . esc_attr($filters['keyword']) . '" /></label>';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
}
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><th>审核</th></tr></thead><tbody>';
if ($items === []) echo '<tr><td colspan="6">' . esc_html__('暂无认证申请', 'aksrt') . '</td></tr>';
foreach ($items as $row) $this->render_verify_row($row);
echo '</tbody></table>';
}
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;
echo '<tr><td><strong><a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html((string) $row['display_name']) . '</a></strong><div>@' . esc_html((string) $row['user_login']) . ' · UID ' . $user_id . '</div><div>' . esc_html((string) $row['user_email']) . '</div></td>';
echo '<td>' . esc_html($this->verify_status_label($status)) . '<div class="description">' . esc_html((string) $row['created_at']) . '</div></td>';
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_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_label(string $status): string {
$labels = ['manual_pending' => __('待审核', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已拒绝', 'aksrt')];
return (string) ($labels[$status] ?? $status);
}
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 ($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 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 = absint($_POST['user_id'] ?? 0);
check_admin_referer('aksrt_users_review_verify_' . $user_id);
$uuid = sanitize_text_field((string) wp_unslash($_POST['application_uuid'] ?? ''));
$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'];
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_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, $uuid, $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);
}
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
|| (string) ($application['type'] ?? '') !== 'general'
|| (string) ($application['channel'] ?? '') !== 'manual'
) {
$this->redirect('aksrt_user_auth', 'verify_failed', ['aksrt_verify_error' => __('认证记录不存在', 'aksrt')]);
}
$deleted = $repo->delete((int) $application['id']);
if (!$deleted) {
$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']);
$this->redirect('aksrt_user_auth', 'verify_deleted');
}
}
+6 -40
View File
@@ -538,49 +538,15 @@ class SchemaUsers {
'verify' => [
'key' => 'verify',
'title' => '身份认证',
'version' => '1.1.0',
'version' => '2.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'display', 'title' => '公开展示', 'fields' => [
['key' => 'user_verify_enabled', 'label' => '启用身份认证', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_public_display', 'label' => '公开页显示认证徽标', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_personal_badge_text', 'label' => '个人认证徽标文案', 'type' => 'input', 'default' => '个人认证'],
['key' => 'user_verify_enterprise_badge_text', 'label' => '企业认证徽标文案', 'type' => 'input', 'default' => '企业认证'],
]],
['key' => 'workflow', 'title' => '认证方式', 'fields' => [
['type' => 'alert', 'content' => '个人认证使用人脸实人认证;企业认证需先通过企业要素与法人关系核验,再由法定代表人完成人脸核身。云端失败可按设置转人工审核。'],
['key' => 'user_verify_provider', 'label' => '当前认证服务商', 'type' => 'select', 'default' => 'aliyun', 'options' => [
['label' => '阿里云', 'value' => 'aliyun'],
['label' => '腾讯云', 'value' => 'tencent'],
]],
['key' => 'user_verify_allow_manual_fallback', 'label' => '云认证失败后允许转人工', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_session_ttl_minutes', 'label' => '云认证会话有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
['key' => 'user_verify_poll_interval_seconds', 'label' => '结果轮询间隔(秒)', 'type' => 'number', 'default' => 5, 'min' => 3, 'max' => 60],
['key' => 'user_verify_max_retries', 'label' => '技术故障允许重试次数', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 3],
]],
['key' => 'aliyun', 'title' => '阿里云流程参数', 'fields' => [
['key' => 'user_verify_aliyun_region', 'label' => 'Region', 'type' => 'input', 'default' => 'cn-hangzhou'],
['key' => 'user_verify_aliyun_personal_flow', 'label' => '个人认证 SceneId', 'type' => 'input', 'default' => ''],
['key' => 'user_verify_aliyun_product_code', 'label' => '个人认证 ProductCode', 'type' => 'select', 'default' => 'ID_PRO', 'options' => [
['label' => '实人认证(ID_PRO)', 'value' => 'ID_PRO'],
['label' => '活体人脸验证(PV_FV)', 'value' => 'PV_FV'],
['label' => '活体检测(LR_FR)', 'value' => 'LR_FR'],
]],
['key' => 'user_verify_aliyun_enterprise_flow', 'label' => '企业认证场景/业务标识', 'type' => 'input', 'default' => ''],
]],
['key' => 'tencent', 'title' => '腾讯云流程参数', 'fields' => [
['key' => 'user_verify_tencent_region', 'label' => 'Region', 'type' => 'input', 'default' => 'ap-guangzhou'],
['key' => 'user_verify_tencent_personal_flow', 'label' => '个人认证 RuleId/流程标识', 'type' => 'input', 'default' => ''],
['key' => 'user_verify_tencent_enterprise_flow', 'label' => '企业认证流程标识', 'type' => 'input', 'default' => ''],
]],
['key' => 'apply', 'title' => '申请设置', 'fields' => [
['key' => 'user_verify_allow_apply', 'label' => '允许用户前台申请', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_disable_desc', 'label' => '停用申请说明', 'type' => 'textarea', 'default' => "由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员"],
['key' => 'user_verify_personal_fee', 'label' => '个人认证每次扣余额(元)', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0.00', 'desc' => '仅接受最多两位小数的十进制字符串。'],
['key' => 'user_verify_personal_free_applications', 'label' => '个人认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次个人认证申请免费,之后按单次费用扣余额。'],
['key' => 'user_verify_enterprise_fee', 'label' => '企业认证每次扣余额(元)', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0.00', 'desc' => '仅接受最多两位小数的十进制字符串。'],
['key' => 'user_verify_enterprise_free_applications', 'label' => '企业认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次企业认证申请免费,之后按单次费用扣余额。'],
['key' => 'user_verify_desc', 'label' => '申请介绍', 'type' => 'textarea', 'default' => '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'],
['key' => 'user_auth_s', 'label' => '启用身份认证', 'type' => 'switch', 'default' => true],
['key' => 'user_auth_apply_s', 'label' => '允许用户前台申请', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'user_auth_s', 'value' => true]],
['key' => 'user_auth_apply_disable_desc', 'label' => '停用申请说明', 'type' => 'textarea', 'default' => "由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员", 'dependsOn' => ['key' => 'user_auth_apply_s', 'value' => false]],
['key' => 'user_auth_apply_limit_level', 'label' => '申请最低用户等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 30, 'desc' => '设置为 0 时不限制用户等级。', 'dependsOn' => ['key' => 'user_auth_apply_s', 'value' => true]],
['key' => 'user_auth_apply_desc', 'label' => '申请介绍', 'type' => 'textarea', 'default' => '请填写认证身份、简介和申请说明,提交后由管理员审核。', 'dependsOn' => ['key' => 'user_auth_apply_s', 'value' => true]],
]],
],
],
-21
View File
@@ -9,8 +9,6 @@ defined('ABSPATH') || exit;
class UserController extends ControllerBase {
use UserControllerReportOperations;
use UserControllerVerifyIntegrations;
use UserControllerIdentitySettings;
use UserControllerAuthPhoneOperations;
use UserControllerRestrictionOperations;
use UserControllerFollowOperations;
@@ -234,25 +232,6 @@ class UserController extends ControllerBase {
'permission_callback' => '__return_true',
]);
// Identity verification provider settings (admin only)
register_rest_route(self::API_NAMESPACE, '/identity/settings', [
[
'methods' => 'GET',
'callback' => [$this, 'get_identity_settings'],
'permission_callback' => [$this, 'check_permission'],
],
[
'methods' => 'POST',
'callback' => [$this, 'update_identity_settings'],
'permission_callback' => [$this, 'check_permission'],
],
]);
register_rest_route(self::API_NAMESPACE, '/identity/settings/test', [
'methods' => 'POST',
'callback' => [$this, 'test_identity_settings'],
'permission_callback' => [$this, 'check_permission'],
]);
// SMS provider status
register_rest_route(self::API_NAMESPACE, '/sms/status', [
'methods' => 'GET',
@@ -1,52 +0,0 @@
<?php
/**
* Admin REST handlers for identity provider configuration.
*
* @package AKSRT
*/
namespace AKSRT\Admin;
use AKSRT\Settings\IdentityAdmin;
defined('ABSPATH') || exit;
trait UserControllerIdentitySettings {
public function get_identity_settings(): \WP_REST_Response {
return $this->success([
'data' => $this->identity_settings_response(IdentityAdmin::read()),
]);
}
public function update_identity_settings(\WP_REST_Request $request): \WP_REST_Response {
$settings = IdentityAdmin::update($this->get_json_params($request));
return $this->success([
'message' => __('身份认证服务商配置已保存', 'aksrt'),
'data' => $this->identity_settings_response($settings),
]);
}
public function test_identity_settings(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$provider = sanitize_key((string) ($params['provider'] ?? ''));
$result = IdentityAdmin::test($provider);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('配置检查失败', 'aksrt')), 422);
}
return $this->success([
'message' => (string) $result['message'],
'data' => ['provider' => $provider, 'configured' => true],
]);
}
private function identity_settings_response(array $settings): array {
$settings['callback_url'] = [
'aliyun' => rest_url(self::API_NAMESPACE . '/identity/callback/aliyun'),
'tencent' => rest_url(self::API_NAMESPACE . '/identity/callback/tencent'),
];
return $settings;
}
}
@@ -1,105 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait UserControllerVerifyIntegrations {
/**
* GET /sms/status — SMS provider connection status.
*/
public function get_sms_status(): \WP_REST_Response {
if (class_exists('\AKSRT\Sms\SMS')) {
return $this->success(['data' => \AKSRT\Sms\SMS::get_instance()->get_provider_status()]);
}
return $this->success([
'data' => [
'enabled' => false,
'provider' => 'none',
'configured' => false,
'available' => false,
'verified' => false,
'transport' => '',
'missing_fields' => [],
'code_length' => 6,
'code_expire' => 300,
'rate_limit' => 60,
'daily_limit' => 10,
'notify_bind' => true,
'notify_login' => false,
],
]);
}
/**
* POST /sms/test — Send a test SMS to verify provider configuration.
*/
public function test_sms_send(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Sms\SMS')) {
return $this->error('短信服务不可用');
}
$params = $this->get_json_params($request);
$phone = sanitize_text_field((string) ($params['phone'] ?? ''));
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
return $this->error('手机号格式不正确');
}
$result = \AKSRT\Sms\SMS::get_instance()->test_provider($phone);
if (!empty($result['success'])) {
return $this->success(['message' => $result['message'] ?? '发送成功']);
}
return $this->error($result['message'] ?? '发送失败');
}
/**
* GET /wechat/status — WeChat notification service status.
*/
public function get_wechat_status(): \WP_REST_Response {
if (class_exists('\AKSRT\Payment\WeChatNotify')) {
return $this->success(['data' => \AKSRT\Payment\WeChatNotify::get_instance()->get_status()]);
}
return $this->success([
'data' => [
'appid' => '',
'configured' => false,
'has_token' => false,
'available' => false,
],
]);
}
/**
* POST /wechat/settings — 更新微信通知设置
*/
public function update_wechat_settings(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$settings = [
'enabled' => !empty($params['enabled']),
'app_id' => sanitize_text_field((string) ($params['app_id'] ?? '')),
'app_secret' => sanitize_text_field((string) ($params['app_secret'] ?? '')),
'template_id' => sanitize_text_field((string) ($params['template_id'] ?? '')),
];
$existing = \AKSRT\Settings\Settings::get_wechat_settings();
if ($settings['app_secret'] === '***') {
$settings['app_secret'] = $existing['app_secret'] ?? '';
}
$merged = array_merge($existing, $settings);
\AKSRT\Settings\Settings::update_wechat_settings($merged);
delete_transient('aksrt_wechat_token');
return $this->success(['message' => __('微信设置已保存', 'aksrt')]);
}
}