Merge pull request 'fix(identity): complete native certification review flow' (#321) from worktree-fix-identity-certification-flow into main
Reviewed-on: CYL/wordpress-theme-aksrt#321
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)),
|
||||
|
||||
+193
-15
@@ -15,6 +15,10 @@ class AuthApply {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user