refactor: remove unused classroom fields

This commit is contained in:
2026-07-13 09:34:15 +08:00
parent a266d450e1
commit b9b295c997
14 changed files with 374 additions and 53 deletions

View File

@@ -546,7 +546,6 @@ export class ClassroomRentalsService {
floor: c.floor,
roomType: c.roomType,
capacity: c.capacity,
supervisor: c.supervisor,
})),
organizations: Array.from(organizationMap.values()),
matrix,

View File

@@ -0,0 +1,7 @@
import { CLASSROOM_TEMPLATE_HEADERS } from './classroom-template';
describe('classroom import template', () => {
it('contains only classroom fields used by the product', () => {
expect(CLASSROOM_TEMPLATE_HEADERS).toEqual(['教室名', '楼栋', '楼层', '类型', '容量']);
});
});

View File

@@ -0,0 +1,9 @@
export const CLASSROOM_TEMPLATE_COLUMNS = [
{ header: '教室名', key: 'name', width: 15 },
{ header: '楼栋', key: 'building', width: 12 },
{ header: '楼层', key: 'floor', width: 8 },
{ header: '类型', key: 'roomType', width: 10 },
{ header: '容量', key: 'capacity', width: 10 },
];
export const CLASSROOM_TEMPLATE_HEADERS = CLASSROOM_TEMPLATE_COLUMNS.map(({ header }) => header);

View File

