From 120e1e5d6b93237b349e7fad58a4dab06011337c Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 04:34:37 +0800 Subject: [PATCH] fix(aihr): validate pilot export date window --- frontend/src/views/train/pilot-export.test.ts | 6 ++++++ frontend/src/views/train/pilot-export.ts | 14 ++++++++++++- frontend/src/views/train/reviews.vue | 21 ++++++++++++++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/train/pilot-export.test.ts b/frontend/src/views/train/pilot-export.test.ts index 0c9d3e6c..472be66c 100644 --- a/frontend/src/views/train/pilot-export.test.ts +++ b/frontend/src/views/train/pilot-export.test.ts @@ -13,4 +13,10 @@ describe('resolvePilotDateWindow', () => { endDate: '2026-07-10' }); }); + + it('rejects malformed or reversed date windows', () => { + expect(resolvePilotDateWindow(['2026-7-07', '2026-07-10'])).toBeNull(); + expect(resolvePilotDateWindow(['2026-02-30', '2026-03-01'])).toBeNull(); + expect(resolvePilotDateWindow(['2026-07-10', '2026-07-07'])).toBeNull(); + }); }); diff --git a/frontend/src/views/train/pilot-export.ts b/frontend/src/views/train/pilot-export.ts index e4eced67..a6c3406c 100644 --- a/frontend/src/views/train/pilot-export.ts +++ b/frontend/src/views/train/pilot-export.ts @@ -3,12 +3,24 @@ export type PilotDateWindow = { endDate: string; }; +const isIsoDate = (value: string) => { + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return false; + } + const [year, month, day] = value.split('-').map(Number); + if (year < 1000 || month < 1 || month > 12 || day < 1) { + return false; + } + const date = new Date(Date.UTC(year, month - 1, day)); + return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day; +}; + export function resolvePilotDateWindow(value: unknown): PilotDateWindow | null { if (!Array.isArray(value) || value.length !== 2) { return null; } const [startDate, endDate] = value; - if (typeof startDate !== 'string' || typeof endDate !== 'string' || !startDate || !endDate) { + if (typeof startDate !== 'string' || typeof endDate !== 'string' || !isIsoDate(startDate) || !isIsoDate(endDate) || startDate > endDate) { return null; } return { startDate, endDate }; diff --git a/frontend/src/views/train/reviews.vue b/frontend/src/views/train/reviews.vue index be491453..ebae1c55 100644 --- a/frontend/src/views/train/reviews.vue +++ b/frontend/src/views/train/reviews.vue @@ -147,6 +147,7 @@ import { Refresh } from '@element-plus/icons-vue'; import { ElMessage } from 'element-plus'; import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; import { calibratePracticeSession, exportPracticePilotCsv, listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice'; +import { resolvePilotDateWindow } from './pilot-export'; import { getPracticeAudioBlobUrl, getOrgSnapshot, @@ -174,6 +175,7 @@ const metrics = reactive({ calibrationCount: 0, consistency: '--', sopUsable: '- const audioSources = ref>({}); const audioLoading = ref>({}); const audioErrors = ref>({}); +const pilotDateWindow = computed(() => resolvePilotDateWindow([startDate.value, endDate.value])); const reviewForm = reactive({ reviewer: '主管', @@ -258,8 +260,16 @@ const loadReviews = async () => { }; const loadMetrics = async () => { + const dateWindow = pilotDateWindow.value; + if (!dateWindow) { + metrics.calibrationCount = 0; + metrics.consistency = '--'; + metrics.sopUsable = '--'; + metrics.ready = false; + return; + } try { - const blob = await exportPracticePilotCsv({ startDate: startDate.value, endDate: endDate.value }); + const blob = await exportPracticePilotCsv(dateWindow); const csv = await blob.text(); const value = (key: string) => csv.match(new RegExp(`(?:^|\\n)"?${key}"?,"?([^"\\n]*)`))?.[1] || ''; metrics.calibrationCount = Number(value('calibration_sample_count')) || 0; @@ -275,13 +285,18 @@ const loadMetrics = async () => { }; const exportPilot = async () => { + const dateWindow = pilotDateWindow.value; + if (!dateWindow) { + ElMessage.warning('请选择有效的试点起止日期'); + return; + } exporting.value = true; try { - const blob = await exportPracticePilotCsv({ startDate: startDate.value, endDate: endDate.value }); + const blob = await exportPracticePilotCsv(dateWindow); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = `aihr-pilot-${startDate.value}-${endDate.value}.csv`; + link.download = `aihr-pilot-${dateWindow.startDate}-${dateWindow.endDate}.csv`; link.click(); URL.revokeObjectURL(url); ElMessage.success('试点 CSV 已导出');