forked from wangziqi/gongxue-base
refactor: remove unused classroom fields
This commit is contained in:
@@ -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,
|
||||
|
||||
7
apps/server/src/classrooms/classroom-template.spec.ts
Normal file
7
apps/server/src/classrooms/classroom-template.spec.ts
Normal 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(['教室名', '楼栋', '楼层', '类型', '容量']);
|
||||
});
|
||||
});
|
||||
9
apps/server/src/classrooms/classroom-template.ts
Normal file
9
apps/server/src/classrooms/classroom-template.ts
Normal 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);
|
||||
@@ -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);
|
||||
|
||||
@@ -130,7 +130,6 @@ export class ClassroomsService {
|
||||
floor?: number;
|
||||
capacity?: number;
|
||||
roomType?: string;
|
||||
courseType?: string;
|
||||
}[],
|
||||
) {
|
||||
let imported = 0;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user