Files
gongxue-base/docs/superpowers/plans/2026-07-13-classroom-field-simplification.md

226 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Classroom Field Simplification Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove the unused classroom `courseType` and `supervisor` fields from the admin UI, API contract, persistence model, and Excel import flow.
**Architecture:** Perform a clean cutover across every classroom caller. Add an idempotent bootstrap migration that drops the legacy columns on SQLite and MySQL, while leaving scheduling unchanged because schedule `subject` and class `classType` already own those concepts.
**Tech Stack:** React 19, Ant Design 6, NestJS 11, TypeORM 0.3, Jest, Vitest, SQLite, MySQL 8
## Global Constraints
- Do not add a replacement abstraction or compatibility alias.
- Preserve classroom name, building, floor, capacity, room type, status, and notes behavior.
- Do not change schedule form fields or schedule conflict behavior.
- Use test-first changes and run focused tests before package builds.
---
### Task 1: Drop Legacy Classroom Columns
**Files:**
- Modify: `apps/server/src/database/database-migrations.spec.ts`
- Modify: `apps/server/src/database/database-migrations.service.ts:11`
- Modify: `apps/server/src/entities/classroom.entity.ts:23`
**Interfaces:**
- Consumes: TypeORM `DataSource`, `QueryRunner`, and the existing bootstrap migration sequence.
- Produces: private `removeUnusedClassroomColumns(): Promise<void>` called during application bootstrap.
- [ ] **Step 1: Write failing migration tests**
Extend `MigrationsPrivate` with `removeUnusedClassroomColumns(): Promise<void>`. Add focused tests proving that the method:
```typescript
it('drops legacy classroom fields when present', async () => {
const runner = mockRunner({
getTable: {
name: 'classrooms',
columns: [{ name: 'id' }, { name: 'course_type' }, { name: 'supervisor' }],
},
});
await bootstrapClassroomCleanup(runner);
await service.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 classrooms or legacy fields are absent', async () => {
const runner = mockRunner({ getTables: [], getTable: { name: 'classrooms', columns: [] } });
await bootstrapClassroomCleanup(runner);
await service.removeUnusedClassroomColumns();
expect(runner.query).not.toHaveBeenCalled();
expect(runner.release).toHaveBeenCalled();
});
```
Use a local `bootstrapClassroomCleanup` helper matching the existing Nest testing-module setup.
- [ ] **Step 2: Run tests and verify RED**
Run: `npm test -w apps/server -- database/database-migrations.spec.ts --runInBand`
Expected: FAIL because `removeUnusedClassroomColumns` does not exist.
- [ ] **Step 3: Implement the idempotent migration**
Call `await this.removeUnusedClassroomColumns();` from `onApplicationBootstrap()`. Implement the method with one query runner: inspect `classrooms`, return when absent, and issue one `ALTER TABLE classrooms DROP COLUMN <name>` per present legacy column. Always release the runner in `finally`.
Remove these entity properties and decorators:
```typescript
courseType: string;
supervisor: string;
```
- [ ] **Step 4: Run migration tests and verify GREEN**
Run: `npm test -w apps/server -- database/database-migrations.spec.ts --runInBand`
Expected: PASS.
### Task 2: Remove Fields From API and Excel Import
**Files:**
- Modify: `apps/server/src/classrooms/dto/classroom.dto.ts:3`
- Modify: `apps/server/src/classrooms/classrooms.controller.ts:48`
- Modify: `apps/server/src/classrooms/classrooms.service.ts:126`
- Create: `apps/server/src/classrooms/classrooms.controller.spec.ts`
**Interfaces:**
- Consumes: `CreateClassroomDto`, `UpdateClassroomDto`, classroom Excel template/import endpoints.
- Produces: classroom requests and spreadsheets containing only `name`, `building`, `floor`, `roomType`, `capacity`, and optional `notes` where applicable.
- [ ] **Step 1: Write a failing Excel contract test**
Instantiate `ClassroomsController` with mocked service/log dependencies, invoke `downloadTemplate`, capture the workbook response buffer, load it with ExcelJS, and assert:
```typescript
expect(worksheet.getRow(1).values).toEqual([
undefined,
'教室名',
'楼栋',
'楼层',
'类型',
'容量',
]);
```
Also assert the usage notes no longer mention course type or supervisor.
- [ ] **Step 2: Run test and verify RED**
Run: `npm test -w apps/server -- classrooms/classrooms.controller.spec.ts --runInBand`
Expected: FAIL because the generated template still includes `课程类型` and `负责人`.
- [ ] **Step 3: Remove fields from server contracts**
Delete `courseType` and `supervisor` from both DTO classes. Remove them from:
- template columns, example rows, and usage notes;
- parsed import rows;
- `batchImport` row type.
Keep the existing field order `name`, `building`, `floor`, `roomType`, `capacity` consistent between export and import.
- [ ] **Step 4: Run focused server tests**
Run: `npm test -w apps/server -- classrooms/classrooms.controller.spec.ts database/database-migrations.spec.ts --runInBand`
Expected: PASS.
### Task 3: Simplify Classroom Admin UI
**Files:**
- Modify: `apps/admin/src/pages/Classrooms/index.tsx:143`
- Create: `apps/admin/src/pages/Classrooms/classroom-fields.ts`
- Create: `apps/admin/src/pages/Classrooms/classroom-fields.integration.test.ts`
**Interfaces:**
- Consumes: classroom records returned by `/classrooms`.
- Produces: exported `CLASSROOM_VISIBLE_FIELDS` used to document/test the UI contract; list and form without `courseType` or `supervisor`.
- [ ] **Step 1: Write a failing UI contract test**
Create a small contract test:
```typescript
import { describe, expect, it } from 'vitest';
import { CLASSROOM_VISIBLE_FIELDS } from './classroom-fields';
describe('classroom visible fields', () => {
it('excludes obsolete course and supervisor metadata', () => {
expect(CLASSROOM_VISIBLE_FIELDS).toEqual([
'name',
'building',
'floor',
'roomType',
'capacity',
'status',
'notes',
]);
});
});
```
- [ ] **Step 2: Run test and verify RED**
Run: `npm test -w apps/admin -- src/pages/Classrooms/classroom-fields.integration.test.ts`
Expected: FAIL because `classroom-fields.ts` does not exist.
- [ ] **Step 3: Implement minimal UI cleanup**
Create the constant exactly as asserted. Remove the `课程类型` and `负责人` table columns and the corresponding two `Form.Item` blocks. Do not alter scheduling pages.
- [ ] **Step 4: Run focused admin test**
Run: `npm test -w apps/admin -- src/pages/Classrooms/classroom-fields.integration.test.ts`
Expected: PASS.
### Task 4: Verify Clean Cutover
**Files:**
- Verify: `apps/server/src/entities/classroom.entity.ts`
- Verify: `apps/server/src/classrooms/`
- Verify: `apps/admin/src/pages/Classrooms/`
**Interfaces:**
- Consumes: completed Tasks 13.
- Produces: buildable admin/server packages with no classroom `courseType` or `supervisor` path.
- [ ] **Step 1: Run focused tests together**
Run: `npm test -w apps/server -- classrooms/classrooms.controller.spec.ts database/database-migrations.spec.ts --runInBand`
Run: `npm test -w apps/admin -- src/pages/Classrooms/classroom-fields.integration.test.ts`
Expected: all PASS.
- [ ] **Step 2: Run package typechecks/builds**
Run: `npm run typecheck -w apps/server`
Run: `npm run build -w apps/admin`
Expected: both exit successfully.
- [ ] **Step 3: Smoke-test observable contracts**
Start the existing app and verify:
1. Adding/editing a classroom shows no course type or supervisor field.
2. Classroom list shows neither obsolete column.
3. Downloaded import template has five headers: 教室名、楼栋、楼层、类型、容量.
4. Creating a schedule still selects and saves a classroom normally.
Expected: all four scenarios succeed without console or API errors.