1.优化管理员提现记录审核为一个操作

2.修复创建玩家报错“参数%s不能为空”
3.修复玩家登录报错
This commit is contained in:
2026-05-30 18:27:26 +08:00
parent 9a3f3b747f
commit 75e91fee13
11 changed files with 438 additions and 98 deletions

View File

@@ -14,7 +14,7 @@ use Webman\Http\Request as WebmanRequest;
*/
class AdminWithdrawOrder extends Backend
{
protected array $noNeedPermission = ['stats', 'approve', 'reject'];
protected array $noNeedPermission = ['stats'];
protected ?object $model = null;
@@ -61,7 +61,13 @@ class AdminWithdrawOrder extends Backend
$list = $res->items();
foreach ($list as $idx => $item) {
$list[$idx]['can_review'] = $this->canReviewOrder(is_array($item) ? $item : []) ? 1 : 0;
$row = is_array($item) ? $item : $item->toArray();
$canReview = $this->canReviewOrder($row) ? 1 : 0;
if (is_array($item)) {
$list[$idx]['can_review'] = $canReview;
} else {
$item->setAttr('can_review', $canReview);
}
}
return $this->success('', [
@@ -79,7 +85,7 @@ class AdminWithdrawOrder extends Backend
return $this->error(__('Parameter error'));
}
if ($this->request && $this->request->method() === 'POST') {
return $this->error(__('Please use approve/reject buttons to review'));
return $this->error(__('Please use the review action to process this order'));
}
$row = $this->loadWithRelations(intval(strval($id)));
if (!$row) {
@@ -91,7 +97,10 @@ class AdminWithdrawOrder extends Backend
return $this->success('', ['row' => $row]);
}
public function approve(WebmanRequest $request): Response
/**
* 审核(通过 / 拒绝)
*/
public function review(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
@@ -101,46 +110,12 @@ class AdminWithdrawOrder extends Backend
return $this->error(__('Parameter error'));
}
$id = intval(strval($request->post('id', 0)));
if ($id <= 0) {
return $this->error(__('Parameter error'));
}
$order = Db::name('admin_withdraw_order')->where('id', $id)->find();
if (!is_array($order)) {
return $this->error(__('Record not found'));
}
if (!$this->canReviewOrder($order)) {
return $this->error(__('You have no permission'));
}
if (intval($order['status'] ?? 0) !== 0) {
return $this->error(__('This withdraw order has already been reviewed'));
}
$remark = trim((string) $request->post('remark', ''));
Db::startTrans();
try {
AdminWalletService::approveWithdraw($order, intval($this->auth->id), $remark);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success(__('Approved'));
}
public function reject(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->method() !== 'POST') {
return $this->error(__('Parameter error'));
}
$id = intval(strval($request->post('id', 0)));
if ($id <= 0) {
$action = strtolower(trim((string) $request->post('action', '')));
if ($id <= 0 || !in_array($action, ['approve', 'reject'], true)) {
return $this->error(__('Parameter error'));
}
$remark = trim((string) $request->post('remark', ''));
if ($remark === '') {
if ($action === 'reject' && $remark === '') {
return $this->error(__('Please provide reject reason'));
}
$order = Db::name('admin_withdraw_order')->where('id', $id)->find();
@@ -155,13 +130,18 @@ class AdminWithdrawOrder extends Backend
}
Db::startTrans();
try {
AdminWalletService::rejectWithdraw($order, intval($this->auth->id), $remark);
if ($action === 'approve') {
AdminWalletService::approveWithdraw($order, intval($this->auth->id), $remark);
} else {
AdminWalletService::rejectWithdraw($order, intval($this->auth->id), $remark);
}
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success(__('Rejected'));
return $this->success($action === 'approve' ? __('Approved') : __('Rejected'));
}
public function stats(WebmanRequest $request): Response
@@ -226,11 +206,11 @@ class AdminWithdrawOrder extends Backend
private function canReviewOrder(array $order): bool
{
if (!$this->auth) {
if (!$this->auth || intval($order['status'] ?? 0) !== 0) {
return false;
}
if ($this->auth->isSuperAdmin() || $this->hasGlobalReadScope()) {
return true;
if (!$this->hasAdminWithdrawReviewPermission()) {
return false;
}
$adminId = intval($order['admin_id'] ?? 0);
if ($adminId <= 0) {
@@ -243,5 +223,19 @@ class AdminWithdrawOrder extends Backend
return in_array($adminId, $scopedAdminIds, true);
}
private function hasAdminWithdrawReviewPermission(): bool
{
if (!$this->auth) {
return false;
}
foreach ($this->buildPermissionRoutePaths('order/adminWithdrawOrder', 'review') as $routePath) {
if ($this->auth->check($routePath)) {
return true;
}
}
return false;
}
}

View File

@@ -108,7 +108,8 @@ class Auth extends MobileBase
$ok = $this->auth->login($username, $password, true);
if (!$ok) {
return $this->mobileError(1101, 'Incorrect account or password');
$detail = (string) $this->auth->getError();
return $this->mobileError(1101, $detail !== '' ? $detail : 'Incorrect account or password');
}
$this->bindMobileDeviceSession($request);

View File

@@ -177,12 +177,15 @@ class Auth extends \ba\Auth
} elseif (preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username)) {
$accountType = 'username';
}
if (!$accountType) {
$this->setError('Account not exist');
return false;
if ($accountType) {
$this->model = User::where($accountType, $username)->find();
} else {
// 兼容历史纯数字账号、带 + 前缀手机号等非标准格式
$this->model = User::where('username', $username)->whereOr('phone', $username)->find();
if (!$this->model && str_starts_with($username, '+')) {
$this->model = User::where('phone', substr($username, 1))->find();
}
}
$this->model = User::where($accountType, $username)->find();
if (!$this->model) {
$this->setError('Account not exist');
return false;
@@ -204,7 +207,7 @@ class Auth extends \ba\Auth
if ($this->model->login_failure > 0 && $lastLoginTs > 0 && time() - $lastLoginTs >= 86400) {
$this->model->login_failure = 0;
$this->model->save();
$this->model = User::where($accountType, $username)->find();
$this->model = User::find($this->model->id);
}
if ($this->model->login_failure >= $userLoginRetry) {
$this->setError('Please try again after 1 day');

View File

@@ -39,13 +39,38 @@ if (!function_exists('env')) {
if (!function_exists('__')) {
/**
* 语言翻译BuildAdmin 兼容)
* ThinkPHP 风格占位符(%s / %d 等 + 数字下标 vars在翻译后走 sprintf
* Symfony 风格占位符(%name% 或 '%s' => value 等字符串键)走 trans/strtr。
*/
function __(string $name, array $vars = [], string $lang = ''): mixed
{
if (is_numeric($name) || !$name) {
return $name;
}
return function_exists('trans') ? trans($name, $vars, null, $lang ?: null) : $name;
if (!function_exists('trans')) {
return $name;
}
$positional = [];
$named = [];
foreach ($vars as $k => $v) {
if (is_int($k)) {
$positional[$k] = $v;
} else {
$named[$k] = $v;
}
}
if ($positional !== [] && $named === []) {
$translated = trans($name, [], null, $lang ?: null);
if ($translated === '' || $translated === $name) {
$translated = $name;
}
return vsprintf($translated, array_values($positional));
}
return trans($name, $vars, null, $lang ?: null);
}
}

View File

@@ -15,10 +15,15 @@ export default {
review_admin_username: 'Reviewer',
remark: 'Remark',
create_time: 'Create time',
review_btn: 'Review',
review_title: 'Withdraw review',
review_btn_approve: 'Approve',
review_btn_reject: 'Reject',
review_btn_back: 'Back',
review_btn_confirm_reject: 'Confirm reject',
review_approve_title: 'Approve order',
review_reject_title: 'Reject order',
review_reject_tip: 'Rejected orders will unfreeze the amount back to the admin wallet',
review_remark_optional: 'Optional review remark',
reject_reason_required: 'Please enter reject reason',
stat_total_count: 'Total orders',

View File

@@ -29,6 +29,7 @@ export default {
section_admin_attribution: 'Administrator',
admin_affiliation: 'Assigned admin',
admin_affiliation_placeholder: 'Role group tree — only admins in your scope',
admin_no_channel_bound: 'The selected admin is not bound to a channel; bind a channel in Admin management before creating users',
register_invite_code_auto_placeholder: 'Filled from selected admin invite code',
channel_id: 'Channel',
channel__name: 'Channel',

View File

@@ -15,10 +15,15 @@ export default {
review_admin_username: '审核人',
remark: '备注',
create_time: '创建时间',
review_btn: '审核',
review_title: '提现审核',
review_btn_approve: '通过',
review_btn_reject: '拒绝',
review_btn_back: '返回',
review_btn_confirm_reject: '确认拒绝',
review_approve_title: '通过审核',
review_reject_title: '拒绝审核',
review_reject_tip: '拒绝后冻结金额将退回管理员钱包',
review_remark_optional: '可选填写审核备注',
reject_reason_required: '请填写拒绝原因',
stat_total_count: '提现总单数',

View File

@@ -29,6 +29,7 @@ export default {
section_admin_attribution: '管理员归属',
admin_affiliation: '归属管理员',
admin_affiliation_placeholder: '按角色组展开,仅展示您可管理范围内的管理员',
admin_no_channel_bound: '所选归属管理员未绑定渠道,无法创建用户,请先在管理员管理中绑定渠道',
register_invite_code_auto_placeholder: '随所选管理员邀请码自动带出',
channel_id: '所属渠道',
channel__name: '渠道名',

View File

@@ -38,18 +38,19 @@
</el-select>
</div>
<Table ref="tableRef"></Table>
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, reactive, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessageBox } from 'element-plus'
import { baTableApi } from '/@/api/common'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
import createAxios from '/@/utils/axios'
import PopupForm from './popupForm.vue'
defineOptions({
name: 'order/adminWithdrawOrder',
@@ -69,50 +70,21 @@ const stats = reactive({
approved_amount: '0.00',
})
const onReview = async (row: anyObj, action: 'approve' | 'reject') => {
const needReason = action === 'reject'
const { value } = await ElMessageBox.prompt(
needReason ? t('order.adminWithdrawOrder.reject_reason_required') : t('order.adminWithdrawOrder.review_remark_optional'),
action === 'approve' ? t('order.adminWithdrawOrder.review_approve_title') : t('order.adminWithdrawOrder.review_reject_title'),
{
confirmButtonText: t('Confirm'),
cancelButtonText: t('Cancel'),
inputPlaceholder: needReason ? t('order.adminWithdrawOrder.reject_reason_required') : t('order.adminWithdrawOrder.review_remark_optional'),
inputPattern: needReason ? /.+/ : undefined,
inputErrorMessage: needReason ? t('order.adminWithdrawOrder.reject_reason_required') : '',
})
await createAxios(
{
url: `/admin/order.AdminWithdrawOrder/${action}`,
method: 'post',
data: { id: row.id, remark: value || '' },
},
{ showSuccessMessage: true }
)
await loadStats()
baTable.getData()
}
const optButtons: OptButton[] = [
{
render: 'tipButton',
name: 'approve',
title: 'order.adminWithdrawOrder.review_btn_approve',
name: 'review',
title: 'order.adminWithdrawOrder.review_btn',
text: '',
type: 'success',
icon: 'el-icon-Check',
type: 'warning',
icon: 'fa fa-check-square-o',
display: (row: TableRow) => Number(row.status) === 0 && Number((row as anyObj).can_review ?? 0) === 1,
click: (row: TableRow) => void onReview(row as anyObj, 'approve'),
},
{
render: 'tipButton',
name: 'reject',
title: 'order.adminWithdrawOrder.review_btn_reject',
text: '',
type: 'danger',
icon: 'el-icon-Close',
display: (row: TableRow) => Number(row.status) === 0 && Number((row as anyObj).can_review ?? 0) === 1,
click: (row: TableRow) => void onReview(row as anyObj, 'reject'),
click: (row: TableRow) => {
baTable.form.operate = 'Review'
baTable.form.operateIds = [String(row[baTable.table.pk!])]
baTable.form.items = { ...(row as anyObj) }
baTable.form.loading = false
},
},
]
@@ -179,7 +151,9 @@ const baTable = new baTableClass(
{ label: t('Operate'), align: 'center', width: 90, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
},
{}
{
defaultItems: {},
}
)
const onStatusFilterChange = () => {
@@ -274,4 +248,3 @@ onMounted(() => {
}
}
</style>

View File

@@ -0,0 +1,293 @@
<template>
<el-dialog
class="ba-operate-dialog admin-withdraw-review-dialog"
:close-on-click-modal="false"
:model-value="isOpen"
width="560px"
@close="onDialogClose"
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ step === 'reject' ? t('order.adminWithdrawOrder.review_reject_title') : t('order.adminWithdrawOrder.review_title') }}
</div>
</template>
<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
<div class="ba-operate-form ba-edit-form">
<el-form
v-if="!loading && step === 'review'"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
@submit.prevent=""
>
<el-form-item :label="t('order.adminWithdrawOrder.order_no')">
<el-input v-model="form.order_no" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.admin_username')">
<el-input :model-value="form.admin_text" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.channel_name')">
<el-input :model-value="form.channel_text" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.amount')">
<el-input :model-value="form.amount_text" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.actual_amount')">
<el-input :model-value="form.actual_amount_text" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.receive_type')">
<el-input :model-value="form.receive_type_text" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.receive_account')">
<el-input :model-value="form.receive_account" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.remark')">
<el-input :model-value="form.remark" type="textarea" :rows="2" readonly />
</el-form-item>
</el-form>
<el-form
v-if="!loading && step === 'reject'"
ref="rejectFormRef"
:model="rejectForm"
:rules="rejectRules"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
@submit.prevent=""
>
<el-alert
class="review-reject-hint"
type="warning"
show-icon
:closable="false"
:title="t('order.adminWithdrawOrder.review_reject_tip')"
/>
<el-form-item :label="t('order.adminWithdrawOrder.order_no')">
<el-input v-model="form.order_no" readonly />
</el-form-item>
<el-form-item :label="t('order.adminWithdrawOrder.remark')" prop="remark">
<el-input
v-model="rejectForm.remark"
type="textarea"
:rows="4"
maxlength="255"
show-word-limit
:placeholder="t('order.adminWithdrawOrder.reject_reason_required')"
/>
</el-form-item>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div class="review-footer">
<template v-if="step === 'review'">
<el-button @click="onDialogClose">{{ t('Cancel') }}</el-button>
<el-button type="danger" :loading="submitting" @click="gotoReject">{{ t('order.adminWithdrawOrder.review_btn_reject') }}</el-button>
<el-button type="primary" v-blur :loading="submitting" @click="submitApprove">{{ t('order.adminWithdrawOrder.review_btn_approve') }}</el-button>
</template>
<template v-else>
<el-button @click="backToReview">{{ t('order.adminWithdrawOrder.review_btn_back') }}</el-button>
<el-button type="danger" v-blur :loading="submitting" @click="submitReject">{{ t('order.adminWithdrawOrder.review_btn_confirm_reject') }}</el-button>
</template>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormInstance, FormItemRule } from 'element-plus'
import { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import createAxios from '/@/utils/axios'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
const config = useConfig()
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rejectFormRef = useTemplateRef<FormInstance>('rejectFormRef')
type Step = 'review' | 'reject'
const step = ref<Step>('review')
const loading = ref(false)
const submitting = ref(false)
const form = reactive({
id: 0,
order_no: '',
admin_text: '-',
channel_text: '-',
amount_text: '0.00',
actual_amount_text: '0.00',
receive_type_text: '-',
receive_account: '-',
remark: '',
})
const rejectForm = reactive({
remark: '',
})
const receiveTypeLabels: Record<string, string> = {
bank: 'order.adminWithdrawOrder.receive_type_bank',
ewallet: 'order.adminWithdrawOrder.receive_type_ewallet',
crypto: 'order.adminWithdrawOrder.receive_type_crypto',
}
const isOpen = computed(() => ['Review'].includes(baTable.form.operate ?? ''))
watch(isOpen, (visible) => {
if (!visible) {
return
}
step.value = 'review'
rejectForm.remark = ''
})
watch(
() => ({ visible: isOpen.value, loadingState: baTable.form.loading, items: baTable.form.items }),
({ visible, loadingState }) => {
if (!visible) {
return
}
loading.value = loadingState === true
if (loadingState) {
return
}
hydrate()
},
{ deep: true, immediate: true }
)
const hydrate = () => {
const row = baTable.form.items as Record<string, unknown> | undefined
if (!row || !row['id']) {
return
}
form.id = Number(row['id'] ?? 0)
form.order_no = String(row['order_no'] ?? '')
form.amount_text = formatAmount(row['amount'])
form.actual_amount_text = formatAmount(row['actual_amount'])
form.receive_account = String(row['receive_account'] ?? '-')
form.remark = String(row['remark'] ?? '')
const receiveType = String(row['receive_type'] ?? '')
const receiveKey = receiveTypeLabels[receiveType]
form.receive_type_text = receiveKey ? t(receiveKey) : receiveType || '-'
form.admin_text = resolveRelationText(row, 'admin', row['admin_id'])
form.channel_text = resolveRelationText(row, 'channel', row['channel_id'])
}
const rejectRules: Record<string, FormItemRule[]> = {
remark: [
{
required: true,
validator: (_r, value, cb) => {
const text = typeof value === 'string' ? value.trim() : ''
if (text === '') {
cb(new Error(t('order.adminWithdrawOrder.reject_reason_required')))
return
}
cb()
},
trigger: 'blur',
},
],
}
const onDialogClose = () => {
if (submitting.value) {
return
}
step.value = 'review'
baTable.toggleForm()
}
const gotoReject = () => {
step.value = 'reject'
rejectForm.remark = ''
}
const backToReview = () => {
step.value = 'review'
}
const submitReview = async (action: 'approve' | 'reject', remark: string) => {
submitting.value = true
try {
await createAxios(
{
url: '/admin/order.AdminWithdrawOrder/review',
method: 'post',
data: { id: form.id, action, remark },
},
{ showSuccessMessage: true }
)
baTable.onTableHeaderAction('refresh', {})
baTable.toggleForm()
} finally {
submitting.value = false
}
}
const submitApprove = async () => {
await submitReview('approve', '')
}
const submitReject = async () => {
const formEl = rejectFormRef.value
if (!formEl) {
return
}
const valid = await formEl.validate().catch(() => false)
if (!valid) {
return
}
await submitReview('reject', rejectForm.remark.trim())
}
function formatAmount(raw: unknown): string {
if (raw === null || raw === undefined || raw === '') {
return '0.00'
}
const n = Number(raw)
if (!Number.isFinite(n)) {
return String(raw)
}
return n.toFixed(2)
}
function resolveRelationText(row: Record<string, unknown>, relationKey: string, fallbackId: unknown): string {
const rel = row[relationKey]
if (rel && typeof rel === 'object') {
const r = rel as Record<string, unknown>
const name = r['username'] ?? r['name']
if (typeof name === 'string' && name !== '') {
return name
}
}
if (fallbackId === null || fallbackId === undefined || fallbackId === '') {
return '-'
}
return 'ID: ' + String(fallbackId)
}
</script>
<style scoped lang="scss">
.admin-withdraw-review-dialog {
:deep(.el-dialog__body) {
padding-top: 8px;
}
.review-reject-hint {
margin-bottom: 14px;
}
}
.review-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>

View File

@@ -277,7 +277,6 @@ function buildAdminMapsFromTree(nodes: TreeNode[]) {
return { mapCh, mapInv }
}
/** 鏄犲皠鏈懡涓椂浠庡師濮嬫爲鏌ユ壘锛堥槻姝?props 瑁佸壀鎴栧紓姝ユ椂搴忛棶棰橈級 */
function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?: number; invite_code: string } | null {
const target = String(adminId).trim()
for (const n of nodes) {
@@ -304,6 +303,28 @@ function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?:
return null
}
function resolveAdminChannelId(adminId: string | number | null | undefined): number | undefined {
if (adminId === undefined || adminId === null || adminId === '') {
return undefined
}
const key = typeof adminId === 'number' ? String(adminId) : String(adminId).trim()
if (key === '') {
return undefined
}
let channelId = adminIdToChannelId.value[key]
if (channelId === undefined) {
const meta = findAdminMetaInTree(adminScopeTree.value, key)
if (meta?.channel_id !== undefined) {
channelId = meta.channel_id
}
}
if (channelId === undefined || channelId === null || Number.isNaN(Number(channelId)) || Number(channelId) <= 0) {
return undefined
}
return typeof channelId === 'number' ? channelId : parseInt(String(channelId), 10)
}
const loadAdminScopeTree = async () => {
const res = await createAxios({
url: '/admin/user.User/adminScopeTree',
@@ -360,8 +381,13 @@ const onAdminTreeChange = (val: string | number | null) => {
if (channelId !== undefined) {
baTable.form.items.channel_id = channelId
} else {
delete baTable.form.items.channel_id
}
baTable.form.items.register_invite_code = inv !== undefined && inv !== null ? inv : ''
nextTick(() => {
formRef.value?.validateField('admin_id').catch(() => {})
})
}
onMounted(() => {
@@ -435,6 +461,16 @@ watch(
}
)
const validatorAdminChannel = (_rule: unknown, val: string | number | null | undefined, callback: (error?: Error) => void) => {
if (val === undefined || val === null || val === '') {
return callback()
}
if (resolveAdminChannelId(val) === undefined) {
return callback(new Error(t('user.user.admin_no_channel_bound')))
}
return callback()
}
const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Error) => void) => {
const operate = baTable.form.operate
const v = typeof val === 'string' ? val.trim() : ''
@@ -454,7 +490,10 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
username: [buildValidatorData({ name: 'required', title: t('user.user.username') })],
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
phone: [buildValidatorData({ name: 'required', title: t('user.user.phone') })],
admin_id: [buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') })],
admin_id: [
buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') }),
{ validator: validatorAdminChannel, trigger: 'change' },
],
create_time: [buildValidatorData({ name: 'date', title: t('user.user.create_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('user.user.update_time') })],
})