fix(aihr): validate pilot export date window

This commit is contained in:
2026-07-14 04:34:37 +08:00
parent c5a6e677dd
commit 120e1e5d6b
3 changed files with 37 additions and 4 deletions
@@ -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();
});
});
+13 -1
View File
@@ -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 };
+18 -3
View File
@@ -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<Record<string, string>>({});
const audioLoading = ref<Record<string, boolean>>({});
const audioErrors = ref<Record<string, string>>({});
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 已导出');