refactor: 移除 SQLite 支持,仅保留 MySQL
This commit is contained in:
@@ -1,64 +0,0 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AddAiChat1784780000000 } from '../migrations/1784780000000-AddAiChat';
|
||||
import { EnhanceAiChatForAntDesignX1784860000000 } from '../migrations/1784860000000-EnhanceAiChatForAntDesignX';
|
||||
import { DropAiMessageFeedback1784920000000 } from '../migrations/1784920000000-DropAiMessageFeedback';
|
||||
|
||||
describe('EnhanceAiChatForAntDesignX1784860000000', () => {
|
||||
let dataSource: DataSource;
|
||||
|
||||
beforeEach(async () => {
|
||||
dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: ':memory:',
|
||||
migrations: [AddAiChat1784780000000, EnhanceAiChatForAntDesignX1784860000000],
|
||||
});
|
||||
await dataSource.initialize();
|
||||
await dataSource.query(
|
||||
'CREATE TABLE users (id integer PRIMARY KEY AUTOINCREMENT, username varchar(100) NOT NULL)',
|
||||
);
|
||||
await dataSource.query(
|
||||
'CREATE TABLE ai_config (id integer PRIMARY KEY AUTOINCREMENT, singleton_key varchar(20) NOT NULL)',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (dataSource.isInitialized) await dataSource.destroy();
|
||||
});
|
||||
|
||||
it('adds Ant Design X chat fields and attachment relations', async () => {
|
||||
await dataSource.runMigrations();
|
||||
const runner = dataSource.createQueryRunner();
|
||||
for (const table of ['ai_attachments', 'ai_message_attachments']) {
|
||||
expect(await runner.hasTable(table)).toBe(true);
|
||||
}
|
||||
expect(await runner.hasColumn('ai_config', 'supports_vision')).toBe(true);
|
||||
expect(await runner.hasColumn('ai_conversations', 'locked_skill_key')).toBe(true);
|
||||
expect(await runner.hasColumn('ai_messages', 'feedback')).toBe(true);
|
||||
expect(await runner.hasColumn('ai_tool_runs', 'skill_key')).toBe(true);
|
||||
await runner.release();
|
||||
});
|
||||
|
||||
it('drops the removed like/dislike feedback columns', async () => {
|
||||
dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: ':memory:',
|
||||
migrations: [
|
||||
AddAiChat1784780000000,
|
||||
EnhanceAiChatForAntDesignX1784860000000,
|
||||
DropAiMessageFeedback1784920000000,
|
||||
],
|
||||
});
|
||||
await dataSource.initialize();
|
||||
await dataSource.query(
|
||||
'CREATE TABLE users (id integer PRIMARY KEY AUTOINCREMENT, username varchar(100) NOT NULL)',
|
||||
);
|
||||
await dataSource.query(
|
||||
'CREATE TABLE ai_config (id integer PRIMARY KEY AUTOINCREMENT, singleton_key varchar(20) NOT NULL)',
|
||||
);
|
||||
await dataSource.runMigrations();
|
||||
const runner = dataSource.createQueryRunner();
|
||||
expect(await runner.hasColumn('ai_messages', 'feedback')).toBe(false);
|
||||
expect(await runner.hasColumn('ai_messages', 'feedback_reason')).toBe(false);
|
||||
await runner.release();
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AddAiChat1784780000000 } from '../migrations/1784780000000-AddAiChat';
|
||||
|
||||
describe('AddAiChat1784780000000', () => {
|
||||
let dataSource: DataSource;
|
||||
|
||||
beforeEach(async () => {
|
||||
dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: ':memory:',
|
||||
migrations: [AddAiChat1784780000000],
|
||||
});
|
||||
await dataSource.initialize();
|
||||
await dataSource.query(
|
||||
'CREATE TABLE users (id integer PRIMARY KEY AUTOINCREMENT, username varchar(100) NOT NULL)',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (dataSource.isInitialized) await dataSource.destroy();
|
||||
});
|
||||
|
||||
it('创建会话、消息和工具记录表,并按会话级联删除', async () => {
|
||||
await dataSource.runMigrations();
|
||||
|
||||
for (const table of ['ai_conversations', 'ai_messages', 'ai_tool_runs']) {
|
||||
expect(await dataSource.createQueryRunner().hasTable(table)).toBe(true);
|
||||
}
|
||||
|
||||
await dataSource.query("INSERT INTO users (username) VALUES ('tester')");
|
||||
await dataSource.query(
|
||||
"INSERT INTO ai_conversations (user_id, title) VALUES (1, '测试会话')",
|
||||
);
|
||||
await dataSource.query(
|
||||
"INSERT INTO ai_messages (conversation_id, role, content) VALUES (1, 'assistant', '回答')",
|
||||
);
|
||||
await dataSource.query(
|
||||
"INSERT INTO ai_tool_runs (message_id, tool_call_id, tool_name, status) VALUES (1, 'call_1', 'search_students', 'success')",
|
||||
);
|
||||
|
||||
await dataSource.query('DELETE FROM ai_conversations WHERE id = 1');
|
||||
|
||||
expect(await dataSource.query('SELECT id FROM ai_messages')).toEqual([]);
|
||||
expect(await dataSource.query('SELECT id FROM ai_tool_runs')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AddA2UiReviews1784880000000 } from '../migrations/1784880000000-AddA2UiReviews';
|
||||
import { EnlargeAiReviewSections1784900000000 } from '../migrations/1784900000000-EnlargeAiReviewSections';
|
||||
|
||||
describe('EnlargeAiReviewSections1784900000000', () => {
|
||||
let dataSource: DataSource;
|
||||
|
||||
beforeEach(async () => {
|
||||
dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: ':memory:',
|
||||
migrations: [AddA2UiReviews1784880000000, EnlargeAiReviewSections1784900000000],
|
||||
});
|
||||
await dataSource.initialize();
|
||||
await dataSource.query(`
|
||||
CREATE TABLE ai_messages (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id integer NOT NULL,
|
||||
role varchar(20) NOT NULL,
|
||||
content text,
|
||||
reasoning_content text,
|
||||
status varchar(20) NOT NULL,
|
||||
error_code varchar(50),
|
||||
reply_to_message_id integer,
|
||||
feedback varchar(10),
|
||||
feedback_reason varchar(500),
|
||||
metadata text,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (dataSource.isInitialized) await dataSource.destroy();
|
||||
});
|
||||
|
||||
it('迁移后可保存远超 256KB 的预览数据,且再次执行幂等', async () => {
|
||||
await dataSource.runMigrations();
|
||||
await dataSource.runMigrations();
|
||||
|
||||
await dataSource.query(
|
||||
`INSERT INTO ai_messages (conversation_id, role, content, status)
|
||||
VALUES (1, 'assistant', '', 'completed')`,
|
||||
);
|
||||
const big = '中'.repeat(300 * 1024);
|
||||
await dataSource.query(
|
||||
`INSERT INTO ai_reviews
|
||||
(id, conversation_id, user_id, assistant_message_id, title, sections_json, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
['review-1', 1, 1, 1, '大体积导入', big, 'pending'],
|
||||
);
|
||||
const rows: Array<{ sections_json: string }> = await dataSource.query(
|
||||
'SELECT sections_json FROM ai_reviews WHERE id = ?',
|
||||
['review-1'],
|
||||
);
|
||||
expect(rows[0].sections_json.length).toBe(big.length);
|
||||
|
||||
const runner = dataSource.createQueryRunner();
|
||||
expect(await runner.hasColumn('ai_reviews', 'sections_json')).toBe(true);
|
||||
await runner.release();
|
||||
});
|
||||
});
|
||||
@@ -637,696 +637,3 @@ describe('AiReviewService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('AiReviewService.submit (real sqlite transaction)', () => {
|
||||
let dataSource: DataSource;
|
||||
let service: AiReviewService;
|
||||
let hostOrg: Organization;
|
||||
let namedOrg: Organization;
|
||||
let assistantMessageId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: ':memory:',
|
||||
entities: Object.values(allEntities).filter(
|
||||
(value): value is new (...args: unknown[]) => unknown =>
|
||||
typeof value === 'function',
|
||||
),
|
||||
synchronize: true,
|
||||
});
|
||||
await dataSource.initialize();
|
||||
const orgRepo = dataSource.getRepository(Organization);
|
||||
hostOrg = await orgRepo.save(
|
||||
orgRepo.create({ publicId: 'host', code: 'HOST', name: '恭学总校', isHost: true }),
|
||||
);
|
||||
namedOrg = await orgRepo.save(
|
||||
orgRepo.create({ publicId: 'org-a', code: 'ORG_A', name: '东校区' }),
|
||||
);
|
||||
const userRepo = dataSource.getRepository(User);
|
||||
const user = await userRepo.save(
|
||||
userRepo.create({ username: 'review-tester', passwordHash: 'x' }),
|
||||
);
|
||||
const conversationRepo = dataSource.getRepository(AiConversation);
|
||||
const conversation = await conversationRepo.save(
|
||||
conversationRepo.create({ userId: user.id, title: '测试会话' }),
|
||||
);
|
||||
const messageRepo = dataSource.getRepository(AiMessage);
|
||||
const assistant = await messageRepo.save(
|
||||
messageRepo.create({
|
||||
conversationId: conversation.id,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
status: 'completed',
|
||||
}),
|
||||
);
|
||||
assistantMessageId = assistant.id;
|
||||
const reviewRepo = dataSource.getRepository(AiReview);
|
||||
service = new AiReviewService(reviewRepo, dataSource);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await dataSource.destroy();
|
||||
});
|
||||
|
||||
it('按 学生→宿舍→换宿 顺序事务入库,并收集逐行问题', async () => {
|
||||
const studentRepo = dataSource.getRepository(Student);
|
||||
const roomRepo = dataSource.getRepository(Room);
|
||||
const bedRepo = dataSource.getRepository(Bed);
|
||||
const occRepo = dataSource.getRepository(Occupancy);
|
||||
|
||||
const existing = await studentRepo.save(
|
||||
studentRepo.create({
|
||||
name: '老王',
|
||||
phone: '13800138000',
|
||||
studentNo: 'S001',
|
||||
organizationId: hostOrg.id,
|
||||
}),
|
||||
);
|
||||
const oldRoom = await roomRepo.save(
|
||||
roomRepo.create({ roomNumber: '1-101', capacity: 4, status: 'available' }),
|
||||
);
|
||||
await bedRepo.save(
|
||||
Array.from({ length: 4 }, (_, index) =>
|
||||
bedRepo.create({ roomId: oldRoom.id, bedNumber: `${index + 1}号床` }),
|
||||
),
|
||||
);
|
||||
await occRepo.save(
|
||||
occRepo.create({
|
||||
studentId: existing.id,
|
||||
roomId: oldRoom.id,
|
||||
checkInDate: '2026-01-05',
|
||||
billingStartDate: '2026-01-05',
|
||||
stayType: 'short',
|
||||
responsibleOrganizationId: hostOrg.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '开学导入',
|
||||
summary: 'Excel 导入',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
{ key: 'organization', title: '机构' },
|
||||
],
|
||||
rows: [
|
||||
{ name: '张三', phone: '13900139000', organization: '东校区' },
|
||||
{ name: '老王', phone: '13800138000', organization: '恭学总校' },
|
||||
{ name: '李四', phone: '13700137000', organization: '不存在的机构' },
|
||||
],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'rooms',
|
||||
title: '宿舍',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'roomNumber', title: '房间号' }],
|
||||
rows: [{ roomNumber: '3-301' }, { roomNumber: '1-101' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'transfers',
|
||||
title: '换宿',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'studentNo', title: '学号' },
|
||||
{ key: 'oldRoom', title: '原宿舍' },
|
||||
{ key: 'newRoom', title: '目标宿舍' },
|
||||
{ key: 'transferDate', title: '换宿日期' },
|
||||
],
|
||||
rows: [
|
||||
{ studentNo: 'S001', oldRoom: '1-101', newRoom: '3-301', transferDate: '2026-03-01' },
|
||||
],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const { review: submittedReview, result } = await service.submitAll(review.id, 7);
|
||||
expect(result.students.created).toBe(2);
|
||||
expect(result.students.skipped).toBe(1);
|
||||
expect(result.rooms.created).toBe(1);
|
||||
expect(result.rooms.skipped).toBe(1);
|
||||
expect(result.transfers.completed).toBe(1);
|
||||
expect(result.transfers.skipped).toBe(0);
|
||||
|
||||
const createdStudent = await studentRepo.findOne({ where: { phone: '13900139000' } });
|
||||
expect(createdStudent?.name).toBe('张三');
|
||||
expect(createdStudent?.organizationId).toBe(namedOrg.id);
|
||||
const hostFallbackStudent = await studentRepo.findOne({ where: { phone: '13700137000' } });
|
||||
expect(hostFallbackStudent?.organizationId).toBe(hostOrg.id);
|
||||
|
||||
const newRoom = await roomRepo.findOne({ where: { roomNumber: '3-301' } });
|
||||
expect(newRoom?.capacity).toBe(4);
|
||||
expect(await bedRepo.count({ where: { roomId: newRoom!.id } })).toBe(4);
|
||||
|
||||
const oldOcc = await occRepo.findOne({
|
||||
where: { studentId: existing.id, roomId: oldRoom.id },
|
||||
});
|
||||
expect(oldOcc?.checkOutDate).toBe('2026-03-01');
|
||||
const newOcc = await occRepo.findOne({
|
||||
where: { studentId: existing.id, roomId: newRoom!.id, checkOutDate: null },
|
||||
});
|
||||
expect(newOcc?.checkInDate).toBe('2026-03-01');
|
||||
expect(newOcc?.billingStartDate).toBe('2026-03-02');
|
||||
|
||||
expect(submittedReview.status).toBe('submitted');
|
||||
expect(submittedReview.submittedAt).toBeInstanceOf(Date);
|
||||
const savedSections = service.parseSections(submittedReview.sectionsJson);
|
||||
const studentSection = savedSections.find((section) => section.key === 'students');
|
||||
expect(studentSection?.issues).toEqual(
|
||||
expect.arrayContaining(['学生「老王」已存在(按手机号/学号匹配),未重复创建']),
|
||||
);
|
||||
expect(submittedReview.resultSummary).toContain('成功导入学生 2 人');
|
||||
});
|
||||
|
||||
it('入住记录分表:学生和宿舍不存在时自动创建后写入住记录', async () => {
|
||||
const studentRepo = dataSource.getRepository(Student);
|
||||
const roomRepo = dataSource.getRepository(Room);
|
||||
const occRepo = dataSource.getRepository(Occupancy);
|
||||
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '入住导入',
|
||||
summary: '宿舍入住记录',
|
||||
sections: [
|
||||
{
|
||||
key: 'checkins',
|
||||
title: '入住记录',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
{ key: 'roomNumber', title: '宿舍号' },
|
||||
{ key: 'checkInDate', title: '入住日期' },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: '於嘉丽',
|
||||
phone: '13611112222',
|
||||
roomNumber: '5-501',
|
||||
checkInDate: '2026-08-01',
|
||||
},
|
||||
{
|
||||
name: '重复学生',
|
||||
phone: '13611112222',
|
||||
roomNumber: '5-502',
|
||||
checkInDate: '2026-08-01',
|
||||
},
|
||||
],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = await service.submitAll(review.id, 7);
|
||||
expect(result.checkins.completed).toBe(1);
|
||||
expect(result.checkins.skipped).toBe(1);
|
||||
|
||||
const created = await studentRepo.findOne({ where: { phone: '13611112222' } });
|
||||
expect(created?.name).toBe('於嘉丽');
|
||||
expect(created?.organizationId).toBe(hostOrg.id);
|
||||
const room = await roomRepo.findOne({ where: { roomNumber: '5-501' } });
|
||||
expect(room?.capacity).toBe(4);
|
||||
const occupancy = await occRepo.findOne({ where: { studentId: created!.id } });
|
||||
expect(occupancy?.checkInDate).toBe('2026-08-01');
|
||||
expect(occupancy?.roomId).toBe(room!.id);
|
||||
});
|
||||
|
||||
it('分步确认:依赖未满足拒绝,重复确认 409,全部完成后整卡提交', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '分步导入',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '分步学生', phone: '13511112222' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'rooms',
|
||||
title: '宿舍',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'roomNumber', title: '房间号' }],
|
||||
rows: [{ roomNumber: '9-901' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'transfers',
|
||||
title: '换宿',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'studentNo', title: '学号' }],
|
||||
rows: [{ studentNo: 'NOPE' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await expect(service.submitSection(review.id, 7, 'transfers')).rejects.toMatchObject({
|
||||
message: expect.stringContaining('请先确认第 1 步'),
|
||||
});
|
||||
|
||||
const studentsStep = await service.submitSection(review.id, 7, 'students');
|
||||
expect(studentsStep.result).toMatchObject({ created: 1, skipped: 0 });
|
||||
expect(
|
||||
service
|
||||
.parseSections(studentsStep.review.sectionsJson)
|
||||
.find((section) => section.key === 'students')?.status,
|
||||
).toBe('submitted');
|
||||
|
||||
await expect(service.submitSection(review.id, 7, 'students')).rejects.toMatchObject({
|
||||
message: expect.stringContaining('已确认导入'),
|
||||
});
|
||||
|
||||
await service.submitSection(review.id, 7, 'rooms');
|
||||
const transferStep = await service.submitSection(review.id, 7, 'transfers');
|
||||
expect(service.parseSections(transferStep.review.sectionsJson).map((s) => s.status)).toEqual([
|
||||
'submitted',
|
||||
'submitted',
|
||||
'submitted',
|
||||
]);
|
||||
expect(transferStep.review.status).toBe('submitted');
|
||||
expect(transferStep.review.submittedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('依赖按类型整组判断:同类型全部 sheet 提交后才允许换宿', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '多 sheet 依赖',
|
||||
sections: [
|
||||
{
|
||||
key: 'students_a',
|
||||
title: '学生 A',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '甲', phone: '13511112222' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'students_b',
|
||||
title: '学生 B',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '乙', phone: '13511113333' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'rooms_9',
|
||||
title: '9 号楼宿舍',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'roomNumber', title: '房间号' }],
|
||||
rows: [{ roomNumber: '9-901' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'checkins_active',
|
||||
type: 'checkins',
|
||||
title: '在住记录',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
{ key: 'roomNumber', title: '宿舍号' },
|
||||
{ key: 'checkInDate', title: '入住日期' },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: '丙',
|
||||
phone: '13511115555',
|
||||
roomNumber: '9-903',
|
||||
checkInDate: '2026-08-01',
|
||||
},
|
||||
],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'transfers_9',
|
||||
title: '换宿',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'studentPhone', title: '手机号' },
|
||||
{ key: 'newRoom', title: '目标宿舍' },
|
||||
{ key: 'transferDate', title: '换宿日期' },
|
||||
],
|
||||
rows: [{ studentPhone: '13511115555', newRoom: '9-901', transferDate: '2026-08-10' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await expect(service.submitSection(review.id, 7, 'transfers_9')).rejects.toMatchObject({
|
||||
message: expect.stringContaining('请先确认第 1 步'),
|
||||
});
|
||||
|
||||
await service.submitSection(review.id, 7, 'students_a');
|
||||
await expect(service.submitSection(review.id, 7, 'transfers_9')).rejects.toMatchObject({
|
||||
message: expect.stringContaining('请先确认第 2 步'),
|
||||
});
|
||||
|
||||
await service.submitSection(review.id, 7, 'students_b');
|
||||
await service.submitSection(review.id, 7, 'rooms_9');
|
||||
await service.submitSection(review.id, 7, 'checkins_active');
|
||||
const transferStep = await service.submitSection(review.id, 7, 'transfers_9');
|
||||
expect(transferStep.result).toMatchObject({ completed: 1, skipped: 0 });
|
||||
const statuses = service
|
||||
.parseSections(transferStep.review.sectionsJson)
|
||||
.map((section) => section.status);
|
||||
expect(statuses).toEqual(['submitted', 'submitted', 'submitted', 'submitted', 'submitted']);
|
||||
});
|
||||
|
||||
it('组确认按 sheet 逐张导入,成功后整组状态已导入', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '整组入住确认',
|
||||
sections: Array.from({ length: 2 }, (_, i) => ({
|
||||
key: `checkins_group_${i + 1}`,
|
||||
type: 'checkins',
|
||||
title: `入住表${i + 1}`,
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
{ key: 'roomNumber', title: '宿舍号' },
|
||||
{ key: 'checkInDate', title: '入住日期' },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: `入住学生${i + 1}`,
|
||||
phone: `1360000000${i + 1}`,
|
||||
roomNumber: `5-50${i + 1}`,
|
||||
checkInDate: '2026-08-01',
|
||||
},
|
||||
],
|
||||
issues: [],
|
||||
})),
|
||||
},
|
||||
);
|
||||
|
||||
const { review: grouped } = await service.submitGroup(review.id, 7, 'checkins');
|
||||
const sections = service.parseSections(grouped.sectionsJson);
|
||||
expect(sections.map((section) => section.status)).toEqual(['submitted', 'submitted']);
|
||||
expect(grouped.status).toBe('submitted');
|
||||
expect(
|
||||
await dataSource.getRepository(Student).count({
|
||||
where: { phone: '13600000001' },
|
||||
}),
|
||||
).toBe(1);
|
||||
expect(
|
||||
await dataSource.getRepository(Student).count({
|
||||
where: { phone: '13600000002' },
|
||||
}),
|
||||
).toBe(1);
|
||||
expect(await dataSource.getRepository(Room).count({ where: { roomNumber: '5-501' } })).toBe(1);
|
||||
expect(await dataSource.getRepository(Room).count({ where: { roomNumber: '5-502' } })).toBe(1);
|
||||
});
|
||||
|
||||
it('全部确认时按类型合并多张 sheet 的统计数量', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '多 sheet 聚合',
|
||||
sections: Array.from({ length: 2 }, (_, i) => ({
|
||||
key: `students_batch_${i + 1}`,
|
||||
type: 'students',
|
||||
title: `学生表${i + 1}`,
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: `批量学生${i + 1}`,
|
||||
phone: `1370000000${i + 1}`,
|
||||
},
|
||||
],
|
||||
issues: [],
|
||||
})),
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = await service.submitAll(review.id, 7);
|
||||
expect(result.students.created).toBe(2);
|
||||
expect(result.students.skipped).toBe(0);
|
||||
expect(result.message).toContain('成功导入学生 2 人');
|
||||
});
|
||||
|
||||
it('组确认依赖未满足时返回 409,不导入任何 sheet', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '组依赖校验',
|
||||
sections: [
|
||||
{
|
||||
key: 'students_a',
|
||||
title: '学生 A',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'name', title: '姓名' }],
|
||||
rows: [{ name: '甲' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'transfers_a',
|
||||
title: '换宿 A',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'studentPhone', title: '手机号' }],
|
||||
rows: [{ studentPhone: '13511114444' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
await expect(service.submitGroup(review.id, 7, 'transfers')).rejects.toMatchObject({
|
||||
message: expect.stringContaining('请先确认第 1 步'),
|
||||
});
|
||||
const sections = service.parseSections((await service.findOwned(review.id, 7)).sectionsJson);
|
||||
expect(sections.map((section) => section.status)).toEqual(['pending', 'pending']);
|
||||
});
|
||||
|
||||
it('单步确认部分成功时持久化 resultSummary 与问题', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '部分成功',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [
|
||||
{ name: '新学生', phone: '13522223333' },
|
||||
{ name: '重复学生', phone: '13522223333' },
|
||||
],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const step = await service.submitSection(review.id, 7, 'students');
|
||||
expect(step.result).toMatchObject({ created: 1, skipped: 1 });
|
||||
const saved = await service.findOwned(review.id, 7);
|
||||
const section = service.parseSections(saved.sectionsJson)[0];
|
||||
expect(section.status).toBe('submitted');
|
||||
expect(section.resultSummary).toContain('成功导入学生 1 人,跳过 1 条');
|
||||
expect(section.issues).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('同一批次中的其他学生')]),
|
||||
);
|
||||
expect(saved.status).toBe('submitted');
|
||||
});
|
||||
|
||||
it('全部确认按固定依赖顺序提交,不受 sections 原始顺序影响', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '乱序导入',
|
||||
sections: [
|
||||
{
|
||||
key: 'transfers',
|
||||
title: '换宿',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'studentNo', title: '学号' }],
|
||||
rows: [{ studentNo: 'NOPE' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '乱序学生', phone: '13544445555' }],
|
||||
issues: [],
|
||||
},
|
||||
{
|
||||
key: 'rooms',
|
||||
title: '宿舍',
|
||||
kind: 'table',
|
||||
columns: [{ key: 'roomNumber', title: '房间号' }],
|
||||
rows: [{ roomNumber: '9-902' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const { review: completed } = await service.submitAll(review.id, 7);
|
||||
expect(completed.status).toBe('submitted');
|
||||
expect(service.parseSections(completed.sectionsJson).map((section) => section.status)).toEqual([
|
||||
'submitted',
|
||||
'submitted',
|
||||
'submitted',
|
||||
]);
|
||||
});
|
||||
|
||||
it('旧数据缺少 section status 字段时默认 pending 并可继续确认', async () => {
|
||||
const review = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: '旧数据兼容',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '旧数据学生', phone: '13533334444' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const legacySections = service
|
||||
.parseSections(review.sectionsJson)
|
||||
.map(
|
||||
({ status: _status, resultSummary: _result, submittedAt: _at, type: _type, ...rest }) =>
|
||||
rest,
|
||||
);
|
||||
review.sectionsJson = JSON.stringify(legacySections);
|
||||
await dataSource.getRepository(AiReview).save(review);
|
||||
|
||||
const step = await service.submitSection(review.id, 7, 'students');
|
||||
expect(step.result).toMatchObject({ created: 1, skipped: 0 });
|
||||
const reloaded = service.parseSections((await service.findOwned(review.id, 7)).sectionsJson)[0];
|
||||
expect(reloaded.status).toBe('submitted');
|
||||
});
|
||||
|
||||
it('同会话生成新预览后旧预览过期,且所有确认入口拒绝', async () => {
|
||||
const conversationId = 9001;
|
||||
const first = await service.createReview(
|
||||
{ ...baseArgs, conversationId, assistantMessageId },
|
||||
{
|
||||
title: '旧预览',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '旧学生', phone: '13711110001' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const second = await service.createReview(
|
||||
{ ...baseArgs, conversationId, assistantMessageId },
|
||||
{
|
||||
title: '新预览',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '新学生', phone: '13711110002' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const expired = await service.expirePreviousReviews(7, conversationId, second.id);
|
||||
expect(expired.map((review) => review.id)).toEqual([first.id]);
|
||||
expect((await service.findOwned(first.id, 7)).status).toBe('expired');
|
||||
expect((await service.findOwned(second.id, 7)).status).toBe('pending');
|
||||
|
||||
await expect(service.findOwnedPending(first.id, 7)).rejects.toThrow('已失效');
|
||||
await expect(service.submitSection(first.id, 7, 'students')).rejects.toMatchObject({
|
||||
message: expect.stringContaining('已失效'),
|
||||
});
|
||||
await expect(service.submitGroup(first.id, 7, 'students')).rejects.toMatchObject({
|
||||
message: expect.stringContaining('已失效'),
|
||||
});
|
||||
await expect(service.submitAll(first.id, 7)).rejects.toMatchObject({
|
||||
message: expect.stringContaining('已失效'),
|
||||
});
|
||||
});
|
||||
|
||||
it('不同会话的旧预览不会被其他会话的新预览过期', async () => {
|
||||
const first = await service.createReview(
|
||||
{ ...baseArgs, assistantMessageId },
|
||||
{
|
||||
title: 'A 会话预览',
|
||||
sections: [
|
||||
{
|
||||
key: 'students',
|
||||
title: '学生',
|
||||
kind: 'table',
|
||||
columns: [
|
||||
{ key: 'name', title: '姓名' },
|
||||
{ key: 'phone', title: '手机号' },
|
||||
],
|
||||
rows: [{ name: '跨会话学生', phone: '13711110003' }],
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
await service.expirePreviousReviews(7, 999, 'other-review');
|
||||
expect((await service.findOwned(first.id, 7)).status).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user