fix(imports): 修复 AI 导入向导多工作表与表头误判

- AI resolve 生成向导时按阶段携带全部 sheetNames,不再只取第一张表
- ImportStageRequest 支持 sheets 数组并兼容旧 sheet;手动重传同步修复
- headerMatches 收窄为单向包含,避免宿舍号被原/新宿舍号反向匹配
- suggestStep 增加入住/换宿显式表头信号,修复入住表误判为换宿
- 预检与预览按工作表逐表解析列映射,兼容异构表头
- 修复预检卡生成向导成功后按钮未复位 loading 的问题
- 补充 mapping/预检/run/ai-chat 多工作表测试
This commit is contained in:
2026-08-06 14:46:33 +08:00
parent 259271f56c
commit ae88372ef8
15 changed files with 355 additions and 27 deletions

View File

@@ -1884,7 +1884,7 @@ describe('AiChatService', () => {
'ai',
expect.objectContaining({ originalName: 'students.xlsx' }),
3,
[{ stepKey: 'students', sheet: '学生', headerRow: 1 }],
[{ stepKey: 'students', sheets: ['学生'], headerRow: 1 }],
{ students: { name: '姓名', studentNo: '学号' } },
{ updateExisting: false },
);
@@ -1906,6 +1906,101 @@ describe('AiChatService', () => {
).toMatchObject({ resolved: true, runId: 'run-9' });
});
it('resolveImportPreflight 多工作表阶段携带全部 sheetNames 生成导入任务', async () => {
const { service } = createService();
const conversations = {
findOne: jest.fn().mockResolvedValue({ id: 3, userId: 7 }),
};
const message = {
id: 42,
conversationId: 3,
role: 'assistant',
metadata: {
a2uiImportPreflight: {
verdict: 'needs_input',
stages: [
{
stepKey: 'checkins',
label: '入住管理',
sheetNames: ['四人间女', '四人间男'],
headers: ['姓名', '学号', '手机号', '宿舍号', '入住日期'],
mapping: { name: '姓名', roomNumber: '宿舍号' },
missingRequired: [],
total: 2,
create: 2,
update: 0,
error: 0,
skip: 0,
},
],
blocks: [],
questions: [],
nextSteps: [],
errorSamples: [],
attachmentId: 9,
headerRow: 1,
permittedSteps: ['checkins'],
resolved: false,
runId: null,
},
},
};
const messages = {
findOne: jest.fn().mockResolvedValue(message),
save: jest.fn(async (value) => value),
exists: jest.fn().mockResolvedValue(false),
};
const attachmentService = {
requireReadyOwned: jest.fn().mockResolvedValue([
{
id: 9,
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
originalName: 'dorm.xlsx',
size: 10,
},
]),
readStoredBuffer: jest.fn().mockResolvedValue(Buffer.from('x')),
};
const importsService = {
createRun: jest.fn().mockResolvedValue({
id: 'run-9',
fileName: 'dorm.xlsx',
sheets: [],
steps: [
{
stepKey: 'checkins',
label: '入住管理',
sheets: ['四人间女', '四人间男'],
status: 'pending',
},
],
}),
};
(service as unknown as { conversations: unknown }).conversations = conversations;
(service as unknown as { messages: unknown }).messages = messages;
(service as unknown as { attachmentService: unknown }).attachmentService = attachmentService;
(service as unknown as { importsService: unknown }).importsService = importsService;
await service.resolveImportPreflight(
authenticatedUser,
42,
{ clientRequestId: 'multi-sheet', mapping: {}, settings: {} },
new AbortController().signal,
jest.fn(),
jest.fn(),
);
expect(importsService.createRun).toHaveBeenCalledWith(
{ id: 7, permissions: ['ai:chat:use'], isSuperAdmin: false },
'ai',
expect.objectContaining({ originalName: 'dorm.xlsx' }),
3,
[{ stepKey: 'checkins', sheets: ['四人间女', '四人间男'], headerRow: 1 }],
{},
{},
);
});
it('resolveImportPreflight 已生成向导时幂等重放,不重复建任务', async () => {
const { service } = createService();
const conversations = {