fix(admin,api): 上分超额提示而非静默截断,并返回中文业务错误
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { ref, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import api from '../api';
|
||||
import { useAdminLocale } from './useAdminLocale';
|
||||
import { resolveApiError } from '../i18n/form-validation';
|
||||
import {
|
||||
depositAmountCap,
|
||||
parsePlayerAvailable,
|
||||
@@ -36,6 +37,20 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise<void>) {
|
||||
|
||||
const transferAmountDisabled = computed(() => transferAmountRange.value.max === 0);
|
||||
|
||||
/** 有上限时超出额度(勿用 el-input-number :max,否则会静默截断到上限) */
|
||||
const transferAmountExceedsCap = computed(() => {
|
||||
const max = transferAmountRange.value.max;
|
||||
if (max === undefined) return false;
|
||||
return transferAmount.value > max;
|
||||
});
|
||||
|
||||
const transferAmountCapError = computed(() => {
|
||||
if (!transferAmountExceedsCap.value) return '';
|
||||
return transferType.value === 'deposit'
|
||||
? t('err.insufficient_credit')
|
||||
: t('transfer.context.withdraw_exceed');
|
||||
});
|
||||
|
||||
function transferTitle() {
|
||||
const name = transferTarget.value?.username ?? transferTarget.value?.id ?? '';
|
||||
return transferType.value === 'deposit'
|
||||
@@ -63,8 +78,7 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise<void>) {
|
||||
transferAmount.value = cap > 0 ? Math.min(100, cap) : 0;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
ElMessage.error(resolveApiError(e, t, 'msg.load_failed'));
|
||||
transferVisible.value = false;
|
||||
} finally {
|
||||
transferContextLoading.value = false;
|
||||
@@ -105,10 +119,7 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise<void>) {
|
||||
transferVisible.value = false;
|
||||
await onSuccess?.();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
const fallback =
|
||||
transferType.value === 'deposit' ? t('msg.topup_failed') : t('msg.transfer_failed');
|
||||
ElMessage.error(err.response?.data?.error ?? fallback);
|
||||
ElMessage.error(resolveApiError(e, t, transferType.value === 'deposit' ? 'msg.topup_failed' : 'msg.transfer_failed'));
|
||||
} finally {
|
||||
transferLoading.value = false;
|
||||
}
|
||||
@@ -125,6 +136,8 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise<void>) {
|
||||
transferContextLoading,
|
||||
transferAmountRange,
|
||||
transferAmountDisabled,
|
||||
transferAmountExceedsCap,
|
||||
transferAmountCapError,
|
||||
transferTitle,
|
||||
openTransfer,
|
||||
submitTransfer,
|
||||
|
||||
@@ -15,7 +15,18 @@ export function resolveFormError(e: unknown, t: (key: string) => string): string
|
||||
return t('msg.form_invalid');
|
||||
}
|
||||
|
||||
/** 从 API 错误响应提取可读文案(Nest 全局过滤器返回 `error` 字段) */
|
||||
/** Nest 默认 4xx 响应里 error 常为 "Bad Request" 等泛化文案,真实原因在 message */
|
||||
const GENERIC_HTTP_ERRORS = new Set([
|
||||
'Bad Request',
|
||||
'Unauthorized',
|
||||
'Forbidden',
|
||||
'Not Found',
|
||||
'Conflict',
|
||||
'Unprocessable Entity',
|
||||
'Internal Server Error',
|
||||
]);
|
||||
|
||||
/** 从 API 错误响应提取可读文案(兼容 GlobalExceptionFilter 与 Nest 默认格式) */
|
||||
export function resolveApiError(
|
||||
err: unknown,
|
||||
t: (key: string) => string,
|
||||
@@ -23,8 +34,13 @@ export function resolveApiError(
|
||||
): string {
|
||||
const data = (err as { response?: { data?: { error?: string | string[]; message?: string | string[] } } })
|
||||
?.response?.data;
|
||||
const raw = data?.error ?? data?.message;
|
||||
if (Array.isArray(raw)) return raw.join(';');
|
||||
if (typeof raw === 'string' && raw.trim()) return raw;
|
||||
const msgRaw = data?.message;
|
||||
const errRaw = data?.error;
|
||||
const message = Array.isArray(msgRaw) ? msgRaw.join(';') : msgRaw;
|
||||
const error = Array.isArray(errRaw) ? errRaw.join(';') : errRaw;
|
||||
if (typeof message === 'string' && message.trim()) {
|
||||
if (!error || GENERIC_HTTP_ERRORS.has(error)) return message;
|
||||
}
|
||||
if (typeof error === 'string' && error.trim() && !GENERIC_HTTP_ERRORS.has(error)) return error;
|
||||
return t(fallbackKey);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import { resolveFormError } from '../i18n/form-validation';
|
||||
import { resolveFormError, resolveApiError } from '../i18n/form-validation';
|
||||
import api from '../api';
|
||||
import { clearStaffSession } from '../stores/auth';
|
||||
|
||||
@@ -43,6 +43,7 @@ import AgentCreditContext from '../components/AgentCreditContext.vue';
|
||||
import { useAdminPlayerTransfer } from '../composables/useAdminPlayerTransfer';
|
||||
import {
|
||||
fetchAdminAgentCreditContext,
|
||||
maxCreditIncreaseAmount,
|
||||
type AgentCreditAdjustContext,
|
||||
} from '../utils/agent-credit-context';
|
||||
|
||||
@@ -571,8 +572,7 @@ async function openCredit(userId: string) {
|
||||
try {
|
||||
creditContext.value = await fetchAdminAgentCreditContext(userId);
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
ElMessage.error(resolveApiError(e, t, 'msg.load_failed'));
|
||||
creditVisible.value = false;
|
||||
} finally {
|
||||
creditContextLoading.value = false;
|
||||
@@ -584,6 +584,11 @@ async function submitCredit() {
|
||||
ElMessage.warning(t('msg.credit_zero'));
|
||||
return;
|
||||
}
|
||||
const maxInc = maxCreditIncreaseAmount(creditContext.value);
|
||||
if (creditForm.value.amount > 0 && maxInc !== undefined && creditForm.value.amount > maxInc) {
|
||||
ElMessage.warning(t('err.insufficient_credit'));
|
||||
return;
|
||||
}
|
||||
creditLoading.value = true;
|
||||
try {
|
||||
await api.post(`/admin/agents/${editingId.value}/credit`, {
|
||||
@@ -596,8 +601,7 @@ async function submitCredit() {
|
||||
load();
|
||||
refreshExpandedParents();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.credit_adjust_failed'));
|
||||
ElMessage.error(resolveApiError(e, t, 'msg.credit_adjust_failed'));
|
||||
} finally {
|
||||
creditLoading.value = false;
|
||||
}
|
||||
@@ -737,6 +741,8 @@ const {
|
||||
transferContextLoading,
|
||||
transferAmountRange,
|
||||
transferAmountDisabled,
|
||||
transferAmountExceedsCap,
|
||||
transferAmountCapError,
|
||||
transferTitle,
|
||||
openTransfer,
|
||||
submitTransfer,
|
||||
@@ -1340,11 +1346,10 @@ function creditTypeLabel(type: string) {
|
||||
<el-form-item :label="t('common.col_id')">
|
||||
<span>{{ transferTarget?.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.amount')">
|
||||
<el-form-item :label="t('user.field.amount')" :error="transferAmountCapError">
|
||||
<el-input-number
|
||||
v-model="transferAmount"
|
||||
:min="transferAmountRange.min"
|
||||
:max="transferAmountRange.max"
|
||||
:disabled="transferAmountDisabled"
|
||||
:step="10"
|
||||
:precision="2"
|
||||
@@ -1360,7 +1365,7 @@ function creditTypeLabel(type: string) {
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="transferLoading"
|
||||
:disabled="transferAmountDisabled"
|
||||
:disabled="transferAmountDisabled || transferAmountExceedsCap"
|
||||
@click="submitTransfer"
|
||||
>
|
||||
{{ t('common.confirm') }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import { resolveFormError } from '../i18n/form-validation';
|
||||
import { resolveFormError, resolveApiError } from '../i18n/form-validation';
|
||||
import api from '../api';
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
@@ -27,6 +27,7 @@ import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
||||
import AgentCreditContext from '../components/AgentCreditContext.vue';
|
||||
import {
|
||||
fetchAdminAgentCreditContext,
|
||||
maxCreditIncreaseAmount,
|
||||
type AgentCreditAdjustContext,
|
||||
} from '../utils/agent-credit-context';
|
||||
|
||||
@@ -201,6 +202,11 @@ async function submitCredit() {
|
||||
ElMessage.warning(t('msg.credit_zero'));
|
||||
return;
|
||||
}
|
||||
const maxInc = maxCreditIncreaseAmount(creditContext.value);
|
||||
if (creditForm.value.amount > 0 && maxInc !== undefined && creditForm.value.amount > maxInc) {
|
||||
ElMessage.warning(t('err.insufficient_credit'));
|
||||
return;
|
||||
}
|
||||
creditLoading.value = true;
|
||||
try {
|
||||
await api.post(`/admin/agents/${editingId.value}/credit`, {
|
||||
@@ -212,8 +218,7 @@ async function submitCredit() {
|
||||
creditVisible.value = false;
|
||||
load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.credit_adjust_failed'));
|
||||
ElMessage.error(resolveApiError(e, t, 'msg.credit_adjust_failed'));
|
||||
} finally {
|
||||
creditLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ const {
|
||||
transferContextLoading,
|
||||
transferAmountRange,
|
||||
transferAmountDisabled,
|
||||
transferAmountExceedsCap,
|
||||
transferAmountCapError,
|
||||
transferTitle,
|
||||
openTransfer,
|
||||
submitTransfer,
|
||||
@@ -757,11 +759,10 @@ function statusLabel(s: string) {
|
||||
<el-form-item :label="t('common.col_id')">
|
||||
<span>{{ transferTarget?.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.amount')">
|
||||
<el-form-item :label="t('user.field.amount')" :error="transferAmountCapError">
|
||||
<el-input-number
|
||||
v-model="transferAmount"
|
||||
:min="transferAmountRange.min"
|
||||
:max="transferAmountRange.max"
|
||||
:disabled="transferAmountDisabled"
|
||||
:step="10"
|
||||
:precision="2"
|
||||
@@ -777,7 +778,7 @@ function statusLabel(s: string) {
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="transferLoading"
|
||||
:disabled="transferAmountDisabled"
|
||||
:disabled="transferAmountDisabled || transferAmountExceedsCap"
|
||||
@click="submitTransfer"
|
||||
>
|
||||
{{ t('common.confirm') }}
|
||||
|
||||
@@ -143,6 +143,20 @@ const transferAmountDisabled = computed(() => {
|
||||
return max === 0;
|
||||
});
|
||||
|
||||
/** 勿用 el-input-number :max,否则会静默截断到上限 */
|
||||
const transferAmountExceedsCap = computed(() => {
|
||||
const max = transferAmountRange.value.max;
|
||||
if (max === undefined) return false;
|
||||
return transferAmount.value > max;
|
||||
});
|
||||
|
||||
const transferAmountCapError = computed(() => {
|
||||
if (!transferAmountExceedsCap.value) return '';
|
||||
return transferType.value === 'deposit'
|
||||
? t('err.insufficient_credit')
|
||||
: t('transfer.context.withdraw_exceed');
|
||||
});
|
||||
|
||||
/* ─── Init ─── */
|
||||
onMounted(async () => {
|
||||
await loadProfile();
|
||||
@@ -818,17 +832,20 @@ function statusTagType(s: string) {
|
||||
<el-form-item :label="t('common.col_id')">
|
||||
<span>{{ transferTarget?.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.amount')">
|
||||
<el-form-item :label="t('user.field.amount')" :error="transferAmountCapError">
|
||||
<el-input-number
|
||||
v-model="transferAmount"
|
||||
:min="transferAmountRange.min" :max="transferAmountRange.max"
|
||||
:disabled="transferAmountDisabled" :step="10" :precision="2" style="width: 100%"
|
||||
:min="transferAmountRange.min"
|
||||
:disabled="transferAmountDisabled"
|
||||
:step="10"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="transferVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="transferLoading" :disabled="transferAmountDisabled" @click="submitTransfer">{{ t('common.confirm') }}</el-button>
|
||||
<el-button type="primary" :loading="transferLoading" :disabled="transferAmountDisabled || transferAmountExceedsCap" @click="submitTransfer">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user