feat(identity): align auth flow with reference theme

This commit is contained in:
2026-10-04 21:10:12 +08:00
parent 5a17b399a0
commit 59901b9e10
34 changed files with 753 additions and 3871 deletions
-8
View File
@@ -17,14 +17,6 @@ export const usersApiMethods = {
deleteNotification(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }) },
runNotificationCleanup() { return this.request('/notifications/cleanup', { method: 'POST', body: JSON.stringify({}) }) },
getIdentitySettings() { return this.request('/identity/settings') },
saveIdentitySettings(data) {
return this.request('/identity/settings', { method: 'POST', body: JSON.stringify(data) })
},
testIdentitySettings(data = {}) {
return this.request('/identity/settings/test', { method: 'POST', body: JSON.stringify(data) })
},
getCreditSettings() { return this.request('/assets/settings') },
saveCreditSettings(data) { return this.request('/assets/settings', { method: 'POST', body: JSON.stringify(data) }) },
listCreditUsers(params = {}) {
+1 -1
View File
@@ -60,7 +60,7 @@ export const usersSection = {
route: '/users/verify',
component: 'UsersVerify',
title: '身份认证',
desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。',
desc: '配置通用认证申请规则;申请审核和认证资料管理位于 WordPress 用户菜单。',
canonicalDomain: 'users',
section: 'verify',
}),
+36 -300
View File
@@ -1,220 +1,52 @@
<template>
<div class="aksrt-page-container">
<PageHeader
title="身份认证"
desc="统一管理认证申请规则与云服务商凭证;审核、驳回和取消认证仍在 WordPress 用户菜单处理。"
/>
<PageHeader title="身份认证" desc="配置通用认证申请规则;申请审核和用户认证资料管理位于 WordPress 用户菜单。" />
<n-spin :show="loading" style="width: 100%; min-height: 240px">
<n-grid :cols="3" :x-gap="12" :y-gap="12" responsive="screen" item-responsive>
<n-grid-item span="3 m:1">
<n-card class="aksrt-status-card" size="small">
<n-statistic label="身份认证功能">
<template #default>{{ verifyForm.user_verify_enabled ? '已启用' : '已停用' }}</template>
</n-statistic>
<n-tag :type="verifyForm.user_verify_enabled ? 'success' : 'default'" round size="small">
{{ verifyForm.user_verify_enabled ? '运行中' : '未运行' }}
</n-tag>
</n-card>
</n-grid-item>
<n-grid-item span="3 m:1" v-for="key in providerKeys" :key="key">
<n-card class="aksrt-status-card" size="small">
<n-statistic :label="providerLabel(key) + '凭证'">
<template #default>{{ providerState(key).configured ? '已配置' : '未配置' }}</template>
</n-statistic>
<n-tag :type="providerState(key).configured ? 'success' : 'warning'" round size="small">
{{ identityForm.provider === key ? '当前服务商' : '备用服务商' }}
</n-tag>
</n-card>
</n-grid-item>
</n-grid>
<n-card class="aksrt-section-card" title="通用认证设置">
<n-form :model="verifyForm" label-placement="left" label-width="190">
<n-form-item label="启用身份认证"><n-switch v-model:value="verifyForm.user_verify_enabled" /></n-form-item>
<n-form-item label="公开页显示认证徽标"><n-switch v-model:value="verifyForm.user_verify_public_display" /></n-form-item>
<n-form-item label="允许用户前台申请"><n-switch v-model:value="verifyForm.user_verify_allow_apply" /></n-form-item>
<n-form-item label="云失败转人工"><n-switch v-model:value="verifyForm.user_verify_allow_manual_fallback" /></n-form-item>
<n-form-item label="认证会话有效期(分钟)"><n-input-number v-model:value="verifyForm.user_verify_session_ttl_minutes" :min="5" :max="1440" /></n-form-item>
<n-form-item label="结果轮询间隔(秒)"><n-input-number v-model:value="verifyForm.user_verify_poll_interval_seconds" :min="3" :max="60" /></n-form-item>
<n-form-item label="个人认证费用"><n-input v-model:value="verifyForm.user_verify_personal_fee" placeholder="0.00" /></n-form-item>
<n-form-item label="企业认证费用"><n-input v-model:value="verifyForm.user_verify_enterprise_fee" placeholder="0.00" /></n-form-item>
<n-form-item label="个人认证徽标文案">
<n-input v-model:value="verifyForm.user_verify_personal_badge_text" placeholder="个人认证" />
<n-card class="aksrt-section-card" title="认证申请设置">
<n-form :model="form" label-placement="left" label-width="190">
<n-form-item label="启用身份认证"><n-switch v-model:value="form.user_auth_s" /></n-form-item>
<n-form-item label="允许用户前台申请"><n-switch v-model:value="form.user_auth_apply_s" :disabled="!form.user_auth_s" /></n-form-item>
<n-form-item v-if="!form.user_auth_apply_s" label="停用申请说明">
<n-input v-model:value="form.user_auth_apply_disable_desc" type="textarea" :rows="2" />
</n-form-item>
<n-form-item label="企业认证徽标文案">
<n-input v-model:value="verifyForm.user_verify_enterprise_badge_text" placeholder="企业认证" />
<n-form-item v-if="form.user_auth_apply_s" label="申请最低用户等级">
<n-input-number v-model:value="form.user_auth_apply_limit_level" :min="0" :max="30" />
<template #feedback>设置为 0 时不限制用户等级。</template>
</n-form-item>
<n-form-item label="停用申请说明">
<n-input v-model:value="verifyForm.user_verify_disable_desc" type="textarea" :rows="2" />
</n-form-item>
<n-form-item label="申请介绍">
<n-input v-model:value="verifyForm.user_verify_desc" type="textarea" :rows="3" />
<n-form-item v-if="form.user_auth_apply_s" label="申请介绍">
<n-input v-model:value="form.user_auth_apply_desc" type="textarea" :rows="3" />
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card" title="云服务商">
<n-form :model="identityForm" label-placement="left" label-width="190">
<n-form-item label="当前服务商">
<n-radio-group v-model:value="identityForm.provider">
<n-space>
<n-radio-button value="aliyun">阿里云</n-radio-button>
<n-radio-button value="tencent">腾讯云</n-radio-button>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="回调地址">
<n-input-group>
<n-input :value="callbackUrlForCurrentProvider" readonly />
<n-button @click="copyCallback">复制</n-button>
</n-input-group>
<template #feedback>将此地址填入服务商控制台需要的回调地址配置。</template>
</n-form-item>
</n-form>
</n-card>
<n-tabs v-model:value="activeTab" type="segment" animated class="aksrt-provider-tabs">
<n-tab-pane v-for="key in providerKeys" :key="key" :name="key" :tab="providerLabel(key)">
<n-card class="aksrt-section-card">
<template #header>
<n-space align="center">
<span>{{ providerLabel(key) }}凭证</span>
<n-tag :type="providerState(key).configured ? 'success' : 'warning'" size="small" round>
{{ providerState(key).configured ? '已配置' : '未配置' }}
</n-tag>
</n-space>
</template>
<n-alert type="info" :show-icon="false" style="margin-bottom: 18px">
已保存凭证不会回显。留空表示保留原值;输入新值并保存会替换对应凭证。
</n-alert>
<n-form label-placement="left" label-width="190">
<n-form-item v-if="key === 'aliyun'" label="Region">
<n-input v-model:value="verifyForm.user_verify_aliyun_region" placeholder="cn-hangzhou" />
</n-form-item>
<n-form-item v-if="key === 'aliyun'" label="个人认证 SceneId">
<n-input v-model:value="verifyForm.user_verify_aliyun_personal_flow" placeholder="从阿里云实人认证控制台获取 SceneId" />
</n-form-item>
<n-form-item v-if="key === 'aliyun'" label="个人认证 ProductCode">
<n-select v-model:value="verifyForm.user_verify_aliyun_product_code" :options="aliyunProductOptions" />
</n-form-item>
<n-form-item v-if="key === 'aliyun'" label="企业认证业务标识">
<n-input v-model:value="verifyForm.user_verify_aliyun_enterprise_flow" placeholder="可留空,默认复用个人认证 SceneId" />
</n-form-item>
<n-form-item v-if="key === 'tencent'" label="Region">
<n-input v-model:value="verifyForm.user_verify_tencent_region" placeholder="ap-guangzhou" />
</n-form-item>
<n-form-item v-if="key === 'tencent'" label="个人认证 RuleId">
<n-input v-model:value="verifyForm.user_verify_tencent_personal_flow" placeholder="从腾讯云实名核身控制台获取 RuleId" />
</n-form-item>
<n-form-item v-if="key === 'tencent'" label="企业认证流程标识">
<n-input v-model:value="verifyForm.user_verify_tencent_enterprise_flow" placeholder="可留空,企业四要素后复用个人认证 RuleId" />
</n-form-item>
<n-form-item v-for="field in credentialFields[key]" :key="field.key" :label="field.label">
<n-input
v-model:value="credentials[key][field.key]"
type="password"
show-password-on="click"
:placeholder="credentialPlaceholder(key, field.key)"
autocomplete="new-password"
/>
</n-form-item>
<n-form-item label="显式清除凭证">
<n-checkbox v-model:checked="clearCredentials[key]">
保存时删除 {{ providerLabel(key) }}全部凭证
</n-checkbox>
<template #feedback>清除后该服务商立即变为未配置;此操作不会因输入框留空而触发。</template>
</n-form-item>
<n-form-item label="配置测试">
<n-button :loading="testing === key" :disabled="saving" @click="testProvider(key)">
测试 {{ providerLabel(key) }}配置
</n-button>
</n-form-item>
</n-form>
</n-card>
</n-tab-pane>
</n-tabs>
<div class="aksrt-form-actions">
<n-button :disabled="!dirty" @click="load">放弃更改</n-button>
<n-button type="primary" :loading="saving" @click="save">保存全部配置</n-button>
<n-button :disabled="!dirty || saving" @click="load">放弃更改</n-button>
<n-button type="primary" :disabled="!dirty" :loading="saving" @click="save">保存设置</n-button>
</div>
</n-spin>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useDialog, useMessage } from 'naive-ui'
import { computed, onMounted, reactive, ref } from 'vue'
import { useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api'
const defaults = {
user_auth_s: true,
user_auth_apply_s: true,
user_auth_apply_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_auth_apply_limit_level: 0,
user_auth_apply_desc: '请填写认证身份、简介和申请说明,提交后由管理员审核。',
}
const message = useMessage()
const dialog = useDialog()
const providerKeys = ['aliyun', 'tencent']
const aliyunProductOptions = [
{ label: '实人认证(ID_PRO)', value: 'ID_PRO' },
{ label: '活体人脸验证(PV_FV)', value: 'PV_FV' },
{ label: '活体检测(LR_FR)', value: 'LR_FR' },
]
const credentialFields = {
aliyun: [
{ key: 'access_key_id', label: 'AccessKey ID' },
{ key: 'access_key_secret', label: 'AccessKey Secret' },
],
tencent: [
{ key: 'secret_id', label: 'SecretId' },
{ key: 'secret_key', label: 'SecretKey' },
],
}
const verifyDefaults = {
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_allow_apply: true,
user_verify_allow_manual_fallback: true,
user_verify_session_ttl_minutes: 30,
user_verify_poll_interval_seconds: 5,
user_verify_personal_fee: '0.00',
user_verify_enterprise_fee: '0.00',
user_verify_personal_badge_text: '个人认证',
user_verify_enterprise_badge_text: '企业认证',
user_verify_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_verify_desc: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
user_verify_aliyun_region: 'cn-hangzhou',
user_verify_aliyun_personal_flow: '',
user_verify_aliyun_product_code: 'ID_PRO',
user_verify_aliyun_enterprise_flow: '',
user_verify_tencent_region: 'ap-guangzhou',
user_verify_tencent_personal_flow: '',
user_verify_tencent_enterprise_flow: '',
}
const form = reactive({ ...defaults })
const initial = ref('')
const loading = ref(true)
const saving = ref(false)
const testing = ref('')
const activeTab = ref('aliyun')
const callbackUrls = reactive({ aliyun: '', tencent: '' })
const callbackUrlForCurrentProvider = computed(() => callbackUrls[identityForm.provider] || '')
const initialSnapshot = ref('')
const states = reactive({ aliyun: {}, tencent: {} })
const verifyForm = reactive({ ...verifyDefaults })
const identityForm = reactive({ provider: 'aliyun' })
const credentials = reactive({ aliyun: {}, tencent: {} })
const clearCredentials = reactive({ aliyun: false, tencent: false })
const snapshot = computed(() => JSON.stringify(form))
const dirty = computed(() => initial.value !== '' && snapshot.value !== initial.value)
const snapshot = computed(() => JSON.stringify({ verifyForm, identityForm, credentials, clearCredentials }))
const dirty = computed(() => initialSnapshot.value !== '' && snapshot.value !== initialSnapshot.value)
watch(() => identityForm.provider, value => { activeTab.value = value })
function providerLabel(key) {
return key === 'aliyun' ? '阿里云' : '腾讯云'
}
function providerState(key) {
return states[key] || {}
}
function credentialPlaceholder(provider, field) {
return providerState(provider).configured_fields?.[field] ? '已配置,留空则保留' : '尚未配置'
}
function extractData(response) {
return response?.data ?? response ?? {}
}
@@ -222,123 +54,27 @@ function extractData(response) {
async function load() {
loading.value = true
try {
const [verifyResponse, identityResponse] = await Promise.all([
api.getSchemaPage('users', 'verify'),
api.getIdentitySettings(),
])
Object.assign(verifyForm, verifyDefaults, extractData(verifyResponse))
const identity = extractData(identityResponse)
identityForm.provider = providerKeys.includes(identity.provider) ? identity.provider : 'aliyun'
Object.assign(callbackUrls, identity.callback_url || {})
providerKeys.forEach(key => {
states[key] = identity.providers?.[key] || {}
credentials[key] = {}
clearCredentials[key] = false
})
activeTab.value = identityForm.provider
initialSnapshot.value = snapshot.value
Object.assign(form, defaults, extractData(await api.getSchemaPage('users', 'verify')))
initial.value = snapshot.value
} catch (error) {
message.error(error.message || '加载身份认证配置失败')
message.error(error.message || '加载身份认证设置失败')
} finally {
loading.value = false
}
}
function buildProviderPayload(key) {
const replacement = {}
credentialFields[key].forEach(({ key: field }) => {
const value = String(credentials[key]?.[field] || '').trim()
if (value) replacement[field] = value
})
return {
credentials: replacement,
clear_credentials: clearCredentials[key],
}
}
async function persist() {
const [verifyResponse, identityResponse] = await Promise.all([
api.saveSchemaPage('users', 'verify', { ...verifyForm }),
api.saveIdentitySettings({
provider: identityForm.provider,
providers: {
aliyun: buildProviderPayload('aliyun'),
tencent: buildProviderPayload('tencent'),
},
}),
])
const identity = extractData(identityResponse)
Object.assign(verifyForm, extractData(verifyResponse))
providerKeys.forEach(key => {
states[key] = identity.providers?.[key] || states[key]
credentials[key] = {}
clearCredentials[key] = false
})
Object.assign(callbackUrls, identity.callback_url || callbackUrls)
initialSnapshot.value = snapshot.value
}
async function save() {
const hasClear = providerKeys.some(key => clearCredentials[key])
const run = async () => {
saving.value = true
try {
await persist()
message.success('身份认证配置已保存')
} catch (error) {
message.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
if (!hasClear) return run()
dialog.warning({
title: '确认清除凭证',
content: '已勾选的服务商凭证将被永久删除,是否继续保存?',
positiveText: '清除并保存',
negativeText: '取消',
onPositiveClick: run,
})
}
async function testProvider(provider) {
testing.value = provider
saving.value = true
try {
const response = await api.testIdentitySettings({ provider })
message.success(response?.message || `${providerLabel(provider)}配置测试通过`)
Object.assign(form, defaults, extractData(await api.saveSchemaPage('users', 'verify', { ...form })))
initial.value = snapshot.value
message.success('身份认证设置已保存')
} catch (error) {
message.error(error.message || `${providerLabel(provider)}配置测试失败`)
message.error(error.message || '保存身份认证设置失败')
} finally {
testing.value = ''
}
}
async function copyCallback() {
if (!callbackUrlForCurrentProvider.value) return message.warning('回调地址暂不可用')
try {
await navigator.clipboard.writeText(callbackUrlForCurrentProvider.value)
message.success('回调地址已复制')
} catch {
message.error('复制失败,请手动复制')
saving.value = false
}
}
onMounted(load)
</script>
<style scoped>
.aksrt-status-card :deep(.n-card__content) {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.aksrt-section-card { margin-top: 16px; }
.aksrt-provider-tabs { margin-top: 16px; }
.aksrt-form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
</style>
@@ -2,83 +2,51 @@ export const verifySchema = {
key: 'users.verify',
title: '身份认证',
icon: 'fa fas fa-id-card',
description: '配置公开认证徽标、申请类型与申请门槛;审核、驳回和取消认证请前往 WordPress 用户 → 身份认证。',
description: '设置通用身份认证的申请条件;用户提交资料后由管理员审核。',
defaults: {
user_verify_enabled: true,
user_verify_public_display: true,
user_verify_personal_badge_text: '个人认证',
user_verify_personal_fee: '0.00',
user_verify_personal_free_applications: 0,
user_verify_personal_require_real_name: false,
user_verify_enterprise_badge_text: '企业认证',
user_verify_enterprise_fee: '0.00',
user_verify_enterprise_free_applications: 0,
user_verify_enterprise_require_real_name: false,
user_verify_allow_apply: true,
user_verify_disable_desc:
'由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_verify_desc:
'认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
user_auth_s: true,
user_auth_apply_s: true,
user_auth_apply_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
user_auth_apply_limit_level: 0,
user_auth_apply_desc: '请填写认证身份、简介和申请说明,提交后由管理员审核。',
},
groups: [
{
key: 'display',
title: '公开展示',
icon: 'fa fas fa-eye',
fields: [
{ key: 'user_verify_enabled', label: '启用身份认证', type: 'switch', default: true },
{
key: 'user_verify_public_display',
label: '公开页显示认证徽标',
type: 'switch',
default: true,
dependsOn: { key: 'user_verify_enabled', value: true },
},
{
key: 'user_verify_personal_badge_text', label: '个人认证徽标文案', type: 'input', default: '个人认证',
dependsOn: { key: 'user_verify_public_display', value: true },
},
{
key: 'user_verify_enterprise_badge_text', label: '企业认证徽标文案', type: 'input', default: '企业认证',
dependsOn: { key: 'user_verify_public_display', value: true },
},
],
},
{
key: 'workflow', title: '认证方式', icon: 'fa fas fa-tasks',
fields: [{ type: 'alert', content: '认证方式固定为个人认证和企业认证,申请人不可自行新增或修改类型。' }],
},
{
key: 'apply',
title: '申请设置',
title: '认证申请',
icon: 'fa fas fa-edit',
fields: [
{ key: 'user_auth_s', label: '启用身份认证', type: 'switch', default: true },
{
key: 'user_verify_allow_apply',
key: 'user_auth_apply_s',
label: '允许用户前台申请',
type: 'switch',
default: true,
dependsOn: { key: 'user_verify_enabled', value: true },
dependsOn: { key: 'user_auth_s', value: true },
},
{
key: 'user_verify_disable_desc',
key: 'user_auth_apply_disable_desc',
label: '停用申请说明',
type: 'textarea',
default: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
dependsOn: { key: 'user_verify_allow_apply', value: false },
dependsOn: { key: 'user_auth_apply_s', value: false },
},
{ key: 'user_verify_personal_fee', label: '个人认证每次扣余额(元)', type: 'input', default: '0.00', help: '仅接受最多两位小数的十进制字符串。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_personal_free_applications', label: '个人认证赠送初始次数', type: 'number', default: 0, min: 0, max: 9999, help: '每位用户前 N 次个人认证申请免费,之后按单次费用扣余额。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_personal_require_real_name', label: '个人认证强制填写真实姓名', type: 'switch', default: false, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_fee', label: '企业认证每次扣余额(元)', type: 'input', default: '0.00', help: '仅接受最多两位小数的十进制字符串。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_free_applications', label: '企业认证赠送初始次数', type: 'number', default: 0, min: 0, max: 9999, help: '每位用户前 N 次企业认证申请免费,之后按单次费用扣余额。', dependsOn: { key: 'user_verify_allow_apply', value: true } },
{ key: 'user_verify_enterprise_require_real_name', label: '企业认证强制填写真实姓名', type: 'switch', default: false, dependsOn: { key: 'user_verify_allow_apply', value: true } },
{
key: 'user_verify_desc',
key: 'user_auth_apply_limit_level',
label: '申请最低用户等级',
type: 'number',
default: 0,
min: 0,
max: 30,
help: '设置为 0 时不限制用户等级。',
dependsOn: { key: 'user_auth_apply_s', value: true },
},
{
key: 'user_auth_apply_desc',
label: '申请介绍',
type: 'textarea',
default: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
dependsOn: { key: 'user_verify_allow_apply', value: true },
default: '请填写认证身份、简介和申请说明,提交后由管理员审核。',
dependsOn: { key: 'user_auth_apply_s', value: true },
},
],
},