DEVELOPER DOCS

排課與調代課系統 — 架構規劃文件

原始檔 docs/architecture.md · 在 GitHub 上檢視

版本:v1.0(2026-07-07) 狀態:規劃定稿,交棒開發依據 tasks.md 授權:MIT 已定技術棧:Python 3.12 + FastAPI + OR-Tools CP-SAT + Vue 3 + PostgreSQL 16 + Docker Compose


0. 專案定位

一套開源免費、單校自架、純 Web 的排課與調代課系統,服務對象為全國國小、國中、普通型高中、綜合型高中、技術型高中的教學組長(主要操作者)、教務主任(審核/查看)與一般教師(查課表、請假、接代課通知)。

三大設計原則(所有後續決策的最高準則):

  1. 不寫死學制:五種學制的差異一律化約為「可設定的資料」(自訂節次表、彈性排課單位、可調約束),而非分支程式邏輯;以「學制範本」讓使用者一鍵初始化。
  2. 一鍵部署:docker compose up -d 一行指令完成安裝;所有維運(備份、還原、升級)都有 UI 按鈕或單行指令。
  3. 30 分鐘上手:首次登入進入「設定精靈」,一步一步完成建置;所有畫面使用台灣教務慣用語(節次、科任、配課、鐘點)。

1. 需求規格書

1.1 使用者角色與權限矩陣

功能 系統管理員 教學組長 教務主任 教師
系統設定、帳號管理、備份還原
基礎資料維護(節次表/教師/班級/科目/場地) 檢視
配課與排課(手動+自動) 檢視
課表發布 核可(可設定是否需要)
調代課安排 檢視 回應代課邀請
請假登記 ✅(可代登) 檢視 ✅(登記本人)
查詢課表(班級/教師/場地) ✅(全校可見或僅本人,可設定)
鐘點統計報表 僅本人
  • 角色採 RBAC,一人可兼多角色(例:教學組長同時是教師)。
  • 「教務主任核可」為可開關的流程節點,預設關閉(多數學校由教學組長直接發布)。

1.2 User Story 清單

優先級:MVP(M0–M5 必做)/ v2 / v3

A. 系統建置與基礎資料

# User Story 優先級
A1 身為資訊組長,我能用一行 Docker 指令架好系統,並用預設管理員帳號登入 MVP
A2 身為教學組長,首次登入有設定精靈:選學制範本 → 設定學年學期 → 自訂節次表 → 匯入教師/班級/科目 → 完成 MVP
A3 身為教學組長,我能自訂節次表(每天節數、每節起訖時間、午休、早自習、週三下午不排課等) MVP
A4 身為教學組長,我能用 Excel 範本批次匯入教師(姓名、任教科目、基本鐘點、行政職減課)與班級 MVP
A5 身為教學組長,我能維護場地(普通教室、專科教室、實習工場,含容量與適用科目) MVP
A6 身為管理員,我能批次建立教師帳號並發送啟用通知 MVP
A7 身為教學組長,我能開新學期並複製上學期的基礎資料(教師/班級/科目/節次表) MVP
A8 身為管理員,我能整合教育雲端帳號(OpenID Connect)登入 v2

B. 配課

# User Story 優先級
B1 身為教學組長,我能建立配課:排課單位 × 科目 × 教師(可多位協同)× 每週節數 × 連堂規則 × 場地需求 MVP
B2 身為教學組長,我能建立「跑班群組」(多班聯排,如高中多元選修、綜高學程),群組內課程同時段開課 MVP
B3 身為教學組長,我能看到每位教師的配課節數統計,即時對照基本鐘點(超鐘點/不足以顏色標示) MVP
A0 身為評估這套系統的人,我能在全新系統上一鍵載入示範資料(18 班國中、48 位教師、384 筆配課),不必先手 key 幾百筆就能試跑自動排課。規格見 app/data/demo_school.json,只在「尚無任何學期」時可執行 v1.2
B3-1 身為管理員,我能設定「超鐘點上限」(預設 8 節),配課超出「應授 + 上限」時系統擋下並指名是誰。上限是相對值而非固定節數,因為應授本身因身分而異(臺南市:國文專任 16、兼任導師 11、兼任主任 6)。未填基本鐘點的教師不受管制——base_periods=0 代表資料未建立,不是真的不用上課,硬擋會讓未維護基鐘的學校連一般配課都做不了 v1.2
B4 身為教學組長,我能用 Excel 批次匯入配課資料 MVP
B5 身為教學組長,我能設定教師「不可排課時段」(兼行政、公假、進修)與「偏好時段」 MVP

C. 排課

# User Story 優先級
C1 身為教學組長,我能在週課表格子上以拖拉方式手動排課,衝突格位即時以紅色提示並說明原因 MVP
C2 身為教學組長,我能切換班級/教師/場地三種課表視角 MVP
C3 身為教學組長,我能一鍵自動排課,看到進度,並在完成後看到軟約束達成度報告 MVP
C4 身為教學組長,我能「鎖定」部分格位(如已敲定的科任時段),自動排課不得移動 MVP
C5 身為教學組長,自動排課無解時,系統告訴我最可能衝突的約束(如「王老師配課 22 節但可排時段僅 20 格」) MVP
C6 身為教學組長,我能保留多個課表草稿版本,比較後選一個發布 MVP
C7 身為教學組長,發布後全校師生可查詢課表 MVP
C8 身為教學組長,我能對已發布課表做局部調整並重新發布(保留歷次版本) MVP
C9 身為教學組長,我能設定進階軟約束權重(科目分佈均勻、教師空堂集中等) v2

