refactor: resolve remaining field audit issues

This commit is contained in:
2026-07-13 15:12:36 +08:00
parent 0533c30ece
commit aa1ed7db56
34 changed files with 953 additions and 263 deletions

View File

@@ -0,0 +1,36 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DatabaseMigrationsService } from './database-migrations.service';
describe('DatabaseMigrationsService — classroom status normalization', () => {
it('normalizes legacy persisted statuses to available', async () => {
const runner = {
connect: jest.fn(),
release: jest.fn(),
getTables: jest.fn().mockResolvedValue([{ name: 'classrooms' }]),
query: jest.fn().mockResolvedValue(undefined),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
DatabaseMigrationsService,
{
provide: getDataSourceToken(),
useValue: {
options: { type: 'better-sqlite3' },
createQueryRunner: jest.fn().mockReturnValue(runner),
},
},
],
}).compile();
const service = module.get(DatabaseMigrationsService) as DatabaseMigrationsService & {
normalizeClassroomStatuses(): Promise<void>;
};
await service.normalizeClassroomStatuses();
expect(runner.query).toHaveBeenCalledWith(
expect.stringContaining("status NOT IN ('available', 'maintenance', 'archived')"),
);
expect(runner.release).toHaveBeenCalled();
});
});

View File

@@ -17,6 +17,7 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
await this.removeUnusedClassroomColumns();
await this.cleanupDepositRefundColumns();
await this.removeUnusedClassStudentColumns();
await this.normalizeClassroomStatuses();
}
private async removeUnusedClassroomColumns(): Promise<void> {
@@ -92,6 +93,22 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
}
}
private async normalizeClassroomStatuses(): Promise<void> {
const runner = this.dataSource.createQueryRunner();
await runner.connect();
try {
const tables = await runner.getTables(['classrooms']);
if (tables.length === 0) return;
await runner.query(`
UPDATE classrooms
SET status = 'available'
WHERE status IS NULL OR status NOT IN ('available', 'maintenance', 'archived')
`);
} finally {
await runner.release();
}
}
private async ensureAiConfigTable(): Promise<void> {
const runner = this.dataSource.createQueryRunner();
await runner.connect();
@@ -410,14 +427,17 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
// Drop any existing FK constraint on schedule_id or class_id
const fkColumns = ['schedule_id', 'class_id'];
for (const col of fkColumns) {
const fkRows: { CONSTRAINT_NAME: string }[] = await runner.query(`
const fkRows: { CONSTRAINT_NAME: string }[] = await runner.query(
`
SELECT CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'attendance_sessions'
AND COLUMN_NAME = ?
AND REFERENCED_TABLE_NAME IS NOT NULL
`, [col]);
`,
[col],
);
for (const row of fkRows) {
try {
@@ -437,13 +457,16 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
];
for (const c of constraints) {
// Only skip if RESTRICT constraint is already confirmed via information_schema
const existing: Array<{ DELETE_RULE: string }> = await runner.query(`
const existing: Array<{ DELETE_RULE: string }> = await runner.query(
`
SELECT DELETE_RULE
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'attendance_sessions'
AND CONSTRAINT_NAME = ?
`, [c.name]);
`,
[c.name],
);
if (existing.length > 0 && existing[0].DELETE_RULE === 'RESTRICT') {
this.logger.log(`考勤场次删除保护约束已存在: ${c.name}`);
@@ -505,17 +528,13 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
FROM attendance_sessions
`);
await runner.query('DROP TABLE attendance_sessions');
await runner.query(
'ALTER TABLE attendance_sessions_new RENAME TO attendance_sessions',
);
await runner.query('ALTER TABLE attendance_sessions_new RENAME TO attendance_sessions');
await runner.query(
'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_schedule_date ON attendance_sessions(schedule_id, lesson_date)',
);
// Rebuild attendance_records to add/protect FK on attendance_session_id
const recordsFk = await runner.query(
"PRAGMA foreign_key_list('attendance_records')",
);
const recordsFk = await runner.query("PRAGMA foreign_key_list('attendance_records')");
const hasSessionFk = recordsFk.some(
(r: { from: string }) => r.from === 'attendance_session_id',
);
@@ -548,9 +567,7 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
FROM attendance_records
`);
await runner.query('DROP TABLE attendance_records');
await runner.query(
'ALTER TABLE attendance_records_new RENAME TO attendance_records',
);
await runner.query('ALTER TABLE attendance_records_new RENAME TO attendance_records');
await runner.query(
'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_student ON attendance_records(attendance_session_id, student_id)',
);
@@ -560,9 +577,7 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
// If violations exist, the transaction rolls back and old tables are preserved.
const checkRows = await runner.query('PRAGMA foreign_key_check');
if (checkRows.length > 0) {
throw new Error(
`外键一致性检查失败: ${checkRows.length} 行违反外键约束`,
);
throw new Error(`外键一致性检查失败: ${checkRows.length} 行违反外键约束`);
}
await runner.query('COMMIT');