Files
NextEdu/src/modules/scheduling

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 修复)
规则回退到全局 班级未配规则时回退全局,保证排课流程不中断