D. 調代課

# User Story 優先級
D1 身為教師,我能登記請假(日期範圍、假別),系統自動列出受影響節次 MVP
D2 身為教學組長,我能替教師代登請假 MVP
D3 身為教學組長,對每一受影響節次,我能選擇處理方式:代課/調課(對調)/併班/自習/公假不處理 MVP
D4 身為教學組長,系統推薦可代課教師(該時段空堂 → 同科目優先 → 當日已在校 → 代課鐘點平衡),一鍵指派 MVP
D5 身為被指派教師,我會收到站內通知與 Email,可一鍵「確認收到」(實務上組長指派前已口頭徵得同意,故不設婉拒流程,2026-07-09 使用者定案) MVP
D6 身為教學組長,我能看到每筆代課通知的確認狀態,對未確認者一鍵再次提醒 MVP
D7 身為教學組長,我能查看任一日期的「今日調代課看板」,列印當日調代課通知單 MVP
D8 身為教學組長,我能設定調課(甲乙教師對調兩節課),系統驗證雙方均無衝突 MVP
D9 身為教學組長,月底我能匯出代課鐘點統計表(依教師、依假別、依經費來源) MVP
D10 身為教師,我能在手機瀏覽器順利完成「查課表、請假、確認代課」 MVP
D11 系統可介接校內差勤系統自動帶入假單 v3

E. 報表與整合

# User Story 優先級
E1 我能匯出班級/教師/場地課表為 Excel、PDF、PNG(A4 直式,適合公告欄張貼) MVP
E2 我能匯出全校總課表(大表)Excel MVP
E3 系統每日自動備份資料庫,管理員可一鍵下載備份檔、一鍵還原 MVP
E4 系統提供 OpenAPI 文件,供校務系統介接(唯讀課表 API) v2
E5 通知支援 webhook(供學校自行串接 LINE 官方帳號、Teams 等) v2

1.3 各學制差異對照表

差異點 國小 國中 普高 綜高 技高 系統對應機制
節次結構 40 分/節,週三下午空 45 分/節 50 分/節 50 分/節 50 分/節 自訂節次表 + 全校性「不排課時段」
授課型態 導師包班+科任 領域專任 專任+跑班選修 學程跑班 群科+實習 配課皆為「排課單位×科目×教師」,包班=同一教師大量配課
跑班 少(彈性課程) 多元選修、加深加廣 學術/專門學程 部分專業科目 跑班群組(多班聯排同時段)
連堂 實驗課 2 連堂 專門學程實習 實習課 2–4 連堂 配課的連堂規則(N 連堂 × 每週 M 次)
場地限制 專科教室 專科教室 實驗室 實習場所 實習工場容量是硬限制 場地類型+容量,排入硬約束
特殊時段 導師時間、晨光 週會、社團 團體活動、彈性學習 同普高 同普高 節次表可標記「固定用途時段」不參與排課
教師特例 科任跨多班 跨年級 兼行政減課 跨學程 業界師資僅特定時段到校 教師「可排課時段」白名單

結論:五學制差異全部收斂為 4 個可設定機制 —— 自訂節次表、彈性排課單位(班級/跑班群組)、連堂規則、可排時段限制。系統出廠附五種學制範本(預填節次表與科目清單),使用者可再修改。

混合學制學校(M0 後補充,2026-07-09):學制標籤掛在班級(class_units.track)而非學校,因此完全中學(國中部+高中部)、普高+綜高並存(如南大附中)、技高附設普通科、K-12 實驗學校等混合型學校天然支援——同一學期內各班級各掛各的學制標籤、各用各的節次表。配套要求: 1. class_units.period_table_id(nullable,空=學期預設節次表)——每個班級必須可指定所屬節次表,排課引擎與衝突檢查據此取得該班合法時段(任務卡 M1-6); 2. 進修部/夜間部以「另一套節次表」處理,不引入新學制標籤; 3. 建學期僅帶入一個主要範本;第二學制的節次表由「新增節次表選範本」建立,科目以 Excel 匯入補齊(範本科目合併帶入列 v2)。

1.4 Out of Scope(明確不做)

  • ❌ 成績管理、學籍管理、差勤簽核(僅接收請假事實,不做假單簽核流程)
  • ❌ 多校共用 SaaS、跨校資料交換
  • ❌ 學生選課系統(選課結果以 Excel 匯入跑班群組名單;選課過程不在本系統)
  • ❌ 原生 App(以響應式 Web 支援手機/平板)
  • ❌ 大學/幼兒園學制
  • ❌ 薪資計算(只產出代課鐘點統計表,計薪由人事/會計系統處理)

2. 領域模型與資料庫設計

2.1 ER 圖