@@ -22,6 +22,7 @@ import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { extractRequestInfo } from '../common/request-utils';
import { RequirePermission } from '../auth/decorators/permission.decorator';
import * as ExcelJS from 'exceljs';
import { CLASSROOM_TEMPLATE_COLUMNS } from './classroom-template';
@UseGuards(JwtAuthGuard)
@Controller('classrooms')
@@ -50,15 +51,7 @@ export class ClassroomsController {
async downloadTemplate(@Res() res: Response) {
const workbook = new ExcelJS.Workbook();
const ws = workbook.addWorksheet('教室导入模板');
ws.columns = [
{ header: '教室名', key: 'name', width: 15 },
{ header: '楼栋', key: 'building', width: 12 },
{ header: '楼层', key: 'floor', width: 8 },
{ header: '类型', key: 'roomType', width: 10 },
{ header: '容量', key: 'capacity', width: 10 },
{ header: '课程类型', key: 'courseType', width: 16 },
{ header: '负责人', key: 'supervisor', width: 12 },
];
ws.columns = CLASSROOM_TEMPLATE_COLUMNS;
ws.getRow(1).font = { bold: true };
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
ws.addRow({
@@ -67,8 +60,6 @@ export class ClassroomsController {
floor: 2,
roomType: '大',
capacity: 60,
courseType: '尊享培优班',
supervisor: '张老师',
});
ws.addRow({
name: 'B301',
@@ -76,8 +67,6 @@ export class ClassroomsController {
floor: 3,
roomType: '次大',
capacity: 40,
courseType: '专业课集训班',
supervisor: '李老师',
});
ws.addRow({
name: 'B405',
@@ -85,8 +74,6 @@ export class ClassroomsController {
floor: 4,
roomType: '小',
capacity: 20,
courseType: '',
supervisor: '',
});
// 说明sheet
@@ -97,8 +84,6 @@ export class ClassroomsController {
'1. 教室名必填,建议采用「楼栋+房号」如 A201、B301',
'2. 类型可填 大 / 次大 / 小,为空默认「大」',
'3. 同名教室会自动跳过(不覆盖)',
'4. 课程类型可填尊享培优班、专业课集训班等产品班级',
'5. 负责人为班主任/对接人',
].forEach((note) => ws2.addRow({ note }));
res.setHeader(
@@ -207,8 +192,6 @@ export class ClassroomsController {
floor: Number(row.getCell(3).value) || undefined,
roomType: String(row.getCell(4).value || '') || undefined,
capacity: Number(row.getCell(5).value) || undefined,
courseType: String(row.getCell(6).value || '') || undefined,
supervisor: String(row.getCell(7).value || '') || undefined,
});
});
const result = await this.service.batchImport(rows);

View File

@@ -130,7 +130,6 @@ export class ClassroomsService {
floor?: number;
capacity?: number;
roomType?: string;
courseType?: string;
}[],
) {
let imported = 0;

View File

@@ -21,13 +21,6 @@ export class CreateClassroomDto {
@IsString()
roomType?: string; // 大 / 次大 / 小
@IsOptional()
@IsString()
courseType?: string;
@IsOptional()
@IsString()
supervisor?: string;
@IsOptional()
@IsString()
@@ -56,13 +49,6 @@ export class UpdateClassroomDto {
@IsString()
roomType?: string;
@IsOptional()
@IsString()
courseType?: string;
@IsOptional()
@IsString()
supervisor?: string;
@IsOptional()
@IsString()

View File

@@ -14,6 +14,26 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
await this.backfillOrganizations();
await this.normalizeClassDates();
await this.protectAttendanceHistory();
await this.removeUnusedClassroomColumns();
}
private async removeUnusedClassroomColumns(): Promise<void> {
const runner = this.dataSource.createQueryRunner();
await runner.connect();
try {
const tables = await runner.getTables(['classrooms']);
if (tables.length === 0) return;
const table = await runner.getTable('classrooms');
const columnNames = new Set(table?.columns.map((column) => column.name) ?? []);
for (const columnName of ['course_type', 'supervisor']) {
if (columnNames.has(columnName)) {
await runner.query(`ALTER TABLE classrooms DROP COLUMN ${columnName}`);
}
}
} finally {
await runner.release();
}
}
private async ensureAiConfigTable(): Promise<void> {

View File

@@ -10,6 +10,13 @@ interface MockTable {
name: string;
columns: MockColumn[];
}
interface MockRunner {
release: jest.Mock;
connect: jest.Mock;
query: jest.Mock;
getTables: jest.Mock;
getTable: jest.Mock;
}
function mockRunner(overrides: {
getTables?: MockTable[];
@@ -28,10 +35,10 @@ function mockRunner(overrides: {
query.mockRejectedValue(overrides.queryError);
}
return { release, connect, query, getTables, getTable };
return { release, connect, query, getTables, getTable } satisfies MockRunner;
}
function createDataSource(runner: ReturnType<typeof mockRunner>, dbType: string = 'better-sqlite3') {
function createDataSource(runner: MockRunner, dbType: string = 'better-sqlite3') {
return {
options: { type: dbType },
createQueryRunner: jest.fn().mockReturnValue(runner),
@@ -46,12 +53,13 @@ interface MigrationsPrivate {
normalizeClassDates(): Promise<void>;
ensureCourseAttendanceSchema(): Promise<void>;
protectAttendanceHistory(): Promise<void>;
removeUnusedClassroomColumns(): Promise<void>;
}
describe('DatabaseMigrationsService — ensureAiConfigTable', () => {
let service: MigrationsPrivate & DatabaseMigrationsService;
async function bootstrap(runner: ReturnType<typeof mockRunner>) {
async function bootstrap(runner: MockRunner) {
const dataSource = createDataSource(runner);
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -220,7 +228,7 @@ describe('DatabaseMigrationsService — course attendance schema', () => {
describe('DatabaseMigrationsService — protectAttendanceHistory', () => {
let service: MigrationsPrivate & DatabaseMigrationsService;
async function bootstrap(runner: ReturnType<typeof mockRunner>, dbType: string = 'better-sqlite3') {
async function bootstrap(runner: MockRunner, dbType: string = 'better-sqlite3') {
const dataSource = createDataSource(runner, dbType);
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -416,7 +424,49 @@ describe('DatabaseMigrationsService — protectAttendanceHistory', () => {
});
});
async function bootstrapCourseAttendance(runner: ReturnType<typeof mockRunner>) {
describe('DatabaseMigrationsService — classroom cleanup', () => {
let cleanupService: MigrationsPrivate & DatabaseMigrationsService;
async function bootstrapClassroomCleanup(runner: MockRunner) {
const dataSource = createDataSource(runner);
const module: TestingModule = await Test.createTestingModule({
providers: [
DatabaseMigrationsService,
{ provide: getDataSourceToken(), useValue: dataSource },
],
}).compile();
cleanupService = module.get(DatabaseMigrationsService);
}
it('drops legacy classroom fields when present', async () => {
const runner = mockRunner({
getTables: [{ name: 'classrooms', columns: [] }],
getTable: {
name: 'classrooms',
columns: [{ name: 'id' }, { name: 'course_type' }, { name: 'supervisor' }],
},
});
await bootstrapClassroomCleanup(runner);
await cleanupService.removeUnusedClassroomColumns();
expect(runner.query).toHaveBeenCalledWith('ALTER TABLE classrooms DROP COLUMN course_type');
expect(runner.query).toHaveBeenCalledWith('ALTER TABLE classrooms DROP COLUMN supervisor');
expect(runner.release).toHaveBeenCalled();
});
it('does nothing when the classrooms table is absent', async () => {
const runner = mockRunner({ getTables: [] });
await bootstrapClassroomCleanup(runner);
await cleanupService.removeUnusedClassroomColumns();
expect(runner.query).not.toHaveBeenCalled();
expect(runner.release).toHaveBeenCalled();
});
});
async function bootstrapCourseAttendance(runner: MockRunner) {
const dataSource = createDataSource(runner);
const module: TestingModule = await Test.createTestingModule({
providers: [

View File

@@ -20,11 +20,6 @@ export class Classroom {
@Column({ name: 'room_type', type: 'varchar', length: 20, default: '大' })
roomType: string; // 大 / 次大 / 小
@Column({ name: 'course_type', length: 50, nullable: true })
courseType: string;
@Column({ length: 50, nullable: true })
supervisor: string;
@Column({ type: 'varchar', length: 20, default: 'reserved' })
status: string;