From 9f10423ff9dbefac732f6b58c4e0e795fd597751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 3 Oct 2026 17:58:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E5=90=8E=E5=8F=B0=E8=A1=A8=E5=8D=95):?= =?UTF-8?q?=20=E6=8A=BD=E5=8F=96=E5=85=B1=E4=BA=AB=E6=95=B4=E6=95=B0?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pageUtils 新增严格的 parsePositiveInteger / parseNonNegativeInteger 与落库用 parsePositiveIntegerOrZero / parseNonNegativeIntegerOrZero。 - 账号配置、会员档位、充值商品三页删除各自的同名本地解析器,统一走共享实现,消除同名不同语义的校验漂移。 - 解析从 parseInt 改为 Number + isSafeInteger,'12abc' 一类脏输入不再被截断成 12。 --- .../src/pages/AdminMembershipPlanPage.tsx | 21 ++++---------- .../pages/AdminProfileWalletConfigPage.tsx | 19 ++++--------- .../src/pages/AdminRechargeProductPage.tsx | 22 ++++++--------- apps/admin-web/src/pages/pageUtils.ts | 28 +++++++++++++++++++ 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/apps/admin-web/src/pages/AdminMembershipPlanPage.tsx b/apps/admin-web/src/pages/AdminMembershipPlanPage.tsx index 0dc67a2bd..07c4ecea7 100644 --- a/apps/admin-web/src/pages/AdminMembershipPlanPage.tsx +++ b/apps/admin-web/src/pages/AdminMembershipPlanPage.tsx @@ -23,7 +23,7 @@ import type { } from '../api/adminApiTypes'; import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm'; import { membershipPlanLabels } from '../config/membershipDisplay'; -import { handlePageError } from './pageUtils'; +import { handlePageError, parsePositiveIntegerOrZero } from './pageUtils'; interface AdminMembershipPlanPageProps { token: string; @@ -106,11 +106,12 @@ export function AdminMembershipPlanPage({ } setErrorMessage(''); - const normalizedMonthPriceCents = parsePositiveInteger(monthPriceCents); - const normalizedYearPriceCents = parsePositiveInteger(yearPriceCents); + const normalizedMonthPriceCents = + parsePositiveIntegerOrZero(monthPriceCents); + const normalizedYearPriceCents = parsePositiveIntegerOrZero(yearPriceCents); const normalizedConcurrentJobLimit = - parsePositiveInteger(concurrentJobLimit); - const normalizedPeriodPoints = parsePositiveInteger(periodPoints); + parsePositiveIntegerOrZero(concurrentJobLimit); + const normalizedPeriodPoints = parsePositiveIntegerOrZero(periodPoints); if (plan !== 'normal') { if (!normalizedMonthPriceCents || !normalizedYearPriceCents) { setErrorMessage('月价与年价必须是大于 0 的整数分'); @@ -454,13 +455,3 @@ function formatPlanLimits(entry: ProfileMembershipPlanAdminResponse) { function formatPrice(priceCents: number) { return `¥${(priceCents / 100).toFixed(2)}`; } - -function parsePositiveInteger(value: string) { - const parsed = parseInteger(value); - return parsed > 0 ? parsed : 0; -} - -function parseInteger(value: string) { - const parsed = Number(value); - return Number.isSafeInteger(parsed) ? parsed : 0; -} diff --git a/apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx b/apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx index 434451913..3d157fcdc 100644 --- a/apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx +++ b/apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx @@ -18,7 +18,11 @@ import { } from '../api/adminApiClient'; import type { ProfileWalletConfigAdminResponse } from '../api/adminApiTypes'; import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm'; -import { handlePageError } from './pageUtils'; +import { + handlePageError, + parseNonNegativeInteger, + parsePositiveInteger, +} from './pageUtils'; interface AdminProfileWalletConfigPageProps { token: string; @@ -200,16 +204,3 @@ export function AdminProfileWalletConfigPage({ ); } - -function parsePositiveInteger(value: string) { - const parsed = Number(value); - return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0; -} - -function parseNonNegativeInteger(value: string): number | null { - if (value.trim() === '') { - return null; - } - const parsed = Number(value); - return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null; -} diff --git a/apps/admin-web/src/pages/AdminRechargeProductPage.tsx b/apps/admin-web/src/pages/AdminRechargeProductPage.tsx index 488284c4b..a0bba36bb 100644 --- a/apps/admin-web/src/pages/AdminRechargeProductPage.tsx +++ b/apps/admin-web/src/pages/AdminRechargeProductPage.tsx @@ -19,7 +19,11 @@ import { } from '../api/adminApiClient'; import type { ProfileRechargeProductConfigAdminResponse } from '../api/adminApiTypes'; import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm'; -import { handlePageError } from './pageUtils'; +import { + handlePageError, + parseNonNegativeIntegerOrZero, + parsePositiveIntegerOrZero, +} from './pageUtils'; interface AdminRechargeProductPageProps { token: string; @@ -104,10 +108,10 @@ export function AdminRechargeProductPage({ const response = await upsertProfileRechargeProduct(token, { productId: productId.trim(), title: title.trim(), - priceCents: parsePositiveInteger(priceCents), + priceCents: parsePositiveIntegerOrZero(priceCents), kind: 'points', - pointsAmount: parsePositiveInteger(pointsAmount), - bonusPoints: parseNonNegativeInteger(bonusPoints), + pointsAmount: parsePositiveIntegerOrZero(pointsAmount), + bonusPoints: parseNonNegativeIntegerOrZero(bonusPoints), durationDays: 0, badgeLabel: badgeLabel.trim(), description: description.trim(), @@ -358,16 +362,6 @@ function formatPrice(priceCents: number) { return `¥${(priceCents / 100).toFixed(2)}`; } -function parsePositiveInteger(value: string) { - const parsed = parseInteger(value); - return parsed > 0 ? parsed : 0; -} - -function parseNonNegativeInteger(value: string) { - const parsed = parseInteger(value); - return parsed > 0 ? parsed : 0; -} - function parseInteger(value: string) { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed)) { diff --git a/apps/admin-web/src/pages/pageUtils.ts b/apps/admin-web/src/pages/pageUtils.ts index bea44fd7e..ed90abdd9 100644 --- a/apps/admin-web/src/pages/pageUtils.ts +++ b/apps/admin-web/src/pages/pageUtils.ts @@ -31,3 +31,31 @@ export function formatUnknownJson(value: unknown) { return String(value); } } + +/** 严格解析正安全整数:空串、小数、非正数与非数字都返回 `null`,用于表单校验。 */ +export function parsePositiveInteger(value: string): number | null { + if (value.trim() === '') { + return null; + } + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null; +} + +/** 严格解析非负安全整数:空串、小数、负数与非数字都返回 `null`,用于表单校验。 */ +export function parseNonNegativeInteger(value: string): number | null { + if (value.trim() === '') { + return null; + } + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null; +} + +/** 落库用宽松解析:非法输入按 `0` 处理,避免后台保存被输入抖动打断。 */ +export function parsePositiveIntegerOrZero(value: string): number { + return parsePositiveInteger(value) ?? 0; +} + +/** 落库用宽松解析:非法输入按 `0` 处理,避免后台保存被输入抖动打断。 */ +export function parseNonNegativeIntegerOrZero(value: string): number { + return parseNonNegativeInteger(value) ?? 0; +}