erDiagram
          SCHOOL ||--o{ SEMESTER : "有"
          SEMESTER ||--o{ PERIOD_TABLE : "使用"
          PERIOD_TABLE ||--o{ PERIOD : "定義節次"
          SEMESTER ||--o{ TEACHER : "聘任(快照)"
          SEMESTER ||--o{ CLASS_UNIT : "編制"
          SEMESTER ||--o{ SUBJECT : "開設"
          SEMESTER ||--o{ ROOM : "可用場地"
          SEMESTER ||--o{ SCHEDULING_UNIT : "排課單位"
          CLASS_UNIT ||--o{ SCHEDULING_UNIT_MEMBER : "參與"
          SCHEDULING_UNIT ||--o{ SCHEDULING_UNIT_MEMBER : "包含班級"
          SCHEDULING_UNIT ||--o{ COURSE_ASSIGNMENT : "配課"
          SUBJECT ||--o{ COURSE_ASSIGNMENT : "科目"
          COURSE_ASSIGNMENT ||--o{ ASSIGNMENT_TEACHER : "授課教師(可協同)"
          TEACHER ||--o{ ASSIGNMENT_TEACHER : "任教"
          COURSE_ASSIGNMENT ||--o{ BLOCK_RULE : "連堂規則"
          TEACHER ||--o{ TEACHER_TIME_RULE : "可排/偏好時段"
          SEMESTER ||--o{ TIMETABLE : "課表版本"
          TIMETABLE ||--o{ SCHEDULE_ENTRY : "格位"
          COURSE_ASSIGNMENT ||--o{ SCHEDULE_ENTRY : "被排入"
          ROOM ||--o{ SCHEDULE_ENTRY : "使用場地"
          TEACHER ||--o{ LEAVE_REQUEST : "請假"
          LEAVE_REQUEST ||--o{ AFFECTED_PERIOD : "受影響節次"
          AFFECTED_PERIOD ||--o| SUBSTITUTION : "處理"
          TEACHER ||--o{ SUBSTITUTION : "代課教師"
          SUBSTITUTION ||--o{ NOTIFICATION : "通知"
          USER ||--o{ USER_ROLE : "角色"
          USER ||--o| TEACHER : "綁定"
      

2.2 核心實體定義

實體 說明 關鍵欄位
app_settings 單校全域設定 不隸屬任何學期,以 key/value 存放(加一個設定不該要一次遷移)。目前放 SMTP 寄信設定;校名走 .envSCHOOL_NAME規劃階段此表原名 school,實作時改為 key/value,故無「校名/Logo」等固定欄位
semester 學年學期 學年度(如 115)、學期(1/2)、起訖日、狀態(準備中/進行中/已封存)
period_table 節次表 名稱;一學期可有多套(如高中部/國中部各一套)
period 節次定義 星期(1–5,可擴至 6)、第幾節、起訖時間、類型(一般課/早自習/午休/導師時間/固定用途)
teacher 教師 姓名、任教科目(多)、基本鐘點、行政職稱與減課數、是否外聘/業界師資、在職狀態、聯絡資訊(Email/手機/LINE ID,皆選填)、綁定帳號(user_id,nullable FK → users)
class_unit 班級 年級、班名、學制標籤(普/綜/技/國中/國小)、群科(技高)、導師、人數;班名同學期唯一(uq(semester_id, name),遷移 0017)——衝突訊息、課表、匯出全都以班名指稱班級,兩個「301」會讓人根本分不出是哪一班
subject 科目 名稱、領域/群別、需要場地類型、預設連堂規則
room 場地 名稱、類型(普通/專科/實習工場/戶外)、容量、適用科目
scheduling_unit 排課單位(關鍵抽象) 類型:single(單一班級)/group(跑班群組);跑班群組透過 scheduling_unit_member 關聯多個班級
course_assignment 配課 排課單位、科目、每週節數、場地需求(類型或指定場地)、是否鎖定場地
assignment_teacher 配課教師 支援協同教學(多教師);主教/協同標記
block_rule 連堂規則 連堂長度(2–4)、每週次數(如「每週 6 節,其中 3 連堂×2 次」)
teacher_time_rule 教師時段規則 類型:unavailable(硬:不可排)/avoid(軟:盡量避開)/prefer(軟:偏好);對應星期×節次
timetable 課表版本 狀態:draft / published / archived;同學期可多份草稿,僅一份 published;unscheduled JSONB(遷移 0016)存部分排課當時「為什麼排不下」的理由——「哪些課沒排」由 completeness() 從 DB 重算(唯一真相,手動改過的課表也算得對),solver 的紀錄只補上原因
schedule_entry 課表格位 課表版本、配課、星期、節次、span(連堂)、room_id(空=沿用配課場地)、locked 旗標;唯一性約束在應用層+求解器驗證(教師/班級/場地同時段不重複)
leave_request 請假 教師、假別(公/事/病/婚/喪/產/進修)、起訖日期時間、事由、登記人
affected_period 受影響節次 請假展開後的每一節課;狀態機見 §5.3
substitution 調代課處置 類型:substitute(代課)/swap(調課)/merge(併班)/self_study(自習)/cancel(不處理);代課教師、是否計鐘點、經費來源標記
notification 通知 站內+Email(v2 經 channel adapter 加 LINE/webhook);類型、收件人、已讀狀態
user / user_role 帳號與角色 本地帳密(bcrypt);角色:admin / director(教務主任)/ scheduler(教學組長)/ teacher
audit_log 操作軌跡 誰在何時改了什麼(排課異動、調代課指派必記)
constraint_config 軟約束權重 每學期一組 key/value(uq(semester_id, key));S1–S8 的權重與 H10 上限等可調參數。開新學期複製時可勾選一併帶走
wizard_state 設定精靈進度 單筆(id=1):目前步驟、是否已完成。未完成時路由守衛會把組長/管理員導回精靈

2.3 關鍵設計決策

D1|「排課單位」抽象是支撐五學制的核心。 排課的最小單位不是「班級」而是 scheduling_unit。單一班級的課 → single;跑班選修(3 個班同時段拆成 5 組上不同課)→ 建一個 group 含 3 個班,群組內的多筆配課由求解器強制排在同一時段。國小包班 = 導師在自己班的大量 single 配課,無需特殊邏輯。

D2|節次表資料化,絕不寫死。 「週三下午不排課」「第 8 節只有高三上」這類規則,全部以 period.type 與教師/班級的時段規則表達。完全中學(國中部+高中部節次時間不同)以多套 period_table 支援——這是市售系統常見痛點,必須在 schema 層解決。

D3|學期快照,不做跨學期外鍵。 教師、班級、配課皆隸屬於 semester。開新學期用「複製精靈」拷貝資料,而非共用主檔——避免「教師去年任教科目變動污染歷史課表」的經典錯誤。跨學年報表用姓名+身分證末四碼(可選填)做軟性對應。

D4|課表版本化。 timetable 支援多草稿並存與發布快照。學期中調整 = 複製 published 為新 draft → 修改 → 重新發布,舊版自動 archived。調代課紀錄掛在 published 版本上,不受草稿影響。

D5|單校 schema,不做 multi-tenant。tenant_id。但所有查詢一律以 semester_id 為範圍,天然支援多學期並存與歷史保存。

D6|時區與日期時間政策(M0 健檢新增)。 三類時間嚴格區分,不得混用: 1. 系統時間戳(created_at、locked_until、通知時間)→ DateTime(timezone=True),一律 UTC aware; 2. 領域日期(學期起訖、請假日期、調代課日期)→ Date,無時區概念; 3. 領域時間(節次起訖時間)→ Time,即學校牆鐘時間,無時區概念。 「今日/本週」等判定以 .envTZ(預設 Asia/Taipei)換算,不用 UTC 直接取日期。SQLite(測試)回傳 naive datetime 的差異,統一在讀取層正規化為 UTC。

D7|跨節次表的資源衝突以「牆鐘時間重疊」判定(M1 健檢新增,2026-07-09)。 教師(H2)與場地(H3)是跨班級共用的資源;當兩堂課所屬班級使用不同節次表時,節次號相同不代表時間相同、節次號不同也可能時間重疊(例:國小部 40 分/節的第 4 節 10:30–11:10,與高中部 50 分/節的第 3 節 10:10–11:00 重疊)。因此: 1. 衝突檢查與 CP-SAT 建模中,教師/場地的「同時段」定義 = 同星期且兩節次的起訖時間區間重疊;兩班同節次表時退化為 period_no 相等(常見情形,零額外成本); 2. 實作上預先計算「節次表兩兩之間的節次重疊矩陣」(每學期節次表數 ≤ 個位數,矩陣極小),衝突檢查仍可達 <100ms; 3. 班級不衝堂(H1)只涉及單一班級自身的節次表,維持 period_no 判定; 4. 跑班群組的成員班級必須使用同一節次表(否則 H7「同時段開課」無意義),配課建立時驗證並拒絕。

D8|場地互斥,容量僅作事前警告(M2 健檢新增,2026-07-10)。 v1 中同一場地同時段至多一門課——H3 一律以互斥判定,手動衝突檢查(M2-3 已如此實作)與 CP-SAT 建模(M3-2)語意一致。room.capacity 不參與求解,僅於 pre-flight 對「班級人數 > 場地容量」提出警告;M3-2 驗收的「實習工場容量限制」即以「互斥 + pre-flight 警告」解讀。若日後需要「一場地多組併用」(大型工場、體育館分區),再引入容量型佔用,屬 schema 不變的引擎擴充。


3. 排課引擎設計

3.1 問題建模

排課 = 對每筆配課的每一節,指派一個(星期×節次)時段與場地,滿足所有硬約束並最大化軟約束加權分數。這是經典的 School Timetabling Problem,採 Google OR-Tools CP-SAT 求解。

決策變數:x[a, s] ∈ {0,1} — 配課 a 的課排在時段 s;連堂以「起始時段變數 + 區間佔用」建模。場地在需求非唯一時另設 r[a, s, room] 變數,佔多數的「固定場地」(班級教室)則預先綁定以縮小搜尋空間。

3.2 約束條件分類

硬約束(違反即無效解)

編號 約束 說明
H1 班級不衝堂 同一班級(含所屬跑班群組)同時段至多一門課
H2 教師不衝堂 同一教師同時段至多一門課(含協同)
H3 場地不衝堂 同一場地同時段至多一門課;實習工場容量限制
H4 教師不可排時段 teacher_time_rule.unavailable(兼行政、業界師資到校日、公假)
H5 節次有效性 只能排在 period.type = 一般課 的時段
H6 連堂完整性 連堂課必須連續且不跨午休
H7 跑班同步 同一跑班群組的所有配課排在同一時段
H8 週節數守恆 每筆配課排入的節數 = 設定的每週節數
H9 鎖定格位 schedule_entry.locked 的格位不得移動
H10 每日科目上限 同班同科目每日至多 N 節(預設 2,可設定)。只計節長 1 的格位——連堂是一次上完的整塊,不計入亦不受限;但同一門課剩下的單節仍受限。定義以 app/solver/validator.py 為準

軟約束(加權計分,權重可調)

權重存於 constraint_config(key/value,未設定則回退預設);權重 0 = 關閉該項。 高=8、中=4、低=1。目標函數 = Σ(權重 × 懲罰),CP-SAT 最小化之。

編號 約束 預設權重
S1 教師偏好時段(prefer 加分 / avoid 扣分)
S2 同班同科目分散於不同日
S3 教師每日授課節數上限(預設 ≤6)
S4 教師空堂集中(減少零碎空堂)
S5 主科(subject.is_major)優先排上午
S6 教師連續授課 ≤3 節
S7 導師的課優先排在自己班的第一節(國中小)
S8 全校教師偏好達成率的公平性(最差者優先)

達成度報告(app/solver/report.py)與建模不共用程式碼,從課表本身重新推導, 輸出「滿分/得分/未達成明細」的人話清單。其 total_penalty 與 CP-SAT 的 objective 刻意用不同尺度:目標函數以「超出的節數」計價(讓 solver 有梯度可下降), 報告以「未達成的次數」計價(讓教學組長知道要修幾個地方)。

3.3 選型理由與效能預估

  • CP-SAT 而非基因演算法/模擬退火:CP-SAT 對硬約束是「證明式滿足」,不會產生違反硬約束的解;GA/SA 需自行調參且無法證明無解。CP-SAT 為 anytime solver,隨時可中斷取當前最佳解,天然支援「進度條+提前結束」。
  • 規模預估:60 班 × 35 節 ≈ 2,100 節課,變數量 10⁵ 級,CP-SAT 在 4 核心機器約 1–5 分鐘可得高品質解(業界同類系統實證區間)。預設 timeout 10 分鐘,可設定。
  • 執行架構:求解跑在獨立 worker 容器(RQ + Redis 佇列),不阻塞 Web;進度以 polling API 回報(前端每 2 秒),避免 WebSocket 增加部署複雜度。
  • 進度與心跳:進度存於獨立的 Redis hash(solve:{job_id}),不放 RQ job meta——RQ 只知道「執行中/失敗」,說不出「已找到 12 個解、目前目標值 148」。worker 每 2 秒送出心跳;API 讀到 running 但心跳超過 30 秒未更新即判定 worker 已死,回報明確錯誤而非讓前端永遠轉圈。
  • 提前結束 vs 取消:stop 停止搜尋但保留當下最佳解並寫出結果草稿;cancel 停止並丟棄。兩者都經由 CpSolver.stop_search(),由背景執行緒觸發。

3.4 無解與降級策略

flowchart TD
          A[啟動自動排課] --> B{CP-SAT 求解}
          B -->|最佳/可行解| C[產出草稿 + 軟約束達成報告]
          B -->|timeout 有部分解| D[回傳當前最佳解 + 標示未達標軟約束]
          B -->|無解 或 timeout 且零解| E[衝突定位]
          E --> P{pre-flight 必要條件}
          P -->|不成立| G
          P -->|全數通過| F["逐項試解:把每個『轉得動的旋鈕』<br/>關掉重解,看誰是瓶頸"]
          F -->|純硬約束有解| J["其實排得出來,只是軟約束最佳化太慢<br/>→ 建議延長時間"]
          F -->|仍然無解| G["人話報告:<br/>『音樂教室需求 30 節,扣掉相關教師的<br/>不可排時段只剩 28 節可用』<br/>『放寬其中任何一項即可排出課表』"]
          G --> H[教學組長修正資料後重跑<br/>或改用部分排課]
          H --> I["部分排課模式:<br/>放寬選定約束為軟約束、允許少數課務未排入,<br/>排入 95%+ 並列出未排清單"]
      
  • 衝突定位是本系統的差異化重點:市售系統只回「排不出來」,本系統要說出是哪幾件事湊在一起、鬆開哪一個就好了,並附上具體數字。
  • 事前檢查(pre-flight check):排課前先跑廉價的必要條件檢查(每位教師配課數 ≤ 可排時段數、每場地需求 ≤ 供給、每班週節數 ≤ 可用節次),攔截 80% 的資料錯誤,不浪費求解時間。必要條件不成立本身就是無解的證明,數字現成,不必啟動 solver。

定位方法:逐項試解(deletion filter),而非 assumption / unsat core

原設計是「每類硬約束掛 assumption literal,無解時取 unsat core」。實測後放棄:

同一份資料(6 班共用一間音樂教室) 證明無解耗時
純硬約束模型 0.8 秒
掛上 assumption literal 的模型 60 秒仍證不完

原因是 enforcement literal 讓 presolve 認不出「30 節課塞進 28 格」的鴿籠結構;換過三種編碼(單條大線性、逐格二元子句、只掛部分約束)都一樣。

改採刪除法:把每個旋鈕整組關掉、重新建一個乾淨的模型求解——每次求解都完整 presolve,上例整套定位約 2~3 秒。附帶的好處是每一條結論都被一次真實的求解驗證過:報告說「放寬音樂師1 的不可排時段就排得出來」,是因為真的排出來了。

  • 旋鈕 = 教學組長改得動的東西:H4 某位教師的不可排時段、H3 某間場地的互斥、H10 全校的每日科目上限、H9 鎖定的格位。
  • H1(班級同時段一門課)與 H2(教師同時段一門課)不是旋鈕——沒有東西可轉,而且真正的成因(某位教師配課超過可排格數)pre-flight 已經算得出來。
  • 報告的誠實邊界:each 模式的每一項都被一次真實求解驗證過(關掉它就排出來了), 這個結論恆為真;但清單是否完整、以及 joint 的組合是否最小,取決於時間是否用完、 以及每次試解是否在時限內判定得出來。任何一次試解回 unknown(沒能證明可行、也沒能證明無解) 都會把 complete 降為 False,structural 的措辭也隨之收斂—— 絕不宣稱一件從未被證明的事
  • 報告分三種:
  • each:每一項各自都是瓶頸,放寬任何一項即可(最常見)。
  • joint:必須同時處理多項(先累加到可行,再逐一剔除不必要的)。
  • structural:旋鈕全轉到底仍然無解,問題出在配課總量,列出最吃緊的班級與教師。
  • 逾時且零解時也要跑定位:帶著軟約束目標函數的 CP-SAT 常常證不出 INFEASIBLE(見上表)。定位以純硬約束求解,能分辨「不可能」與「只是慢」——這兩件事的處置完全不同。

部分排課(partial scheduling)

  • 每個 lesson 多一個「未排入」變數,H8 週節數守恆 因此降級為高權重軟約束;模型永遠有解(最差是整張表空著)。
  • 「永遠有解」必須連「完全排不下的課」都算數(M6-3):某門課的可排時段被硬條件蓋滿時(例如協同教學的兩位教師不可排時段剛好聯集成整週),建模階段早期是直接 raise,結果部分排課在最需要它的時候整鍋失敗。現改為建一個恆為 1 的未排入變數(不建位置變數),把該課列進未排清單並記下原因,其餘照排;一般模式維持 raise。
  • 未排的節數以排課單位計,不是逐筆配課:跑班群組是多門選修同時段開,少排一個時段就是少一節課,逐筆記會灌水成數倍。
  • 使用者可另外勾選放寬 H4 / H9 / H10H1 / H2 / H3 永遠不可放寬——一位教師不能同時出現在兩間教室、一間教室不能同時容納兩班,那是物理,不是政策;放寬只會產生一張沒有人能照著上課的課表。
  • 懲罰量級刻意拉開:未排入(10000) ≫ 違反被放寬的約束(1000) ≫ 軟約束(1~8)。 這個順序是正確性前提,不是調參偏好:軟約束權重因此硬性上限 MAX_WEIGHT = 100 (一節課最多參與約 8 個懲罰項 → 軟約束總代價 < 800 < 1000)。若允許把某項軟約束設到 20000,solver 會理性地「丟掉一節課」去換取分散度——那是災難。勾了「可放寬教師不可排時段」,意思就是「寧可讓老師委屈一節,也不要讓這門課排不進去」。
  • 因此部分排課的 objective 與一般模式尺度不同(被未排入的懲罰灌爆),UI 顯示「未排 N 節」而不是目標值。
  • pre-flight 的把關也隨之放寬:部分排課只擋結構性錯誤(連堂比整段連續節次還長、跑班群組節數不一致、某類型場地一間都沒有)——那些連模型都建不起來;「教師配課超量」「場地不夠」正是部分排課要處理的事。
  • 結果一律再交給 validator 檢查:除了被明確放寬的那類約束以外,其餘硬約束必須一格都沒違反。「少排幾節」不可以偷偷變成「排錯幾節」。

4. 技術架構

4.1 技術選型

選型 理由
前端 Vue 3 + TypeScript + Vite + Pinia + Naive UI 台灣社群 Vue 使用率高、學習曲線平緩,對兼職開源貢獻者友善;Naive UI 元件完整且 TypeScript 原生
課表互動 自製 CSS Grid 課表 + HTML5 Drag & Drop(封裝為 TimetableGrid 元件) 課表拖拉邏輯高度領域化,通用套件反而綁手腳
後端 Python 3.12 + FastAPI + SQLAlchemy 2.0 + Alembic + Pydantic v2 與 OR-Tools 同語言;FastAPI 自帶 OpenAPI 文件(滿足 E4,正式部署預設關閉,.envAPI_DOCS_ENABLED 可開);型別完整
排課引擎 OR-Tools CP-SAT(獨立 solver/ 模組,與 Web 解耦) 見 §3.3
任務佇列 RQ + Redis 極簡、純 Python;排課與 Email 寄送皆走佇列
資料庫 PostgreSQL 16 主流、可靠;pg_dump 備份簡單
反向代理 Caddy 設定檔 3 行、自動 HTTPS(有網域時)、自動 HTTP 降級(內網 IP 時)——對學校資訊組最友善
匯入匯出 openpyxl(Excel)、WeasyPrint(PDF)、Pillow(PNG) 純 Python、無外部二進位依賴
測試 pytest + Vitest + Playwright 各層主流
CI GitHub Actions 開源標配

4.2 系統架構圖

flowchart LR
          subgraph 使用者
              U1[教學組長/教師<br/>瀏覽器]
          end
          subgraph Docker Compose
              C[Caddy<br/>反向代理+靜態檔] --> API[FastAPI<br/>REST API]
              API --> PG[(PostgreSQL 16)]
              API --> RD[(Redis)]
              W[worker<br/>佇列 default<br/>OR-Tools 排課] --> RD
              W --> PG
              WO[worker-ops<br/>佇列 ops<br/>匯出·備份·還原·寄信<br/>+ 定時任務排程器] --> RD
              WO --> PG
              WO --> V[/備份 Volume/]
          end
          U1 --> C
          WO -.SMTP.-> M[學校/免費 SMTP]
      

前端建置為靜態檔由 Caddy 直接服務(不需 Node 容器),共 6 個容器:caddy(web)、api、worker、worker-ops、postgres、redis。

D9 背景任務分兩條佇列(M6-2):default 只跑自動排課,ops 跑匯出/備份/還原/寄信與定時任務,各由一個 worker 行程守著(同一個映像)。理由是快慢任務不該互相堵住——一次 60 班排課會佔住 worker 好幾分鐘,而那正是教學組長最常按「匯出課表」的時候;單一佇列下匯出會排在排課後面直到逾時失敗。排課永遠只走 default,故 worker-ops 不載入求解引擎,記憶體預算低得多(1536MB vs 512MB)。

定時任務(每日備份、排程器心跳)一律排進 ops 並由 worker-opswith_scheduler=True 撈回執行:排課 worker 一忙就是好幾分鐘,不該負責「準時」的事。

還原仍須在排課進行中拒絕(409)——這條在分佇列後依然成立,但理由是資料安全而非排隊:pg_restore 會覆蓋整個資料庫,而排課 worker 正要把結果寫回同一個庫。

4.3 部署方案

主線:Docker Compose 自架

安裝三步驟(deploy/README 首頁):
      1. 安裝 Docker(附各 OS 圖文)
      2. 下載 docker-compose.yml + .env 範例,改兩個值(管理員密碼、校名)
      3. docker compose up -d → 開瀏覽器 http://<主機IP> 進設定精靈
      
  • 硬體最低需求:2 核 4GB RAM、10GB 磁碟(自動排課建議 4 核 8GB;求解期間 CPU 滿載屬正常,文件需註明)。相容 x86-64 與 ARM64(NAS/樹莓派),CI 產出雙架構 image。
  • 升級:docker compose pull && docker compose up -d,Alembic 於 api 啟動時自動遷移。
  • 副線:低成本 VPS:文件提供「VPS + 網域 + Caddy 自動 HTTPS」指引,適合無校內主機的小校;強調資料在自己 VPS、非 SaaS。

4.4 資料匯入/匯出與整合

  • 匯入:系統內建可下載的 Excel 範本(教師、班級、科目、配課、跑班名單),上傳後逐列驗證,錯誤以「第 N 列:原因」清單回報,全對才入庫(交易式)。
  • 匯出:班級/教師/場地課表 → Excel / PDF(A4 直式)/ PNG;全校總表 → Excel;代課鐘點統計 → Excel。
  • 校務系統銜接:v2 提供唯讀 REST API(API key 授權)+ OpenAPI 文件;不主動做特定廠商介接(各縣市校務系統版本混亂,由社群依 API 自行開發)。

4.5 帳號與安全

  • MVP:本地帳號。管理員批次建立教師帳號(匯入 Excel 時一併產生),首次登入強制改密碼;bcrypt 雜湊;session cookie(HttpOnly + SameSite,簽章式無狀態 token)。
  • Session 撤銷:token 內嵌密碼指紋(password_hash 尾段),改密碼即令所有既有 session 失效;帳號停用(is_active=false)每請求檢查即時生效。
  • 登入防暴力:連續失敗 5 次鎖定 15 分鐘(M0-2 已實作,參數可設定)。
  • 教育雲端帳號(OpenID Connect)評估:技術上為標準 OIDC,可行;但各縣市申請流程不一且需學校行政程序,故列 v2 選配,MVP 不阻塞。程式面預留:user.auth_provider 欄位 + 登入流程策略介面。
  • 個資:教師個資最小化蒐集(不收身分證全碼);audit_log 記錄排課與調代課異動;HTTPS 由 Caddy 處理。
  • 備份還原:每日 02:00 自動 pg_dump 至 volume(保留 30 份);管理 UI 提供「立即備份、下載、上傳還原」;文件教學 NAS 排程二次備援。

5. UI/UX 規劃

5.1 資訊架構與關鍵頁面

側邊欄(教學組長/管理員視角;純教師只看得到 ★ 三項):
      ├── 儀表板(今日調代課摘要、待辦)
      ├── 課表查詢 ★(班級/教師/場地,含匯出 Excel/PDF/PNG)
      ├── 基礎資料
      │   ├── 學期與節次表
      │   └── 教師/班級/科目/場地(含 Excel 匯入)
      ├── 排課作業
      │   ├── 配課管理
      │   ├── 排課工作台(拖拉介面)
      │   ├── 自動排課(OR-Tools;進度、達成度、無解定位)
      │   ├── 版本與發布
      │   └── 課表元件(示範)
      ├── 調代課
      │   ├── 請假登記 ★
      │   ├── 調代課處理(推薦與指派)
      │   ├── 今日調代課(含 A4 公告列印)
      │   ├── 調代課紀錄
      │   ├── 代課鐘點統計 ★(教師只看自己)
      │   └── 通知確認看板
      └── 系統管理(備份還原、SMTP、重設精靈;限管理員)
      
      獨立於側邊欄之外:登入、首次登入強制改密、設定精靈、A4 公告列印頁。
      

排課工作台(核心頁面)線框:

┌─────────────────────────────────────────────────────────┐
      │ 視角:[班級▼ 301班] [教師] [場地]   版本:草稿A ▼  [自動排課] │
      ├───────────────┬─────────────────────────────────────────┤
      │ 未排課務        │        一   二   三   四   五              │
      │ ┌───────────┐ │  第1節 ┌──┐┌──┐┌──┐┌──┐┌──┐             │
      │ │國文 王老師  │ │       │國文││  ││數學││  ││英文│             │
      │ │剩 2 節     │ │  第2節 │🔒 ││  ││    ││  ││    │             │
      │ ├───────────┤ │  ...   └──┘└──┘└──┘└──┘└──┘             │
      │ │數學 李老師  │ │  拖入時:可放=綠框、衝突=紅框+浮窗原因         │
      │ │剩 4 節     │ │  已排格:點擊→鎖定/移除/檢視;🔒=鎖定          │
      │ └───────────┘ │  週三下午格位反灰(不排課時段)                │
      └───────────────┴─────────────────────────────────────────┘
      
  • 拖曳時前端即時呼叫 POST /check-conflict(<100ms)渲染可放/衝突;放下即存(草稿自動儲存)。
  • 教師視角同一 grid 元件換資料源;衝突偵測共用同一後端服務,避免雙套邏輯。

5.2 設定精靈(30 分鐘上手的關鍵)

五步驟,每步可略過、可回頭:① 選學制範本(預填節次表+科目)→ ② 學年學期③ 節次表微調④ 下載 Excel 範本→匯入教師/班級⑤ 完成,引導至配課管理。精靈完成率是 UX 首要指標。

5.3 調代課狀態機

stateDiagram-v2
          [*] --> 已登記: 教師請假/組長代登
          已登記 --> 待處理: 系統展開受影響節次
          待處理 --> 已確認: 指派(代課/調課/併班/自習/不處理)
          已確認 --> 已完成: 上課日結束自動轉換
          已完成 --> [*]: 計入月鐘點統計
          待處理 --> 已取消: 銷假
          已確認 --> 已取消: 銷假(通知代課教師)
      

2026-07-09 使用者定案:不設「邀請/婉拒」流程。 台灣實務上教學組長指派代課前必先口頭徵得教師同意,系統通知只是正式告知。指派即生效;被指派教師收到通知後可一鍵「確認收到」(通知層的已讀確認旗標,不影響課務狀態),組長在看板可見各筆確認狀態並可對未確認者再次提醒。此設計較原「邀請中→接受/婉拒」大幅簡化狀態機與 UI。

  • 代課推薦排序:該時段空堂(硬性)→ 同科目 → 當日已有課(已在校)→ 本月代課鐘點少者優先(公平)。每位候選人顯示排序理由。
  • 調課(swap):選甲教師受影響節次 ↔ 乙教師某節次,系統驗證交換後雙方及兩班均無衝突才允許。
  • 通知管道(2026-07-09 健檢時評估更新):以 NotificationChannel 介面分層,MVP 實作兩個 channel——站內通知(永遠可用)與 Email(SMTP 設定後啟用);v2 以 adapter 增 webhookLINE。LINE 評估結論:LINE Notify 已於 2025-03 終止服務,個人化 LINE 通知須各校自申請 LINE 官方帳號走 Messaging API(免費額度約 200 則/月,超量付費),且推播需要經由加好友綁定取得的內部 userId(教師自填的 LINE ID 無法直接推播,僅供人工聯絡)——建置成本掛在各校行政端,故列 v2 選配,MVP 不阻塞。教師「手機」欄位僅供人工聯絡,不做簡訊(需付費)。收件人解析:teacher.user_id → 站內;teacher.email → Email(教師無帳號、僅有 Email 時仍可收信)。教師端所有操作(查課表/請假/確認代課)必須手機瀏覽器可完成。

6. 開發交棒計畫

完整 Milestone 切分、任務卡(含驗收標準)、專案目錄結構與測試策略,見 tasks.md。開發者(Claude Opus 4.8 / Sonnet 5)每次取一張任務卡實作,完成後依卡上驗收標準自我驗證。

本頁由 docs/architecture.md 自動產生(scripts/build_docs.py)。 要修改內容請改 Markdown 原始檔,不要直接編輯這份 HTML。
排課與調代課系統 · MIT 授權 · GitHub