scheduling 排课模块
课表自动排课(贪心算法 + 约束规则)、人工调课申请与审批、冲突检测(教师/教室/班级时间重叠) 代码规模: 14 文件 · 109 符号 · 2,785 行(arch.db) 模块路径:
src/modules/scheduling/
架构图
graph TB
subgraph Client["客户端组件"]
direction TB
RulesForm["scheduling-rules-form<br/>规则配置 (236 行)"]
AutoPanel["auto-schedule-panel<br/>自动排课 (121 行)"]
AutoResult["auto-schedule-result<br/>排课结果 (95 行)"]
GridView["schedule-grid-view<br/>课表网格 (177 行)"]
ChgForm["schedule-change-form<br/>调课申请 (209 行)"]
ChgList["schedule-change-list<br/>调课列表 (220 行)"]
Conflict["schedule-conflicts-view<br/>冲突视图 (138 行)"]
end
subgraph Server["服务端"]
Actions["actions.ts<br/>(276 行, Server Actions)"]
Scheduler["auto-scheduler.ts<br/>(312 行, 贪心排课算法)"]
end
subgraph DA["Data Access"]
DA["data-access.ts<br/>(488 行, 规则/调课/冲突)"]
DACls["data-access-class-schedule.ts<br/>(127 行, classSchedule 写入口)"]
end
subgraph Lib["纯函数库"]
Valid["lib/schedule-validation.ts<br/>(159 行, 字段校验纯函数)"]
Schema["schema.ts<br/>(82 行, Zod)"]
end
subgraph DB["数据库表"]
Rules[(scheduling_rules)]
Changes[(schedule_changes)]
ClsSch[(class_schedule)]
end
RulesForm --> Actions
AutoPanel --> Actions
AutoResult --> AutoPanel
GridView --> Actions
ChgForm --> Actions
ChgList --> Actions
Conflict --> Actions
Actions --> DA
Actions --> Scheduler
Scheduler --> Valid
DA --> DACls
DA --> Rules
DA --> Changes
DACls --> ClsSch
核心流程图
流程 1: 自动排课(贪心算法 + 约束)
sequenceDiagram
participant T as 教师/管理员
participant Panel as auto-schedule-panel
participant Act as actions.ts
participant Sch as auto-scheduler.ts
participant DA as data-access.ts
participant DB as MySQL
T->>Panel: 选择班级 + 点击"自动排课"
Panel->>Act: autoScheduleAction (classId)
Act->>Act: requirePermission(SCHEDULE_AUTO)
Act->>DA: getSchedulingRules(classId)
DA-->>Act: rules(回退全局规则)
Act->>DA: getClassSubjectsForScheduling(classId)
DA-->>Act: 科目 + 教师分配
Act->>DA: getClassroomsForScheduling
DA-->>Act: 候选教室
Act->>Sch: autoSchedule(params, rules)
Sch->>Sch: buildDefaultTimeSlots(按规则生成时段)
loop 每个科目课时
Sch->>Sch: findOptimalSlot(贪心:首个满足约束的时段)
Note over Sch: 约束: 午休/每日上限/避免连堂/学科均衡
end
Sch-->>Act: AutoScheduleResult {schedule, conflicts}
Act->>DA: replaceClassSchedule(classId, schedule)
DA->>DB: DELETE 旧 + INSERT 新 class_schedule
Act->>Act: invalidateFor("scheduling.auto")
Act-->>Panel: ActionState<AutoScheduleResult>
流程 2: 人工调课申请 + 审批
sequenceDiagram
participant T as 教师
participant Form as schedule-change-form
participant Act as actions.ts
participant DA as data-access.ts
participant Admin as 管理员
participant List as schedule-change-list
participant DB as MySQL
T->>Form: 填写调课申请(代课/改时间)
Form->>Act: createScheduleChangeAction (FormData)
Act->>Act: requirePermission(SCHEDULE_ADJUST)
Act->>Act: ScheduleChangeSchema.safeParse
Act->>DA: createScheduleChange(input)
DA->>DB: INSERT schedule_changes (status=pending)
Act->>Act: invalidateFor("scheduling.change.create")
Act-->>Form: ActionState
Admin->>List: 查看待审批调课
List->>Act: getScheduleChangesAction
Act->>DA: getScheduleChanges(query)
DA-->>Act: ScheduleChangeListItem[]
Act-->>List: 列表
Admin->>List: 审批(通过/拒绝)
List->>Act: updateScheduleChangeStatusAction(id, status)
Act->>Act: requirePermission(SCHEDULE_ADJUST)
Act->>DA: updateScheduleChangeStatus(id, approved/rejected)
DA->>DB: UPDATE schedule_changes SET status, approvedBy
Act-->>List: ActionState
流程 3: 冲突检测(教师/教室/班级时间重叠)
flowchart TB
A[写入课表前] --> B[data-access.ts<br/>getClassConflicts]
B --> C[lib/schedule-validation.ts<br/>isOverlap 时间重叠]
C --> D{检测维度}
D --> E[教师冲突<br/>同教师同时段多班]
D --> F[教室冲突<br/>同教室同时段多课]
D --> G[班级冲突<br/>同班级同时段多课]
E --> H{有冲突?}
F --> H
G --> H
H -- 是 --> I[返回 ScheduleConflict[]<br/>阻止写入]
H -- 否 --> J[允许写入 class_schedule]
流程 4: 排课规则配置
flowchart LR
A[管理员配置规则] --> B[scheduling-rules-form]
B --> C[saveSchedulingRulesAction]
C --> D[requirePermission<br/>SCHEDULE_ADJUST]
D --> E[SchedulingRuleSchema.safeParse]
E --> F[upsertSchedulingRules]
F --> G[INSERT/UPDATE scheduling_rules]
G --> H[invalidateFor]
I[规则字段] --> J[每日最大课时<br/>maxDailyHours]
I --> K[连堂上限<br/>maxContinuousHours]
I --> L[午休时段<br/>lunchBreakStart/End]
I --> M[每日窗口<br/>morningStart/afternoonEnd]
I --> N[避免连堂<br/>avoidBackToBack]
I --> O[学科均衡<br/>balancedSubjects]
技术栈
| 类别 | 技术 | 用途 |
|---|---|---|
| 数据库 | Drizzle ORM | scheduling_rules / schedule_changes / class_schedule 表 CRUD |
| 缓存 | cacheFn | data-access 函数请求级去重 |
| 排课算法 | 自研贪心算法 | auto-scheduler.ts findOptimalSlot 满足约束的首个时段 |
| 校验 | 纯函数 schedule-validation | HH:MM 格式 / weekday 范围 / 时间区间校验 |
| 表单 | Zod schema | SchedulingRule / ScheduleChange / AutoScheduleParams 验证 |
| Server Action | requirePermission | SCHEDULE_ADJUST / SCHEDULE_AUTO 双权限点 |
| 缓存失效 | invalidateFor | 按 scheduling.{saveRules/auto/change.*} 精确失效 |
| 跨模块资源 | 对方 data-access 调用 | classes 校验班级归属 / school 获取教室科目 / users 教师姓名 |
文件结构
src/modules/scheduling/
├─ actions.ts # Server Actions (276 行): 规则/排课/调课/冲突
├─ data-access.ts # 数据访问层 (488 行): 规则/调课/冲突检测
├─ data-access-class-schedule.ts # classSchedule 写入口 (127 行): replaceClassSchedule
├─ auto-scheduler.ts # 贪心排课算法 (312 行): findOptimalSlot/autoSchedule
├─ schema.ts # Zod 验证 (82 行)
├─ types.ts # 类型定义 (145 行): Rule/Change/Conflict/TimeSlot
├─ lib/
│ └─ schedule-validation.ts # 纯校验函数 (159 行): isTimeHHMM/isOverlap/normalize
└─ components/ # 7 个客户端组件
├─ schedule-change-list.tsx # 调课列表 (220 行)
├─ scheduling-rules-form.tsx # 规则配置 (236 行)
├─ schedule-change-form.tsx # 调课申请 (209 行)
├─ schedule-grid-view.tsx # 课表网格 (177 行)
├─ schedule-conflicts-view.tsx # 冲突视图 (138 行)
├─ auto-schedule-panel.tsx # 自动排课面板 (121 行)
└─ auto-schedule-result.tsx # 排课结果 (95 行)
依赖关系
依赖(本模块 → 他模块)
| 模块 | 依赖类型 | 用途 |
|---|---|---|
| classes | data-access-call | 校验班级归属 / 获取班级学生 |
| school | data-access-call + import | 获取教室 getClassroomsForScheduling / 科目 |
| users | data-access-call + import | getUserNamesByIds 教师/代课教师姓名 |
| shared | import | DB / cache / auth-guard |
被依赖(他模块 → 本模块)
| 模块 | 依赖类型 | 用途 |
|---|---|---|
| (无) | - | 仅 app 路由层直接调用,无业务模块反向依赖 |
关键约束
- classSchedule 写入口唯一化:
replaceClassSchedule是 class_schedule 表唯一写入口,其他模块不得直接写入,避免双写导致归属与冲突校验缺失 - 排课算法独立成 auto-scheduler.ts: 贪心算法与数据访问分离,可单独测试与替换为其他算法
- 冲突检测在 data-access 层: 保证写入口与冲突校验在同一事务边界,
getClassConflicts在写入前必检 - 校验纯函数抽离:
lib/schedule-validation.ts为纯函数无 DB 访问(A-02 修复),便于单测 - 规则回退机制: 班级无规则时回退到全局规则(classId=null),保证排课可执行
- 双权限点: SCHEDULE_AUTO(自动排课)/SCHEDULE_ADJUST(规则配置+调课)分离
架构决策
| 决策 | 原因 |
|---|---|
| classSchedule 写入口唯一化 | 避免 classes 模块双写导致归属与冲突校验缺失 |
| 排课算法独立 auto-scheduler.ts | 算法逻辑可单独测试与替换,与数据访问解耦 |
| 冲突检测在 data-access 层 | 保证写入口与校验在同一事务边界,防御性编程 |
| 贪心算法而非约束求解器 | K12 排课规模小,贪心满足约束即可,避免引入 OR-Tools 等重依赖 |
| 校验纯函数抽离到 lib | data-access 只做 DB 操作,字段校验纯函数易测试(A-02 修复) |
| 规则回退到全局 | 班级未配规则时回退全局,保证排课流程不中断 |