From ae07f8a533faeee6e5276c050ea99392c88decde Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Sun, 19 Jul 2026 11:43:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(analytics):=20=E4=BF=AE=E5=A4=8D=E7=8E=AF?= =?UTF-8?q?=E6=AF=94=20NaN%/Infinity%=20=E6=98=BE=E7=A4=BA=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: chainRatio 函数只防 undefined/0, 未防 null/NaN/Infinity - 后端 Long 字段返回 null(无上期数据)时, curr=null/prev=null 算出 NaN - curr=有效值/prev=null 时算出 Infinity, StatCard 渲染 '↓ NaN%'/'↓ Infinity%' 修复: - chainRatio 用 Number.isFinite 统一拦截 null/undefined/NaN/Infinity - StatCard 模板加 Number.isFinite 二次保险 - merchantRank.proportion / channelSuccess.rate 加 Number.isFinite 兜底 --- .../src/components/charts/StatCard.vue | 2 +- .../analytics/composables/useAnalyticsData.ts | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/daxpay-admin/src/components/charts/StatCard.vue b/apps/daxpay-admin/src/components/charts/StatCard.vue index 869135d83..47a9acfec 100644 --- a/apps/daxpay-admin/src/components/charts/StatCard.vue +++ b/apps/daxpay-admin/src/components/charts/StatCard.vue @@ -57,7 +57,7 @@
diff --git a/apps/daxpay-admin/src/views/dashboard/analytics/composables/useAnalyticsData.ts b/apps/daxpay-admin/src/views/dashboard/analytics/composables/useAnalyticsData.ts index 63323c1ed..f306227a0 100644 --- a/apps/daxpay-admin/src/views/dashboard/analytics/composables/useAnalyticsData.ts +++ b/apps/daxpay-admin/src/views/dashboard/analytics/composables/useAnalyticsData.ts @@ -49,11 +49,16 @@ function fenToYuan(fen?: number): number { /** * 环比百分比计算 + * + * 防护策略:后端 Long 字段可能返回 null(无上期数据),或前端派生计算产生 NaN/Infinity。 + * Number.isFinite 一次性拦截 null/undefined/NaN/Infinity 四类无效输入, + * prev=0 时无法计算环比(除零),统一返回 null(显示 "—")。 + * * @returns null 表示无法计算(prev 缺数据或为 0) */ -function chainRatio(curr?: number, prev?: number): null | number { - if (curr === undefined || prev === undefined || prev === 0) return null; - return Math.round(((curr - prev) / prev) * 1000) / 10; +function chainRatio(curr?: null | number, prev?: null | number): null | number { + if (!Number.isFinite(curr) || !Number.isFinite(prev) || prev === 0) return null; + return Math.round(((curr as number - (prev as number)) / (prev as number)) * 1000) / 10; } /** @@ -267,7 +272,8 @@ function buildAnalyticsData(input: { // ===== channelSuccess(渠道成功率) ===== const channelSuccess = (providerSuccessRes ?? []).map((i) => ({ name: providerLabel(i.provider), - rate: i.rate ?? 0, + // 后端 NULLIF 防除零, 这里 Number.isFinite 二次兜底防异常值 + rate: Number.isFinite(i.rate) ? (i.rate as number) : 0, })); // ===== hourlyDist(24 小时时段, 取 count 数组) ===== @@ -285,7 +291,8 @@ function buildAnalyticsData(input: { amount: fenToYuan(i.amount), merchantName: i.merchantName ?? '-', orders: i.orders ?? 0, - proportion: i.proportion ?? 0, + // 后端 computeProportion 已防除零, 这里 Number.isFinite 二次兜底防异常值 + proportion: Number.isFinite(i.proportion) ? (i.proportion as number) : 0, })); return { amountRange, channelSuccess, channelVolume, hourlyDist, merchantRank, overview, payMethod, refundTrend, tradeTrend };