fix(identity): complete native certification review flow

This commit is contained in:
2026-07-29 12:25:58 +08:00
parent 32658be87d
commit 2dfaa10798
16 changed files with 571 additions and 1167 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ class ApiClient {
// Dual-signature support:
// request('/forum/overview')
// request({ url: '/aksrt/v1/forum/topics', method: 'GET', params: { limit: 20 } })
// request({ url: '/aksrt/v1/verify/review', method: 'POST', data: { user_id: 1 } })
// request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' })
let endpoint, method, headers, params, body, rest
if (typeof endpointOrConfig === 'object' && endpointOrConfig !== null) {
+2 -4
View File
@@ -63,15 +63,13 @@ export const usersSection = {
canonicalDomain: 'users',
section: 'credits',
}),
customPage({
schemaPage({
key: 'users-verify',
name: 'UsersVerify',
label: '身份认证',
route: '/users/verify',
component: 'UsersVerify',
status: 'available',
title: '身份认证',
desc: '认证展示、申请门槛与审核策略(审核队列在 用户 → 身份认证)。',
desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。',
canonicalDomain: 'users',
section: 'verify',
}),
@@ -1,387 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader
title="身份认证"
desc="维护公开认证展示和后台审核台,统一查看待审、已通过、已驳回的人工认证记录。"
/>
<n-grid cols="1 s:1 m:2" :x-gap="16" :y-gap="16" responsive="screen">
<n-grid-item>
<n-card size="small" title="当前状态">
<n-space wrap>
<n-tag round :type="form.user_verify_enabled ? 'success' : 'default'">
认证结构:{{ form.user_verify_enabled ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round :type="authSummary.emailVerificationEnabled ? 'info' : 'default'">
邮箱验证:{{ authSummary.emailVerificationEnabled ? '已启用' : '未启用' }}
</n-tag>
<n-tag round type="warning">
待审核:{{ counts.pending }}
</n-tag>
<n-tag round type="success">
已通过:{{ counts.verified }}
</n-tag>
<n-tag round type="default">
已驳回:{{ counts.rejected }}
</n-tag>
</n-space>
</n-card>
</n-grid-item>
<n-grid-item>
<n-card size="small" title="审核说明">
<n-alert type="info" :show-icon="false">
官方认证必须由用户提交申请并经审核通过;邮箱验证仅用于账户安全,不授予官方认证徽标。
</n-alert>
</n-card>
</n-grid-item>
</n-grid>
<n-card class="aksrt-verify-page__card" size="small" title="公开展示与审核开关">
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
<n-form-item label="启用身份认证结构">
<n-switch v-model:value="form.user_verify_enabled" />
</n-form-item>
<n-form-item label="公开显示认证徽标">
<n-switch v-model:value="form.user_verify_public_display" :disabled="!form.user_verify_enabled" />
</n-form-item>
<n-form-item label="认证徽标文案">
<n-input v-model:value="form.user_verify_badge_text" :disabled="!form.user_verify_enabled" maxlength="20" />
</n-form-item>
<n-form-item label="启用人工审核台">
<n-switch v-model:value="form.user_verify_require_manual_review" :disabled="!form.user_verify_enabled" />
</n-form-item>
<n-form-item label="申请类型选项">
<n-input
v-model:value="form.user_verify_apply_option"
type="textarea"
:autosize="{ minRows: 2, maxRows: 6 }"
:disabled="!form.user_verify_enabled"
placeholder="slug|显示名称,每行一条"
/>
</n-form-item>
<n-form-item label="允许用户前台申请">
<n-switch v-model:value="form.user_verify_allow_apply" :disabled="!form.user_verify_enabled" />
</n-form-item>
<n-form-item v-if="!form.user_verify_allow_apply" label="停用申请说明">
<n-input
v-model:value="form.user_verify_disable_desc"
type="textarea"
:autosize="{ minRows: 2, maxRows: 6 }"
:disabled="!form.user_verify_enabled"
/>
</n-form-item>
<n-form-item v-if="form.user_verify_allow_apply" label="申请要求等级">
<n-input-number
v-model:value="form.user_verify_limit_level"
:min="0"
:max="99"
:disabled="!form.user_verify_enabled"
style="width: 180px;"
/>
</n-form-item>
<n-form-item v-if="form.user_verify_allow_apply" label="申请介绍">
<n-input
v-model:value="form.user_verify_desc"
type="textarea"
:autosize="{ minRows: 3, maxRows: 8 }"
:disabled="!form.user_verify_enabled"
/>
</n-form-item>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
</n-form>
</n-card>
<n-card class="aksrt-verify-page__card" size="small" title="认证审核台">
<n-space vertical :size="12">
<n-grid cols="1 s:2 m:4" :x-gap="12" :y-gap="12" responsive="screen">
<n-grid-item>
<n-input v-model:value="filters.keyword" clearable placeholder="搜索用户、邮箱、备注" />
</n-grid-item>
<n-grid-item>
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="审核状态" />
</n-grid-item>
<n-grid-item>
<n-date-picker v-model:value="filters.dateRange" type="daterange" clearable style="width: 100%;" />
</n-grid-item>
<n-grid-item>
<n-button type="primary" block @click="handleSearch">筛选记录</n-button>
</n-grid-item>
</n-grid>
<n-data-table
:columns="columns"
:data="requests"
:loading="reviewLoading"
:bordered="false"
:pagination="pagination"
:row-key="(row) => row.user_id"
/>
</n-space>
</n-card>
<VerifyReviewDrawer
v-model:show="drawerVisible"
:user-id="currentUserId"
@updated="loadRequests"
/>
</div>
</template>
<script setup>
import { computed, h, onMounted, reactive, ref } from 'vue'
import { NButton, NSpace, NTag, useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import VerifyReviewDrawer from './components/VerifyReviewDrawer.vue'
const message = useMessage()
const loading = ref(true)
const saving = ref(false)
const reviewLoading = ref(false)
const requests = ref([])
const drawerVisible = ref(false)
const currentUserId = ref(null)
const form = reactive({
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_badge_text: '已认证',
user_verify_require_manual_review: false,
user_verify_allow_apply: true,
user_verify_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_verify_limit_level: 2,
user_verify_desc: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
user_verify_apply_option: 'real_name|实名认证\norganization|企业认证',
})
const authSummary = reactive({
emailVerificationEnabled: false,
})
const counts = reactive({
pending: 0,
verified: 0,
rejected: 0,
total: 0,
})
const filters = reactive({
keyword: '',
status: '',
dateRange: null,
})
const pager = reactive({
page: 1,
pageSize: 20,
itemCount: 0,
})
const optionsState = reactive({
statuses: [],
})
const statusOptions = computed(() => optionsState.statuses.length
? [{ label: '全部状态', value: '' }, ...optionsState.statuses]
: [
{ label: '全部状态', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '已通过', value: 'verified' },
{ label: '已驳回', value: 'rejected' },
])
const pagination = computed(() => ({
page: pager.page,
pageSize: pager.pageSize,
itemCount: pager.itemCount,
showSizePicker: true,
pageSizes: [10, 20, 50],
onUpdatePage: (page) => {
pager.page = page
loadRequests()
},
onUpdatePageSize: (pageSize) => {
pager.pageSize = pageSize
pager.page = 1
loadRequests()
},
}))
const columns = [
{
title: '用户',
key: 'display_name',
width: 160,
},
{
title: '邮箱',
key: 'email',
minWidth: 220,
ellipsis: { tooltip: true },
},
{
title: '状态',
key: 'status',
width: 100,
render: (row) => {
const typeMap = { pending: 'warning', verified: 'success', rejected: 'error' }
return h(NTag, { size: 'small', round: true, type: typeMap[row.status] || 'default' }, () => row.status)
},
},
{
title: '邮箱验证',
key: 'email_verified',
width: 100,
render: (row) => row.email_verified ? '已完成' : '未完成',
},
{
title: '申请时间',
key: 'requested_at',
width: 170,
},
{
title: '处理时间',
key: 'reviewed_at',
width: 170,
render: (row) => row.reviewed_at || '-',
},
{
title: '历史',
key: 'history_count',
width: 80,
},
{
title: '操作',
key: 'actions',
width: 120,
render: (row) => h(NSpace, { size: 'small' }, () => [
h(NButton, {
size: 'tiny',
type: 'primary',
quaternary: true,
onClick: () => openDrawer(row.user_id),
}, () => '审核'),
]),
},
]
onMounted(async () => {
await load()
})
function extractData(response) {
return response?.data ?? response ?? {}
}
function formatDateValue(value) {
const date = new Date(value)
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
}
function buildParams() {
const params = {
page: pager.page,
per_page: pager.pageSize,
}
if (filters.keyword) params.keyword = filters.keyword
if (filters.status) params.status = filters.status
if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
params.date_from = formatDateValue(filters.dateRange[0])
params.date_to = formatDateValue(filters.dateRange[1])
}
return params
}
async function load() {
loading.value = true
try {
const [schemaRes, authRes] = await Promise.all([
api.getSchemaPage('users', 'verify'),
api.getAuthSettings(),
])
const schemaData = extractData(schemaRes)
const authData = extractData(authRes)
form.user_verify_enabled = schemaData.user_verify_enabled !== false
form.user_verify_public_display = schemaData.user_verify_public_display !== false
form.user_verify_badge_text = schemaData.user_verify_badge_text || '已认证'
form.user_verify_require_manual_review = !!schemaData.user_verify_require_manual_review
form.user_verify_allow_apply = schemaData.user_verify_allow_apply !== false
form.user_verify_disable_desc = schemaData.user_verify_disable_desc
|| '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员'
form.user_verify_limit_level = Number(schemaData.user_verify_limit_level ?? 2)
form.user_verify_desc = schemaData.user_verify_desc
|| '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'
form.user_verify_apply_option = schemaData.user_verify_apply_option
|| 'real_name|实名认证\norganization|企业认证'
authSummary.emailVerificationEnabled = !!authData.registration_email_verification_enabled
await loadRequests()
} catch (error) {
message.error(error?.message || '加载身份认证页失败')
} finally {
loading.value = false
}
}
async function loadRequests() {
reviewLoading.value = true
try {
const res = await api.request('/verify/requests', { params: buildParams() })
const data = res?.data || {}
requests.value = data.items || []
pager.itemCount = data.total || 0
Object.assign(counts, data.counts || {})
optionsState.statuses = data.options?.statuses || []
} catch (error) {
message.error(error?.message || '加载认证审核台失败')
} finally {
reviewLoading.value = false
}
}
async function handleSave() {
saving.value = true
try {
await api.saveSchemaPage('users', 'verify', {
user_verify_enabled: form.user_verify_enabled,
user_verify_public_display: form.user_verify_public_display,
user_verify_badge_text: form.user_verify_badge_text,
user_verify_require_manual_review: form.user_verify_require_manual_review,
user_verify_allow_apply: form.user_verify_allow_apply,
user_verify_disable_desc: form.user_verify_disable_desc,
user_verify_limit_level: Number(form.user_verify_limit_level || 0),
user_verify_desc: form.user_verify_desc,
user_verify_apply_option: form.user_verify_apply_option,
})
message.success('身份认证配置已保存')
} catch (error) {
message.error(error?.message || '保存失败')
} finally {
saving.value = false
}
}
function handleSearch() {
pager.page = 1
loadRequests()
}
function openDrawer(userId) {
currentUserId.value = userId
drawerVisible.value = true
}
</script>
<style scoped>
.aksrt-verify-page__card {
margin-top: 16px;
}
</style>
@@ -1,210 +0,0 @@
<template>
<n-drawer :show="show" :width="680" @update:show="$emit('update:show', $event)">
<n-drawer-content title="认证审核" closable>
<n-spin :show="loading">
<template v-if="detail.request">
<n-descriptions bordered :column="1" size="small" label-placement="left">
<n-descriptions-item label="用户">{{ detail.request.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="邮箱">{{ detail.request.email || '-' }}</n-descriptions-item>
<n-descriptions-item label="当前状态">{{ detail.request.status || '-' }}</n-descriptions-item>
<n-descriptions-item label="邮箱验证">
{{ detail.request.email_verified ? '已完成' : '未完成' }}
</n-descriptions-item>
<n-descriptions-item label="申请时间">{{ detail.request.requested_at || '-' }}</n-descriptions-item>
<n-descriptions-item label="最近处理">{{ detail.request.reviewed_at || '-' }}</n-descriptions-item>
</n-descriptions>
<div class="aksrt-verify-drawer__section">
<div class="aksrt-verify-drawer__title">申请资料</div>
<n-descriptions bordered :column="1" size="small" label-placement="left">
<n-descriptions-item label="认证类型">{{ detail.request.application?.type || '-' }}</n-descriptions-item>
<n-descriptions-item label="认证身份">{{ detail.request.application?.name || '-' }}</n-descriptions-item>
<n-descriptions-item label="身份简介">{{ detail.request.application?.desc || '-' }}</n-descriptions-item>
<n-descriptions-item label="申请说明">{{ detail.request.application?.msg || '-' }}</n-descriptions-item>
</n-descriptions>
<div v-if="detail.request.application?.evidence?.length" class="aksrt-verify-drawer__proofs">
<a v-for="item in detail.request.application.evidence" :key="item.id" :href="item.url" target="_blank" rel="noopener noreferrer">
<img :src="item.thumbnail_url || item.url" alt="认证证明材料" />
</a>
</div>
</div>
<div class="aksrt-verify-drawer__section">
<div class="aksrt-verify-drawer__title">当前备注</div>
<n-alert type="info" :show-icon="false">
{{ detail.request.reason || '暂无备注' }}
</n-alert>
</div>
<div class="aksrt-verify-drawer__section">
<div class="aksrt-verify-drawer__title">审核历史</div>
<div v-if="detail.history?.length" class="aksrt-verify-drawer__timeline">
<div v-for="(entry, index) in detail.history" :key="index" class="aksrt-verify-drawer__timeline-item">
<div class="aksrt-verify-drawer__timeline-head">
<strong>{{ entry.status || '-' }}</strong>
<span>{{ entry.time || '-' }}</span>
</div>
<p>{{ entry.reason || '无备注' }}</p>
<small>{{ entry.reviewer_name || '系统' }}</small>
</div>
</div>
<n-empty v-else description="暂无审核历史" />
</div>
<div class="aksrt-verify-drawer__section">
<div class="aksrt-verify-drawer__title">执行审核</div>
<n-space vertical :size="12">
<n-select v-model:value="form.status" :options="detail.options?.statuses || []" />
<n-input
v-model:value="form.note"
type="textarea"
:rows="4"
placeholder="填写审核说明或驳回原因"
/>
<n-space>
<n-button type="primary" :loading="submitting" @click="handleSubmit">提交审核</n-button>
<n-button @click="loadDetail">刷新详情</n-button>
</n-space>
</n-space>
</div>
</template>
<n-empty v-else description="暂无认证详情" />
</n-spin>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api'
const props = defineProps({
show: { type: Boolean, default: false },
userId: { type: [Number, String], default: null },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const loading = ref(false)
const submitting = ref(false)
const detail = ref({ request: null, history: [], options: { statuses: [] } })
const form = reactive({
status: 'verified',
note: '',
})
watch(
() => [props.show, props.userId],
async ([visible, id]) => {
if (visible && id) {
await loadDetail()
}
if (!visible) {
detail.value = { request: null, history: [], options: { statuses: [] } }
form.status = 'verified'
form.note = ''
}
},
)
async function loadDetail() {
if (!props.userId) {
return
}
loading.value = true
try {
const res = await api.request(`/verify/requests/${props.userId}`)
detail.value = res?.data || { request: null, history: [], options: { statuses: [] } }
form.status = detail.value?.request?.status === 'rejected' ? 'verified' : 'verified'
form.note = ''
} catch (error) {
message.error(error?.message || '加载认证详情失败')
} finally {
loading.value = false
}
}
async function handleSubmit() {
if (!props.userId) {
return
}
submitting.value = true
try {
await api.request(`/verify/requests/${props.userId}/review`, {
method: 'POST',
data: {
status: form.status,
note: form.note,
},
})
message.success('认证审核已提交')
await loadDetail()
emit('updated')
} catch (error) {
message.error(error?.message || '审核失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.aksrt-verify-drawer__section {
margin-top: 20px;
}
.aksrt-verify-drawer__title {
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.aksrt-verify-drawer__proofs {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.aksrt-verify-drawer__proofs img {
display: block;
width: 92px;
height: 92px;
border-radius: 10px;
object-fit: cover;
}
.aksrt-verify-drawer__timeline {
display: grid;
gap: 12px;
}
.aksrt-verify-drawer__timeline-item {
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #f8fafc;
}
.aksrt-verify-drawer__timeline-head {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.aksrt-verify-drawer__timeline-item p {
margin: 0 0 6px;
color: #111827;
white-space: pre-wrap;
}
.aksrt-verify-drawer__timeline-item small {
color: #6b7280;
}
</style>
@@ -1,7 +1,6 @@
export const usersPageLoaders = {
UsersFeatures: () => import('@/pages/users/UsersFeatures.vue'),
UsersSignin: () => import('@/pages/users/UsersSignin.vue'),
UsersVerify: () => import('@/pages/users/UsersVerify.vue'),
UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
UsersWechatTemplate: () => import('@/pages/users/UsersWechatTemplate.vue'),
@@ -2,12 +2,11 @@ export const verifySchema = {
key: 'users.verify',
title: '身份认证',
icon: 'fas fa-id-card',
description: '配置公开认证徽标、申请门槛与审核策略(审核队列在 用户 → 身份认证 原生菜单)。',
description: '配置公开认证徽标、申请类型与申请门槛;审核、驳回和取消认证请前往 WordPress 用户 → 身份认证。',
defaults: {
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_badge_text: '已认证',
user_verify_require_manual_review: false,
user_verify_allow_apply: true,
user_verify_disable_desc:
'由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
@@ -44,13 +43,6 @@ export const verifySchema = {
title: '审核流程',
icon: 'fas fa-tasks',
fields: [
{
key: 'user_verify_require_manual_review',
label: '保留人工审核位',
type: 'switch',
default: false,
dependsOn: { key: 'user_verify_enabled', value: true },
},
{
key: 'user_verify_apply_option',
label: '申请类型选项',
@@ -17,7 +17,8 @@ $settings = class_exists(AuthApply::class) ? AuthApply::settings() : ['enabled'
$status = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_status($user_id) : '';
$info = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_auth_info($user_id) : [];
$apply = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_apply_payload($user_id) : [];
$evidence = class_exists(AuthApply::class) ? AuthApply::get_evidence_items($apply) : [];
$reason = $user_id > 0 && class_exists(AuthApply::class) ? (string) get_user_meta($user_id, AuthApply::REASON_META, true) : '';
$evidence = class_exists(AuthApply::class) ? AuthApply::get_evidence_items($apply, $user_id) : [];
$type_labels = [];
foreach ((array) ($settings['types'] ?? []) as $type) {
$type_labels[(string) ($type['slug'] ?? '')] = (string) ($type['label'] ?? '');
@@ -54,7 +55,13 @@ $can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) &&
<div class="uc-auth-card__notice"><?php echo wp_kses_post(nl2br(esc_html((string) ($settings['disable_desc'] ?? __('暂时不能申请身份认证', 'aksrt'))))); ?></div>
<?php elseif ($blocked_level) : ?>
<div class="uc-auth-card__notice"><?php printf(esc_html__('达到 LV.%d 后即可申请', 'aksrt'), $limit); ?></div>
<?php elseif ($can_apply) : ?>
<?php elseif (in_array($status, ['rejected', 'cancelled'], true) && $reason !== '') : ?>
<div class="uc-auth-card__notice">
<strong><?php echo $status === 'cancelled' ? esc_html__('认证已取消', 'aksrt') : esc_html__('上次申请未通过', 'aksrt'); ?></strong><br>
<?php echo nl2br(esc_html($reason)); ?>
</div>
<?php endif; ?>
<?php if ($can_apply) : ?>
<button type="button" class="btn-primary uc-auth-apply-open" data-auth-modal-open>
<?php echo $status === 'pending' ? esc_html__('查看申请进度', 'aksrt') : esc_html__('申请认证', 'aksrt'); ?>
</button>
+3
View File
@@ -68,6 +68,9 @@ abstract class ControllerBase {
case 'string':
$out[$key] = sanitize_text_field($val);
break;
case 'textarea':
$out[$key] = sanitize_textarea_field($val);
break;
case 'html':
$out[$key] = wp_kses_post($val);
break;
+7 -195
View File
@@ -9,6 +9,8 @@ defined('ABSPATH') || exit;
class UsersMenus {
use UsersMenusVerify;
public const CAPABILITY = 'manage_options';
private static $instance = null;
@@ -159,7 +161,11 @@ class UsersMenus {
return;
}
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
$message = $map[$code];
if ($code === 'verify_failed' && isset($_GET['aksrt_verify_error'])) {
$message = sanitize_text_field((string) wp_unslash($_GET['aksrt_verify_error']));
}
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
}
private function redirect(string $page, string $msg, array $extra = []): void {
@@ -452,200 +458,6 @@ class UsersMenus {
$this->redirect('aksrt_invite_codes', $ok ? 'invite_disabled' : 'invite_failed');
}
/* ------------------------------------------------------------------
* Verify
* ----------------------------------------------------------------*/
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();
echo '<div class="wrap"><h1>' . esc_html__('身份认证', 'aksrt') . '</h1>';
$this->flash();
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$offset = ($page_num - 1) * $per_page;
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_user_auth" />';
echo '<select name="status">';
foreach (['pending' => __('待审核', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已拒绝', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
global $wpdb;
$where = "INNER JOIN {$wpdb->usermeta} status ON status.user_id = u.ID AND status.meta_key = 'aksrt_user_verify_status'";
$params = [];
$sql_where = '';
if ($status !== '') {
$sql_where = ' WHERE status.meta_value = %s';
$params[] = $status;
}
$count_sql = "SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u {$where}{$sql_where}";
$total = $params
? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
: (int) $wpdb->get_var($count_sql);
$sql = "SELECT u.ID, u.display_name, u.user_email, u.user_registered, status.meta_value AS verify_status,
reason.meta_value AS verify_reason
FROM {$wpdb->users} u
{$where}
LEFT JOIN {$wpdb->usermeta} reason ON reason.user_id = u.ID AND reason.meta_key = 'aksrt_user_verify_reason'
{$sql_where}
ORDER BY u.user_registered DESC
LIMIT %d OFFSET %d";
$params[] = $per_page;
$params[] = $offset;
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('邮箱', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('备注', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="6">' . esc_html__('暂无认证申请', 'aksrt') . '</td></tr>';
}
foreach ((array) $items as $row) {
$uid = (int) ($row['ID'] ?? 0);
$st = (string) ($row['verify_status'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $uid) . '</td>';
echo '<td>' . esc_html((string) ($row['display_name'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($row['user_email'] ?? '')) . '</td>';
echo '<td>' . esc_html($st) . '</td>';
$apply = get_user_meta($uid, 'aksrt_user_verify_apply', true);
$apply = is_array($apply) ? $apply : [];
$type_labels = [];
if (class_exists('\AKSRT\User\AuthApply')) {
foreach ((array) \AKSRT\User\AuthApply::settings()['types'] as $type) {
$type_labels[(string) ($type['slug'] ?? '')] = (string) ($type['label'] ?? '');
}
}
echo '<td>';
if ($apply) {
$type_key = sanitize_key((string) ($apply['type'] ?? ''));
echo '<strong>' . esc_html((string) ($apply['name'] ?? '')) . '</strong>';
if ($type_key !== '') {
echo '<div><span class="dashicons dashicons-id" aria-hidden="true"></span> ' . 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((string) $apply['msg']) . '</div>';
}
$evidence_items = class_exists('\AKSRT\User\AuthApply')
? \AKSRT\User\AuthApply::get_evidence_items($apply)
: [];
foreach ($evidence_items as $item) {
echo '<a href="' . esc_url((string) $item['url']) . '" target="_blank" rel="noopener noreferrer"><img src="' . esc_url((string) $item['thumbnail_url']) . '" alt="" style="width:56px;height:56px;object-fit:cover;margin:6px 6px 0 0;border-radius:4px;" /></a>';
}
}
if (!empty($row['verify_reason'])) {
echo '<div class="description">' . esc_html((string) $row['verify_reason']) . '</div>';
}
echo '</td>';
echo '<td>';
if ($st === 'pending') {
foreach (['verified' => __('通过', 'aksrt'), 'rejected' => __('拒绝', 'aksrt')] as $action => $label) {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block;margin-right:4px;">';
echo '<input type="hidden" name="action" value="aksrt_users_review_verify" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $uid) . '" />';
echo '<input type="hidden" name="status" value="' . esc_attr($action) . '" />';
wp_nonce_field('aksrt_users_review_verify_' . $uid);
if ($action === 'rejected') {
echo '<input type="text" name="note" placeholder="' . esc_attr__('拒绝原因', 'aksrt') . '" /> ';
}
submit_button($label, 'small', '', false);
echo '</form>';
}
} else {
echo '—';
}
echo '</td></tr>';
}
echo '</tbody></table>';
$this->pagination($pages, $page_num);
echo '</div>';
}
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);
$ok = false;
$current_status = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true));
$application = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
if (
$user_id > 0
&& in_array($status, ['verified', 'rejected'], true)
&& get_userdata($user_id)
&& $current_status === 'pending'
&& is_array($application)
&& !empty($application['name'])
&& !empty($application['type'])
) {
update_user_meta($user_id, 'aksrt_user_verify_status', $status);
update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql'));
update_user_meta($user_id, 'aksrt_user_verify_reviewer', get_current_user_id());
if ($note !== '') {
update_user_meta($user_id, 'aksrt_user_verify_reason', $note);
} elseif ($status === 'verified') {
delete_user_meta($user_id, 'aksrt_user_verify_reason');
}
// Promote apply payload to public auth info when approved.
if ($status === 'verified') {
$apply = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
if (is_array($apply)) {
update_user_meta($user_id, 'aksrt_user_verify_info', [
'name' => (string) ($apply['name'] ?? ''),
'desc' => (string) ($apply['desc'] ?? ''),
'type' => (string) ($apply['type'] ?? ''),
'time' => current_time('Y-m-d H:i'),
]);
}
}
$log = get_option('aksrt_verify_review_log', []);
$log = is_array($log) ? $log : [];
$log[] = [
'user_id' => $user_id,
'status' => $status,
'reason' => $note,
'reviewer' => get_current_user_id(),
'time' => current_time('mysql'),
];
update_option('aksrt_verify_review_log', array_slice($log, -500), false);
if (class_exists('\AKSRT\Messages\Message')) {
$message = $status === 'verified'
? __('您的官方认证申请已通过审核', 'aksrt')
: sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note !== '' ? $note : __('请修改资料后重新申请', 'aksrt'));
\AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
'title' => __('官方认证审核结果', 'aksrt'),
'target_url' => aksrt_get_user_center_tab_url('auth'),
]);
}
$ok = true;
}
$this->redirect('aksrt_user_auth', $ok ? 'verify_updated' : 'verify_failed', ['status' => 'pending']);
}
/* ------------------------------------------------------------------
* Ban / reports
* ----------------------------------------------------------------*/
+346
View File
@@ -0,0 +1,346 @@
<?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);
}
}
+1 -2
View File
@@ -599,8 +599,7 @@ class SchemaUsers {
['key' => 'user_verify_public_display', 'label' => '公开页显示认证徽标', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_badge_text', 'label' => '认证徽标文案', 'type' => 'input', 'default' => '已认证'],
]],
['key' => 'workflow', 'title' => '审核流程', 'fields' => [
['key' => 'user_verify_require_manual_review', 'label' => '保留人工审核位', 'type' => 'switch', 'default' => false],
['key' => 'workflow', 'title' => '申请类型', 'fields' => [
['key' => 'user_verify_apply_option', 'label' => '申请类型选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证", 'desc' => '每行:slug|显示名称'],
]],
['key' => 'apply', 'title' => '申请设置', 'fields' => [
+3 -1
View File
@@ -394,6 +394,8 @@ class SchemaProvider {
switch ($type) {
case 'html':
return wp_kses_post((string) $value);
case 'textarea':
return sanitize_textarea_field((string) $value);
case 'code':
return is_string($value) ? wp_unslash($value) : '';
case 'url':
@@ -433,7 +435,7 @@ class SchemaProvider {
'switch' => 'bool',
'input' => 'string',
'text' => 'string',
'textarea' => 'string',
'textarea' => 'textarea',
'number' => 'int',
'slider' => 'int',
'select' => 'string',
-29
View File
@@ -253,35 +253,6 @@ class UserController extends ControllerBase {
'permission_callback' => function () { return is_user_logged_in(); },
]);
// Verify: list pending verification requests
register_rest_route(self::API_NAMESPACE, '/verify/pending', [
'methods' => 'GET',
'callback' => [$this, 'get_verify_pending'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/verify/requests', [
'methods' => 'GET',
'callback' => [$this, 'get_verify_requests'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_verify_request_detail'],
'permission_callback' => [$this, 'check_permission'],
]);
// Verify: approve or reject a verification request
register_rest_route(self::API_NAMESPACE, '/verify/review', [
'methods' => 'POST',
'callback' => [$this, 'review_verify_request'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/verify/requests/(?P<id>\d+)/review', [
'methods' => 'POST',
'callback' => [$this, 'review_verify_request_by_id'],
'permission_callback' => [$this, 'check_permission'],
]);
// SMS provider status
register_rest_route(self::API_NAMESPACE, '/sms/status', [
'methods' => 'GET',
@@ -9,311 +9,6 @@ defined('ABSPATH') || exit;
trait UserControllerVerifyIntegrations {
public function get_verify_pending(): \WP_REST_Response {
$response = $this->get_verify_requests(new \WP_REST_Request('GET', '/'));
$data = $response->get_data();
$items = isset($data['data']['items']) && is_array($data['data']['items']) ? $data['data']['items'] : [];
$pending = array_values(array_filter($items, static function ($item) {
return ($item['status'] ?? '') === 'pending';
}));
return $this->success([
'data' => [
'requests' => array_slice($pending, 0, 50),
'total' => count($pending),
],
]);
}
public function get_verify_requests(\WP_REST_Request $request): \WP_REST_Response {
global $wpdb;
$page = max(1, (int) ($request->get_param('page') ?: 1));
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
$status = sanitize_key((string) $request->get_param('status'));
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$sql = "
SELECT u.ID, u.display_name, u.user_email, u.user_registered,
status.meta_value AS verify_status,
reason.meta_value AS verify_reason,
reviewed_at.meta_value AS reviewed_at,
reviewer.meta_value AS reviewer_id
FROM {$wpdb->users} u
INNER JOIN {$wpdb->usermeta} status
ON status.user_id = u.ID AND status.meta_key = 'aksrt_user_verify_status'
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'
ORDER BY u.user_registered DESC
LIMIT 500
";
$rows = $wpdb->get_results($sql, ARRAY_A);
$items = [];
$counts = ['pending' => 0, 'verified' => 0, 'rejected' => 0, 'total' => 0];
foreach ((array) $rows as $row) {
$item = $this->build_verify_request_item($row);
$item_status = $item['status'];
if (isset($counts[$item_status])) {
$counts[$item_status]++;
}
$counts['total']++;
if ($status !== '' && $item_status !== $status) {
continue;
}
if ($date_from !== '' && strcmp((string) $item['requested_at'], $date_from . ' 00:00:00') < 0) {
continue;
}
if ($date_to !== '' && strcmp((string) $item['requested_at'], $date_to . ' 23:59:59') > 0) {
continue;
}
if ($keyword !== '' && mb_stripos(implode(' ', [
(string) ($item['display_name'] ?? ''),
(string) ($item['email'] ?? ''),
(string) ($item['reason'] ?? ''),
]), $keyword) === false) {
continue;
}
$items[] = $item;
}
$total = count($items);
$offset = ($page - 1) * $per_page;
return $this->success([
'data' => [
'items' => array_values(array_slice($items, $offset, $per_page)),
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'counts' => $counts,
'options' => [
'statuses' => [
['label' => __('待审核', 'aksrt'), 'value' => 'pending'],
['label' => __('已通过', 'aksrt'), 'value' => 'verified'],
['label' => __('已驳回', 'aksrt'), 'value' => 'rejected'],
],
],
],
]);
}
public function get_verify_request_detail(\WP_REST_Request $request): \WP_REST_Response {
$user_id = (int) $request->get_param('id');
if ($user_id <= 0) {
return $this->error(__('认证申请不存在', 'aksrt'), 404);
}
$user = get_userdata($user_id);
if (!$user) {
return $this->error(__('用户不存在', 'aksrt'), 404);
}
$detail = $this->build_verify_request_item([
'ID' => $user->ID,
'display_name' => $user->display_name,
'user_email' => $user->user_email,
'user_registered' => $user->user_registered,
'verify_status' => get_user_meta($user_id, 'aksrt_user_verify_status', true),
'verify_reason' => get_user_meta($user_id, 'aksrt_user_verify_reason', true),
'reviewed_at' => get_user_meta($user_id, 'aksrt_user_verify_reviewed_at', true),
'reviewer_id' => get_user_meta($user_id, 'aksrt_user_verify_reviewer', true),
]);
return $this->success([
'data' => [
'request' => $detail,
'history' => $this->get_verify_history_for_user($user_id),
'options' => [
'statuses' => [
['label' => __('通过认证', 'aksrt'), 'value' => 'verified'],
['label' => __('驳回申请', 'aksrt'), 'value' => 'rejected'],
],
],
],
]);
}
/**
* POST /verify/review — approve or reject a verification request.
*/
public function review_verify_request(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$user_id = (int) ($params['user_id'] ?? 0);
if ($user_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
return $this->perform_verify_review($user_id, $params);
}
public function review_verify_request_by_id(\WP_REST_Request $request): \WP_REST_Response {
$user_id = (int) $request->get_param('id');
if ($user_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
return $this->perform_verify_review($user_id, $this->get_json_params($request));
}
private function perform_verify_review(int $user_id, array $payload): \WP_REST_Response {
$status = sanitize_key((string) ($payload['status'] ?? ''));
$note = sanitize_textarea_field((string) ($payload['note'] ?? ($payload['reason'] ?? '')));
if (!in_array($status, ['verified', 'rejected'], true)) {
return $this->error(__('审核状态无效', 'aksrt'));
}
$user = get_userdata($user_id);
if (!$user) {
return $this->error(__('用户不存在', 'aksrt'), 404);
}
$current_status = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true));
$application = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
if ($current_status !== 'pending' || !is_array($application) || empty($application['name']) || empty($application['type'])) {
return $this->error(__('该用户没有可审核的认证申请', 'aksrt'), 409);
}
update_user_meta($user_id, 'aksrt_user_verify_status', $status);
update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql'));
update_user_meta($user_id, 'aksrt_user_verify_reviewer', get_current_user_id());
if ($note !== '') {
update_user_meta($user_id, 'aksrt_user_verify_reason', $note);
} elseif ($status === 'verified') {
delete_user_meta($user_id, 'aksrt_user_verify_reason');
}
if ($status === 'verified') {
$apply = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
if (is_array($apply)) {
update_user_meta($user_id, 'aksrt_user_verify_info', [
'name' => (string) ($apply['name'] ?? ''),
'desc' => (string) ($apply['desc'] ?? ''),
'type' => (string) ($apply['type'] ?? ''),
'time' => current_time('Y-m-d H:i'),
]);
}
}
$log = get_option('aksrt_verify_review_log', []);
if (!is_array($log)) {
$log = [];
}
$log[] = [
'user_id' => $user_id,
'reviewer' => get_current_user_id(),
'status' => $status,
'reason' => $note,
'time' => current_time('mysql'),
];
if (count($log) > 500) {
$log = array_slice($log, -500);
}
update_option('aksrt_verify_review_log', $log, false);
if (class_exists('\AKSRT\Messages\Message')) {
$message = $status === 'verified'
? __('您的官方认证申请已通过审核', 'aksrt')
: sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note !== '' ? $note : __('请修改资料后重新申请', 'aksrt'));
\AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
'title' => __('官方认证审核结果', 'aksrt'),
'target_url' => aksrt_get_user_center_tab_url('auth'),
]);
}
return $this->success([
'message' => $status === 'verified'
? __('已通过认证', 'aksrt')
: __('已驳回申请', 'aksrt'),
]);
}
private function build_verify_request_item(array $row): array {
$user_id = (int) ($row['ID'] ?? 0);
$reviewer_id = (int) ($row['reviewer_id'] ?? 0);
$reviewer = $reviewer_id > 0 ? get_userdata($reviewer_id) : null;
$status = sanitize_key((string) ($row['verify_status'] ?? 'pending'));
$email_verified = (bool) get_user_meta($user_id, 'aksrt_email_verified', true);
$application = get_user_meta($user_id, 'aksrt_user_verify_apply', true);
$application = is_array($application) ? $application : [];
$application_view = class_exists('\AKSRT\User\AuthApply')
? \AKSRT\User\AuthApply::application_view($application)
: [];
return [
'user_id' => $user_id,
'display_name' => (string) ($row['display_name'] ?? ''),
'email' => (string) ($row['user_email'] ?? ''),
'status' => $status,
'reason' => (string) ($row['verify_reason'] ?? ''),
'requested_at' => (string) ($application['time'] ?? $row['user_registered'] ?? ''),
'reviewed_at' => (string) ($row['reviewed_at'] ?? ''),
'email_verified' => $email_verified,
'application' => $application_view,
'reviewer' => $this->build_admin_user_summary($reviewer_id, $reviewer),
'history_count' => count($this->get_verify_history_for_user($user_id)),
];
}
private function get_verify_history_for_user(int $user_id): array {
$log = get_option('aksrt_verify_review_log', []);
if (!is_array($log)) {
return [];
}
$history = [];
foreach ($log as $entry) {
if ((int) ($entry['user_id'] ?? 0) !== $user_id) {
continue;
}
$reviewer_id = (int) ($entry['reviewer'] ?? 0);
$history[] = [
'status' => sanitize_key((string) ($entry['status'] ?? '')),
'reason' => (string) ($entry['reason'] ?? ''),
'time' => (string) ($entry['time'] ?? ''),
'reviewer' => $reviewer_id,
'reviewer_name' => $reviewer_id > 0 && ($user = get_userdata($reviewer_id)) ? $user->display_name : '',
];
}
usort($history, static function ($left, $right) {
return strcmp((string) ($right['time'] ?? ''), (string) ($left['time'] ?? ''));
});
return $history;
}
private function build_admin_user_summary(int $user_id, $user = null): ?array {
if ($user_id <= 0 && !$user instanceof \WP_User) {
return null;
}
if (!$user instanceof \WP_User) {
$user = get_userdata($user_id);
}
if (!$user instanceof \WP_User) {
return null;
}
return [
'id' => (int) $user->ID,
'display_name' => (string) $user->display_name,
'email' => (string) $user->user_email,
'avatar_url' => get_avatar_url((int) $user->ID, ['size' => 64]),
];
}
/**
* GET /sms/status — SMS provider connection status.
*/
+1 -2
View File
@@ -142,7 +142,7 @@ class UserRuntime {
* User identity, level, medal, and report governance settings.
*
* @return array{
* verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,require_manual_review:bool,allow_apply:bool,disable_desc:string,limit_level:int,desc:string,apply_option:string},
* verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,allow_apply:bool,disable_desc:string,limit_level:int,desc:string,apply_option:string},
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array<int,int>},
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,icon_url?:string}>},
@@ -162,7 +162,6 @@ class UserRuntime {
'public_display' => !isset($settings['user_verify_public_display']) || !empty($settings['user_verify_public_display']),
'badge_text' => $verify_badge_text !== '' ? $verify_badge_text : __('已认证', 'aksrt'),
'accept_email_verified' => false,
'require_manual_review' => !empty($settings['user_verify_require_manual_review']),
'allow_apply' => !isset($settings['user_verify_allow_apply']) || !empty($settings['user_verify_allow_apply']),
'disable_desc' => (string) ($settings['user_verify_disable_desc'] ?? ''),
'limit_level' => max(0, (int) ($settings['user_verify_limit_level'] ?? 0)),
+197 -19
View File
@@ -11,10 +11,14 @@ defined('ABSPATH') || exit;
class AuthApply {
public const STATUS_META = 'aksrt_user_verify_status';
public const REASON_META = 'aksrt_user_verify_reason';
public const APPLY_META = 'aksrt_user_verify_apply';
public const INFO_META = 'aksrt_user_verify_info';
public const STATUS_META = 'aksrt_user_verify_status';
public const REASON_META = 'aksrt_user_verify_reason';
public const APPLY_META = 'aksrt_user_verify_apply';
public const INFO_META = 'aksrt_user_verify_info';
public const REQUESTED_AT_META = 'aksrt_user_verify_requested_at';
public const REVIEWED_AT_META = 'aksrt_user_verify_reviewed_at';
public const REVIEWER_META = 'aksrt_user_verify_reviewer';
public const HISTORY_META = 'aksrt_user_verify_history';
private static $instance = null;
@@ -27,6 +31,7 @@ class AuthApply {
public function init(): void {
add_action('wp_ajax_aksrt_user_auth_apply', [$this, 'ajax_apply']);
add_action('admin_post_aksrt_verify_evidence', [$this, 'serve_evidence']);
}
/**
@@ -98,44 +103,209 @@ class AuthApply {
}
/**
* Build the only public representation of certification evidence.
* Return certification evidence only to its owner or an administrator.
*
* @param array<string,mixed> $application
* @return array<int,array{id:int,url:string,thumbnail_url:string}>
*/
public static function get_evidence_items(array $application): array {
public static function get_evidence_items(array $application, int $owner_id = 0, bool $admin_context = false): array {
if ($owner_id <= 0) {
return [];
}
$can_view = $admin_context
? current_user_can('manage_options')
: get_current_user_id() === $owner_id;
if (!$can_view) {
return [];
}
$items = [];
foreach ((array) ($application['evidence_attachment_ids'] ?? []) as $attachment_id) {
$attachment_id = (int) $attachment_id;
if ($attachment_id <= 0 || get_post_type($attachment_id) !== 'attachment') {
if (
$attachment_id <= 0
|| get_post_type($attachment_id) !== 'attachment'
|| (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true) !== $owner_id
) {
continue;
}
$url = wp_get_attachment_url($attachment_id);
if (!is_string($url) || $url === '') {
continue;
}
$thumb = wp_get_attachment_image_url($attachment_id, 'thumbnail');
$url = add_query_arg([
'action' => 'aksrt_verify_evidence',
'attachment' => $attachment_id,
'_wpnonce' => wp_create_nonce('aksrt_verify_evidence_' . $attachment_id),
], admin_url('admin-post.php'));
$items[] = [
'id' => $attachment_id,
'url' => esc_url_raw($url),
'thumbnail_url' => esc_url_raw(is_string($thumb) && $thumb !== '' ? $thumb : $url),
'thumbnail_url' => esc_url_raw($url),
];
}
return $items;
}
public function serve_evidence(): void {
if (!is_user_logged_in()) {
wp_die(esc_html__('请先登录', 'aksrt'), '', ['response' => 401]);
}
$attachment_id = isset($_GET['attachment']) ? (int) $_GET['attachment'] : 0;
if ($attachment_id <= 0 || !check_admin_referer('aksrt_verify_evidence_' . $attachment_id)) {
wp_die(esc_html__('安全校验失败', 'aksrt'), '', ['response' => 403]);
}
$owner_id = (int) get_post_meta($attachment_id, '_aksrt_verify_evidence_owner', true);
if ($owner_id <= 0 || (get_current_user_id() !== $owner_id && !current_user_can('manage_options'))) {
wp_die(esc_html__('您没有权限查看该证明材料', 'aksrt'), '', ['response' => 403]);
}
$file_path = get_attached_file($attachment_id);
if (!is_string($file_path) || $file_path === '' || !is_file($file_path) || !is_readable($file_path)) {
wp_die(esc_html__('证明材料不存在', 'aksrt'), '', ['response' => 404]);
}
$mime = (string) get_post_mime_type($attachment_id);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) {
wp_die(esc_html__('证明材料类型无效', 'aksrt'), '', ['response' => 415]);
}
nocache_headers();
header('Content-Type: ' . $mime);
header('Content-Disposition: inline; filename="' . rawurlencode(basename($file_path)) . '"');
header('Content-Length: ' . (string) filesize($file_path));
readfile($file_path);
exit;
}
/** @param array<string,mixed> $application */
public static function application_view(array $application): array {
public static function application_view(array $application, int $owner_id = 0, bool $admin_context = false): array {
return [
'name' => (string) ($application['name'] ?? ''),
'desc' => (string) ($application['desc'] ?? ''),
'msg' => (string) ($application['msg'] ?? ''),
'type' => sanitize_key((string) ($application['type'] ?? '')),
'time' => (string) ($application['time'] ?? ''),
'evidence' => self::get_evidence_items($application),
'evidence' => self::get_evidence_items($application, $owner_id, $admin_context),
];
}
/** @return array<int,array<string,mixed>> */
public static function get_review_history(int $user_id): array {
$history = get_user_meta($user_id, self::HISTORY_META, true);
$history = is_array($history) ? array_values($history) : [];
usort($history, static fn(array $left, array $right): int => strcmp(
(string) ($right['time'] ?? ''),
(string) ($left['time'] ?? '')
));
return $history;
}
/**
* Apply one authoritative certification status transition.
*
* @return array<string,mixed>|\WP_Error
*/
public static function review(int $user_id, string $status, string $note, int $reviewer_id) {
$status = sanitize_key($status);
$note = sanitize_textarea_field($note);
if (!in_array($status, ['verified', 'rejected', 'cancelled'], true)) {
return new \WP_Error('aksrt_verify_status', __('审核状态无效', 'aksrt'));
}
if ($user_id <= 0 || !get_userdata($user_id) || $reviewer_id <= 0) {
return new \WP_Error('aksrt_verify_user', __('用户不存在', 'aksrt'));
}
if ($status === 'cancelled' && $note === '') {
return new \WP_Error('aksrt_verify_note', __('取消认证时必须填写原因', 'aksrt'));
}
if ($status === 'rejected' && $note === '') {
return new \WP_Error('aksrt_verify_note', __('驳回申请时必须填写原因', 'aksrt'));
}
$current = self::get_status($user_id);
$allowed = $status === 'cancelled'
? $current === 'verified'
: $current === 'pending';
$application = self::get_apply_payload($user_id);
if (!$allowed || $application === [] || empty($application['name']) || empty($application['type'])) {
return new \WP_Error('aksrt_verify_transition', __('该认证记录当前不可执行此操作', 'aksrt'));
}
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT umeta_id, meta_value
FROM {$wpdb->usermeta}
WHERE user_id = %d AND meta_key = %s
ORDER BY umeta_id ASC",
$user_id,
self::STATUS_META
), ARRAY_A);
if (count((array) $rows) !== 1 || sanitize_key((string) ($rows[0]['meta_value'] ?? '')) !== $current) {
return new \WP_Error('aksrt_verify_state', __('认证状态数据异常,请修复后重试', 'aksrt'));
}
$updated = $wpdb->query($wpdb->prepare(
"UPDATE {$wpdb->usermeta}
SET meta_value = %s
WHERE umeta_id = %d AND meta_value = %s",
$status,
(int) $rows[0]['umeta_id'],
$current
));
if ($updated !== 1) {
return new \WP_Error('aksrt_verify_conflict', __('认证状态已变化,请刷新后重试', 'aksrt'));
}
$reviewed_at = current_time('mysql');
update_user_meta($user_id, self::REVIEWED_AT_META, $reviewed_at);
update_user_meta($user_id, self::REVIEWER_META, $reviewer_id);
if ($note !== '') {
update_user_meta($user_id, self::REASON_META, $note);
} else {
delete_user_meta($user_id, self::REASON_META);
}
if ($status === 'verified') {
update_user_meta($user_id, self::INFO_META, [
'name' => (string) ($application['name'] ?? ''),
'desc' => (string) ($application['desc'] ?? ''),
'type' => sanitize_key((string) ($application['type'] ?? '')),
'time' => current_time('Y-m-d H:i'),
]);
} elseif ($status === 'cancelled') {
delete_user_meta($user_id, self::INFO_META);
}
$entry = [
'status' => $status,
'reason' => $note,
'reviewer' => $reviewer_id,
'time' => $reviewed_at,
'application' => self::application_view($application),
];
$history = self::get_review_history($user_id);
array_unshift($history, $entry);
update_user_meta($user_id, self::HISTORY_META, array_slice($history, 0, 50));
self::send_review_notification($user_id, $status, $note);
do_action('aksrt_user_auth_reviewed', $user_id, $status, $note, $reviewer_id);
return ['status' => $status, 'reviewed_at' => $reviewed_at];
}
private static function send_review_notification(int $user_id, string $status, string $note): void {
if (!class_exists('\\AKSRT\\Messages\\Message')) {
return;
}
if ($status === 'verified') {
$message = __('您的官方认证申请已通过审核', 'aksrt');
$title = __('官方认证审核结果', 'aksrt');
} elseif ($status === 'cancelled') {
$message = sprintf(__('您的官方认证已被取消:%s', 'aksrt'), $note);
$title = __('官方认证已取消', 'aksrt');
} else {
$message = sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note);
$title = __('官方认证审核结果', 'aksrt');
}
\AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [
'title' => $title,
'target_url' => aksrt_get_user_center_tab_url('auth'),
]);
}
public function ajax_apply(): void {
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
wp_send_json_error(['message' => __('请求方式无效', 'aksrt')], 405);
@@ -222,13 +392,14 @@ class AuthApply {
$this->renew_submission_lock($lock_key, $lock_token);
$previous_payload = self::get_apply_payload($user_id);
$requested_at = current_time('Y-m-d H:i');
$payload = [
'name' => $name,
'desc' => $desc,
'msg' => $msg,
'type' => $type,
'evidence_attachment_ids' => $uploaded,
'time' => current_time('Y-m-d H:i'),
'time' => $requested_at,
];
$payload_saved = update_user_meta($user_id, self::APPLY_META, $payload);
@@ -246,14 +417,17 @@ class AuthApply {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
update_user_meta($user_id, self::REQUESTED_AT_META, $requested_at);
delete_user_meta($user_id, self::REVIEWED_AT_META);
delete_user_meta($user_id, self::REVIEWER_META);
delete_user_meta($user_id, self::REASON_META);
if (!$this->owns_submission_lock($lock_key, $lock_token)) {
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请已提交,请刷新查看最新状态', 'aksrt')], 409);
}
$this->renew_submission_lock($lock_key, $lock_token);
delete_user_meta($user_id, self::REASON_META);
$this->delete_attachments(array_column(self::get_evidence_items($previous_payload), 'id'));
$this->delete_attachments(array_column(self::get_evidence_items($previous_payload, $user_id), 'id'));
/** @param int $user_id @param array $payload */
do_action('aksrt_user_auth_apply_submitted', $user_id, $payload);
@@ -314,7 +488,11 @@ class AuthApply {
$this->delete_attachments($attachment_ids);
return new \WP_Error('aksrt_auth_upload_type', __('证明材料类型校验失败', 'aksrt'));
}
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
wp_update_post([
'ID' => $attachment_id,
'post_author' => $user_id,
'post_status' => 'private',
]);
update_post_meta($attachment_id, '_aksrt_verify_evidence_owner', $user_id);
$attachment_ids[] = $attachment_id;
}