diff --git a/app/admin/controller/order/AdminWithdrawOrder.php b/app/admin/controller/order/AdminWithdrawOrder.php index 565cf96..2f04eb8 100644 --- a/app/admin/controller/order/AdminWithdrawOrder.php +++ b/app/admin/controller/order/AdminWithdrawOrder.php @@ -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; + } } diff --git a/app/api/controller/Auth.php b/app/api/controller/Auth.php index 6fb6138..1eef47f 100644 --- a/app/api/controller/Auth.php +++ b/app/api/controller/Auth.php @@ -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); diff --git a/app/common/library/Auth.php b/app/common/library/Auth.php index b09980a..58a1cc9 100644 --- a/app/common/library/Auth.php +++ b/app/common/library/Auth.php @@ -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'); diff --git a/app/functions.php b/app/functions.php index f266340..505789e 100644 --- a/app/functions.php +++ b/app/functions.php @@ -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); } } diff --git a/web/src/lang/backend/en/order/adminWithdrawOrder.ts b/web/src/lang/backend/en/order/adminWithdrawOrder.ts index 3fb3d8c..bfafe5b 100644 --- a/web/src/lang/backend/en/order/adminWithdrawOrder.ts +++ b/web/src/lang/backend/en/order/adminWithdrawOrder.ts @@ -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', diff --git a/web/src/lang/backend/en/user/user.ts b/web/src/lang/backend/en/user/user.ts index 775b9b4..651eb75 100644 --- a/web/src/lang/backend/en/user/user.ts +++ b/web/src/lang/backend/en/user/user.ts @@ -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', diff --git a/web/src/lang/backend/zh-cn/order/adminWithdrawOrder.ts b/web/src/lang/backend/zh-cn/order/adminWithdrawOrder.ts index b09150d..eaea25f 100644 --- a/web/src/lang/backend/zh-cn/order/adminWithdrawOrder.ts +++ b/web/src/lang/backend/zh-cn/order/adminWithdrawOrder.ts @@ -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: '提现总单数', diff --git a/web/src/lang/backend/zh-cn/user/user.ts b/web/src/lang/backend/zh-cn/user/user.ts index 5c22a7e..b0306cd 100644 --- a/web/src/lang/backend/zh-cn/user/user.ts +++ b/web/src/lang/backend/zh-cn/user/user.ts @@ -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: '渠道名', diff --git a/web/src/views/backend/order/adminWithdrawOrder/index.vue b/web/src/views/backend/order/adminWithdrawOrder/index.vue index 5826a16..3d06f6c 100644 --- a/web/src/views/backend/order/adminWithdrawOrder/index.vue +++ b/web/src/views/backend/order/adminWithdrawOrder/index.vue @@ -38,18 +38,19 @@
+ + + diff --git a/web/src/views/backend/user/user/popupForm.vue b/web/src/views/backend/user/user/popupForm.vue index a7a5173..dde4b94 100644 --- a/web/src/views/backend/user/user/popupForm.vue +++ b/web/src/views/backend/user/user/popupForm.vue @@ -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> = 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') })], })