后台增加撤销用户实名功能

实名姓名与收款账户不一致会导致收款码登记失败,新增后台撤销实名能力:
- 新增 user:revoke_realname 权限,授予超级管理员与运营角色
- 撤销时重置用户实名状态为未认证并删除实名记录,使其可重新认证,写入审计日志
- 用户管理页新增实名状态列与"撤销实名"操作(含原因弹窗)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-06-17 07:03:52 +08:00
co-authored by Claude Opus 4.8
parent 8192344027
commit 4d54f1b03a
8 changed files with 184 additions and 5 deletions
+14 -3
View File
@@ -31,9 +31,12 @@ export async function fetchAdminUsers(page = 1, pageSize = 20, query: AdminUserQ
const params: Record<string, unknown> = { page, page_size: pageSize }
if (query.keyword) params.keyword = query.keyword
if (query.status) params.status = query.status
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminUserItem>>>('/admin/users', {
params,
})
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminUserItem>>>(
'/admin/users',
{
params,
}
)
return data.data
}
@@ -56,3 +59,11 @@ export async function setAdminUserDepositFreeQuota(id: number, amountCent: numbe
)
return data.data
}
export async function revokeAdminUserRealname(id: number, reason: string) {
const { data } = await apiClient.post<ApiResponse<AdminUserItem>>(
`/admin/users/${id}/revoke-realname`,
{ reason }
)
return data.data
}
@@ -7,6 +7,7 @@ import { reactive, ref } from 'vue'
import {
fetchAdminUsers,
freezeAdminUser,
revokeAdminUserRealname,
setAdminUserDepositFreeQuota,
unfreezeAdminUser,
type AdminUserItem,
@@ -14,14 +15,16 @@ import {
} from '@/features/admin/api/adminUsers'
import { centToYuan, formatCent, yuanToCent } from '@/shared/utils/money'
import { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable'
import { userStatusLabel } from '@/shared/utils/statusLabels'
import { realnameStatusLabel, userStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const submitting = ref(false)
const activeUser = ref<AdminUserItem | null>(null)
const quotaUser = ref<AdminUserItem | null>(null)
const realnameUser = ref<AdminUserItem | null>(null)
const freezeReason = ref('')
const revokeRealnameReason = ref('')
const quotaAmount = ref(0)
const filters = reactive<AdminUserQuery>({
@@ -104,6 +107,26 @@ async function handleUnfreeze(row: AdminUserItem) {
}
}
function openRevokeRealname(row: AdminUserItem) {
realnameUser.value = row
revokeRealnameReason.value = ''
}
async function handleRevokeRealname() {
if (!realnameUser.value) return
submitting.value = true
try {
await revokeAdminUserRealname(realnameUser.value.id, revokeRealnameReason.value)
ElMessage.success('已撤销实名,用户可重新认证')
realnameUser.value = null
await loadUsers()
} catch (error) {
ElMessage.error(readError(error, '撤销实名失败'))
} finally {
submitting.value = false
}
}
function moneyCent(value: number | string | undefined) {
return formatCent(Number(value || 0))
}
@@ -161,6 +184,9 @@ function moneyCent(value: number | string | undefined) {
<el-table-column label="状态" width="110">
<template #default="{ row }">{{ userStatusLabel(row.status) }}</template>
</el-table-column>
<el-table-column label="实名状态" width="110">
<template #default="{ row }">{{ realnameStatusLabel(row.realname_status) }}</template>
</el-table-column>
<el-table-column label="免押额度" width="130">
<template #default="{ row }">¥{{ moneyCent(row.deposit_free_quota_cent) }}</template>
</el-table-column>
@@ -173,9 +199,18 @@ function moneyCent(value: number | string | undefined) {
<el-table-column label="注册时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="220">
<el-table-column label="操作" width="320">
<template #default="{ row }">
<el-button size="small" @click="openDepositQuota(row)">免押</el-button>
<el-button
v-if="row.realname_status !== 'unverified'"
size="small"
type="warning"
:loading="submitting"
@click="openRevokeRealname(row)"
>
撤销实名
</el-button>
<el-button
v-if="row.status === 'active'"
size="small"
@@ -229,6 +264,36 @@ function moneyCent(value: number | string | undefined) {
</template>
</el-dialog>
<el-dialog
:model-value="!!realnameUser"
title="撤销实名认证"
width="560px"
@update:model-value="realnameUser = null"
>
<div v-if="realnameUser" class="dialog-body">
<p>
<strong>{{ realnameUser.phone }}</strong> · {{ realnameUser.nickname }}
</p>
<el-alert
type="warning"
:closable="false"
title="撤销后该用户实名信息将被清除,需重新完成实名认证后才能下单与登记收款码。"
/>
<el-input
v-model="revokeRealnameReason"
type="textarea"
:rows="4"
placeholder="填写撤销原因(如实名姓名与收款账户不一致),便于审计追踪"
/>
</div>
<template #footer>
<el-button @click="realnameUser = null">取消</el-button>
<el-button type="warning" :loading="submitting" @click="handleRevokeRealname"
>确认撤销</el-button
>
</template>
</el-dialog>
<el-dialog
:model-value="!!quotaUser"
title="设置免押额度"