forked from wangziqi/gongxue-base
refactor: resolve remaining field audit issues
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user