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 };