Files
gongxue-base/apps/server/src/imports/imports.helpers.ts
wangziqi ae88372ef8 fix(imports): 修复 AI 导入向导多工作表与表头误判
- AI resolve 生成向导时按阶段携带全部 sheetNames,不再只取第一张表
- ImportStageRequest 支持 sheets 数组并兼容旧 sheet;手动重传同步修复
- headerMatches 收窄为单向包含,避免宿舍号被原/新宿舍号反向匹配
- suggestStep 增加入住/换宿显式表头信号,修复入住表误判为换宿
- 预检与预览按工作表逐表解析列映射,兼容异构表头
- 修复预检卡生成向导成功后按钮未复位 loading 的问题
- 补充 mapping/预检/run/ai-chat 多工作表测试
2026-08-06 14:46:33 +08:00

98 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as ExcelJS from 'exceljs';
import type { CellValue } from './imports.types';
export function parseJson<T>(raw: string | null | undefined): T | null {
if (!raw) return null;
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
export function textValue(value: CellValue): string {
if (value === null || value === undefined) return '';
return String(value).trim();
}
export function normalizeHeader(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[\s()]/g, '');
}
export function headerMatches(header: string, alias: string): boolean {
const h = normalizeHeader(header);
const a = normalizeHeader(alias);
if (!h || !a) return false;
// 只允许“表头包含别名”的单向匹配,避免短表头(如“宿舍号”)
// 被长复合别名(如“原宿舍号/新宿舍号”)反向包含而误判。
return h === a || h.includes(a);
}
export function cellValue(cell: ExcelJS.Cell | undefined): CellValue {
if (!cell) return null;
const value = cell.value;
if (value === null || value === undefined) return null;
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (value instanceof Date) return value;
if (typeof value === 'object') {
const candidate = value as { text?: unknown; result?: unknown };
if (typeof candidate.text === 'string') return candidate.text;
if (typeof candidate.result === 'string' || typeof candidate.result === 'number') {
return candidate.result;
}
if (candidate.result instanceof Date) return candidate.result;
}
return null;
}
export function parseDateValue(value: CellValue): string | null {
if (value instanceof Date && !Number.isNaN(value.getTime())) {
const year = value.getFullYear();
const month = String(value.getMonth() + 1).padStart(2, '0');
const day = String(value.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
const raw = textValue(value);
if (!raw) return null;
const match = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/.exec(raw);
if (!match) return null;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const date = new Date(Date.UTC(year, month - 1, day));
if (
date.getUTCFullYear() !== year ||
date.getUTCMonth() !== month - 1 ||
date.getUTCDate() !== day
) {
return null;
}
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
export function safeError(error: unknown): string {
if (error instanceof Error) return error.message.slice(0, 120);
return '未知错误';
}
export function applyString(target: object, key: string, value: CellValue): void {
const text = textValue(value);
if (text) (target as Record<string, unknown>)[key] = text;
}
export function optionalNumber(value: CellValue): number | null {
const text = textValue(value);
if (!text) return null;
const parsed = Number(text);
return Number.isFinite(parsed) ? parsed : null;
}
export function csvCell(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}