fix(admin): reject negative platform quota limits before saving

This commit is contained in:
chihao.ou
2026-09-16 18:17:29 +08:00
parent 881f320269
commit 0838e0e610
2 changed files with 26 additions and 6 deletions
@@ -231,14 +231,12 @@ function onClearAll() {
async function onSave() {
if (!props.user) return
// 校验所有 input:v-model.number 在用户输入"0."等中间状态时会写回 NaN,
// 之前的 normalizeLimit(NaN) 静默返回 null("无限制"),把"有限额"配置悄悄改成"无限制"。
// 这里在 save 前显式检测 NaN,提示用户修正后再提交。
// 拒绝非法数值,避免 normalizeLimit 将其静默转换为 null(无限额)。
const invalid: string[] = []
for (const row of quotas.value) {
for (const win of ['daily', 'weekly', 'monthly'] as const) {
const v = row[`${win}_limit_usd` as const]
if (typeof v === 'number' && Number.isNaN(v)) {
if (typeof v === 'number' && (!Number.isFinite(v) || v < 0)) {
invalid.push(`${row.platform}.${win}`)
}
}
@@ -268,7 +266,7 @@ async function onSave() {
}
// 仅在合法输入下返回数字:null/undefined/NaN/±Inf/负数 → null(视为"无限额")。
// 调用方负责在 NaN 路径上做单独的用户提示(见 onSave)。
// 调用方在保存前拦截非法数值(见 onSave)。
function normalizeLimit(v: number | null | undefined): number | null {
if (v === null || v === undefined) return null
if (typeof v === 'number' && Number.isFinite(v) && v >= 0) return v
@@ -3,6 +3,7 @@ import { mount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
const apiMocks = vi.hoisted(() => ({
showError: vi.fn(),
getPlatformQuotas: vi.fn(),
updatePlatformQuotas: vi.fn(),
resetPlatformQuotaWindow: vi.fn(),
@@ -20,7 +21,7 @@ vi.mock('@/api/admin', () => ({
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError: vi.fn(),
showError: apiMocks.showError,
showSuccess: vi.fn(),
}),
}))
@@ -74,6 +75,27 @@ beforeEach(() => {
})
describe('UserPlatformQuotaModal', () => {
it.each([0, 4, 14])('does not turn a negative limit in input %s into unlimited', async (index) => {
const w = await mountAndOpen()
await w.findAll('input[type=number]')[index].setValue('-1')
await w.findAll('button').find(b => b.text() === 'admin.users.platformQuota.save')!.trigger('click')
await flushPromises()
expect(apiMocks.updatePlatformQuotas).not.toHaveBeenCalled()
expect(apiMocks.showError).toHaveBeenCalledWith('admin.users.platformQuota.invalidNumber')
expect(w.emitted('success')).toBeUndefined()
w.unmount()
})
it.each([['0', 0], ['', null]])('preserves the meaning of a limit entered as %j', async (input, expected) => {
const w = await mountAndOpen()
await w.findAll('input[type=number]')[0].setValue(input)
await w.findAll('button').find(b => b.text() === 'admin.users.platformQuota.save')!.trigger('click')
await flushPromises()
expect(apiMocks.updatePlatformQuotas).toHaveBeenCalledTimes(1)
expect(apiMocks.updatePlatformQuotas.mock.calls[0][1][0].daily_limit_usd).toBe(expected)
w.unmount()
})
it('挂载并 show=true 时调用 getPlatformQuotas', async () => {
await mountAndOpen()
expect(apiMocks.getPlatformQuotas).toHaveBeenCalledWith(99)