|
| 1 | +# 專案架構與前後端溝通說明 |
| 2 | + |
| 3 | +本文件補充本專案的完整系統架構、API 規格,以及登入/查詢/分享三大流程的時序圖。 |
| 4 | + |
| 5 | +## 1) 系統架構圖(Mermaid) |
| 6 | + |
| 7 | +```mermaid |
| 8 | +flowchart TD |
| 9 | +
|
| 10 | + U[使用者瀏覽器<br/>index.html] |
| 11 | + FE[Frontend SPA<br/>public/app.js] |
| 12 | +
|
| 13 | + BE[Flask API<br/>server.py] |
| 14 | +
|
| 15 | + GF[GradeFetcher<br/>fetcher.py<br/>requests] |
| 16 | +
|
| 17 | + SCH[School System API<br/>shcloud2.k12ea.gov.tw] |
| 18 | +
|
| 19 | + TS[Cloudflare Turnstile<br/>siteverify] |
| 20 | +
|
| 21 | + SF[(shared_grades<br/>JSON Cache)] |
| 22 | +
|
| 23 | + U --> FE |
| 24 | +
|
| 25 | + FE -->|POST /api/login| BE |
| 26 | + FE -->|GET /api/structure| BE |
| 27 | + FE -->|POST /api/fetch| BE |
| 28 | + FE -->|POST /api/share| BE |
| 29 | + FE -->|GET /api/share/:id| BE |
| 30 | +
|
| 31 | + BE -->|fetch grades| GF |
| 32 | + GF --> SCH |
| 33 | +
|
| 34 | + BE -->|verify token| TS |
| 35 | +
|
| 36 | + BE <--> SF |
| 37 | +``` |
| 38 | + |
| 39 | +## 2) 元件分層與責任 |
| 40 | + |
| 41 | +| 層級 | 元件 | 責任 | |
| 42 | +|---|---|---| |
| 43 | +| 前端呈現層 | `public/index.html`, `public/style.css` | 提供儀表板、Modal、分享 UI。 | |
| 44 | +| 前端邏輯層 | `public/app.js` | API 呼叫、表單互動、localStorage、圖表渲染、分享頁唯讀模式。 | |
| 45 | +| API 層 | `server.py` | 路由、Session、Turnstile 驗證、分享檔案讀寫、靜態檔案服務。 | |
| 46 | +| 整合層 | `fetcher.py` | 以 requests 登入學校系統並抓取結構/成績。 | |
| 47 | +| 外部服務 | 學校系統、Cloudflare | 資料來源與人機驗證。 | |
| 48 | +| 佈署層 | `Dockerfile`, `docker-compose.yml` | Gunicorn 啟動、健康檢查、cloudflared tunnel。 | |
| 49 | + |
| 50 | +## 3) 前後端 API 規格 |
| 51 | + |
| 52 | +> Base URL: https://score.clhs.dev |
| 53 | +
|
| 54 | +### 3.1 安全驗證與登入 |
| 55 | + |
| 56 | +#### `GET /api/turnstile-site-key` |
| 57 | +- **用途**:取得前端初始化 Turnstile 的 site key。 |
| 58 | +- **回應 200** |
| 59 | +```json |
| 60 | +{ "siteKey": "<TURNSTILE_SITE_KEY or empty>" } |
| 61 | +``` |
| 62 | + |
| 63 | +#### `POST /api/login` |
| 64 | +- **用途**:登入學校系統,建立後端 session。 |
| 65 | +- **Request JSON** |
| 66 | +```json |
| 67 | +{ |
| 68 | + "username": "學號", |
| 69 | + "password": "密碼", |
| 70 | + "cf-turnstile-response": "token" |
| 71 | +} |
| 72 | +``` |
| 73 | +- **回應 200** |
| 74 | +```json |
| 75 | +{ "success": true, "message": "登入成功" } |
| 76 | +``` |
| 77 | +- **回應 400/401/403/500** |
| 78 | +```json |
| 79 | +{ "success": false, "message": "錯誤訊息" } |
| 80 | +``` |
| 81 | + |
| 82 | +#### `GET /api/check_login` |
| 83 | +- **用途**:確認當前 session 是否已登入。 |
| 84 | +- **回應 200** |
| 85 | +```json |
| 86 | +{ "logged_in": true } |
| 87 | +``` |
| 88 | +- **回應 401** |
| 89 | +```json |
| 90 | +{ "logged_in": false } |
| 91 | +``` |
| 92 | + |
| 93 | +#### `POST /api/logout` |
| 94 | +- **用途**:清除 session。 |
| 95 | +- **回應 200** |
| 96 | +```json |
| 97 | +{ "success": true, "message": "已登出" } |
| 98 | +``` |
| 99 | + |
| 100 | +### 3.2 成績查詢 |
| 101 | + |
| 102 | +#### `GET /api/structure` |
| 103 | +- **用途**:取得「學年度/學期」與可查詢考試清單。 |
| 104 | +- **Query**:`reload=true`(可選,強制重抓) |
| 105 | +- **回應 200** |
| 106 | +```json |
| 107 | +{ |
| 108 | + "structure": { |
| 109 | + "113學年度第2學期": { |
| 110 | + "year_value": "113_2", |
| 111 | + "exams": [ |
| 112 | + { "text": "第一次段考", "value": "1" }, |
| 113 | + { "text": "第二次段考", "value": "2" } |
| 114 | + ] |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | +``` |
| 119 | +- **回應 401** |
| 120 | +```json |
| 121 | +{ "error": "錯誤訊息" } |
| 122 | +``` |
| 123 | + |
| 124 | +#### `POST /api/fetch` |
| 125 | +- **用途**:查詢指定學期與考次的成績資料。 |
| 126 | +- **Request JSON** |
| 127 | +```json |
| 128 | +{ |
| 129 | + "year_value": "113_2", |
| 130 | + "exam_value": "2" |
| 131 | +} |
| 132 | +``` |
| 133 | +- **回應 200** |
| 134 | +```json |
| 135 | +{ |
| 136 | + "success": true, |
| 137 | + "message": "成績已更新", |
| 138 | + "data": { |
| 139 | + "Result": { |
| 140 | + "StudentName": "王小明", |
| 141 | + "StudentNo": "123456", |
| 142 | + "StudentClassName": "三年甲班", |
| 143 | + "SubjectExamInfoList": [] |
| 144 | + } |
| 145 | + } |
| 146 | +} |
| 147 | +``` |
| 148 | +- **回應 401/500** |
| 149 | +```json |
| 150 | +{ "success": false, "error": "錯誤訊息" } |
| 151 | +``` |
| 152 | + |
| 153 | +### 3.3 分享機制 |
| 154 | + |
| 155 | +#### `POST /api/share` |
| 156 | +- **用途**:將前端成績 JSON 產生分享連結。 |
| 157 | +- **Request JSON**:任意成績資料(需包含前端使用資料),且附帶 `cf-turnstile-response`。 |
| 158 | +```json |
| 159 | +{ |
| 160 | + "Result": {}, |
| 161 | + "cf-turnstile-response": "token" |
| 162 | +} |
| 163 | +``` |
| 164 | +- **回應 200** |
| 165 | +```json |
| 166 | +{ "success": true, "id": "Abc12-Def34_Gh~" } |
| 167 | +``` |
| 168 | +- **回應 400/403/500** |
| 169 | +```json |
| 170 | +{ "error": "錯誤訊息" } |
| 171 | +``` |
| 172 | + |
| 173 | +#### `GET /api/share/:share_id` |
| 174 | +- **用途**:讀取分享資料。 |
| 175 | +- **回應 200** |
| 176 | +```json |
| 177 | +{ "success": true, "data": { "Result": {} } } |
| 178 | +``` |
| 179 | +- **回應 400/404/500** |
| 180 | +```json |
| 181 | +{ "error": "Invalid ID format or Link expired or not found" } |
| 182 | +``` |
| 183 | + |
| 184 | +#### `GET /share/:share_id` |
| 185 | +- **用途**:回傳前端頁面(由前端再呼叫 `/api/share/:share_id` 載入唯讀資料)。 |
| 186 | + |
| 187 | +### 3.4 其他 |
| 188 | + |
| 189 | +#### `GET /health` |
| 190 | +- **用途**:健康檢查。 |
| 191 | +- **回應 200** |
| 192 | +```json |
| 193 | +{ "status": "ok" } |
| 194 | +``` |
| 195 | + |
| 196 | +## 4) 核心流程時序圖(Mermaid) |
| 197 | + |
| 198 | +### 4.1 登入 + 預抓結構 |
| 199 | + |
| 200 | +```mermaid |
| 201 | +sequenceDiagram |
| 202 | + participant Browser as Browser(app.js) |
| 203 | + participant Server as Flask(server.py) |
| 204 | + participant Turnstile as Cloudflare Turnstile |
| 205 | + participant Fetcher as GradeFetcher(fetcher.py) |
| 206 | + participant School as School API |
| 207 | +
|
| 208 | + Browser->>Server: GET /api/turnstile-site-key |
| 209 | + Server-->>Browser: {siteKey} |
| 210 | +
|
| 211 | + Browser->>Server: POST /api/login (username/password/token) |
| 212 | + Server->>Turnstile: siteverify(token) |
| 213 | + Turnstile-->>Server: success/fail |
| 214 | + alt 驗證成功 |
| 215 | + Server->>Fetcher: login_and_get_tokens() |
| 216 | + Fetcher->>School: GET login page + token |
| 217 | + Fetcher->>School: POST DoCloudLoginCheck |
| 218 | + Fetcher->>School: GET grades page + API token |
| 219 | + Fetcher-->>Server: cookies + student_no + api_token |
| 220 | + Server->>Fetcher: get_structure_via_api() |
| 221 | + Fetcher->>School: 取年期 + 平行取考試 |
| 222 | + Fetcher-->>Server: structure |
| 223 | + Server-->>Browser: {success:true} |
| 224 | + else 驗證失敗 |
| 225 | + Server-->>Browser: 403 |
| 226 | + end |
| 227 | +``` |
| 228 | + |
| 229 | +### 4.2 查詢成績 |
| 230 | + |
| 231 | +```mermaid |
| 232 | +sequenceDiagram |
| 233 | + participant Browser as Browser(app.js) |
| 234 | + participant Server as Flask(server.py) |
| 235 | + participant Fetcher as GradeFetcher(fetcher.py) |
| 236 | + participant School as School API |
| 237 | +
|
| 238 | + Browser->>Server: GET /api/structure |
| 239 | + Server-->>Browser: {structure} |
| 240 | +
|
| 241 | + Browser->>Server: POST /api/fetch (year_value, exam_value) |
| 242 | + Server->>Fetcher: fetch_grades_via_api(...) |
| 243 | + Fetcher->>School: POST GetScoreForStudentExamContent |
| 244 | + School-->>Fetcher: grades JSON |
| 245 | + Fetcher-->>Server: grades JSON |
| 246 | + Server-->>Browser: {success:true,data} |
| 247 | + Browser->>Browser: store localStorage + update dashboard |
| 248 | +``` |
| 249 | + |
| 250 | +### 4.3 分享與唯讀檢視 |
| 251 | + |
| 252 | +```mermaid |
| 253 | +sequenceDiagram |
| 254 | + participant Browser as Browser(app.js) |
| 255 | + participant Server as Flask(server.py) |
| 256 | + participant Turnstile as Cloudflare Turnstile |
| 257 | + participant FS as shared_grades folder |
| 258 | +
|
| 259 | + Browser->>Server: POST /api/share (grades + token) |
| 260 | + Server->>Turnstile: siteverify(token) |
| 261 | + Turnstile-->>Server: success |
| 262 | + Server->>FS: write <share_id>.json |
| 263 | + Server-->>Browser: {success:true,id} |
| 264 | +
|
| 265 | + Browser->>Server: GET /share/:id |
| 266 | + Server-->>Browser: index.html |
| 267 | + Browser->>Server: GET /api/share/:id |
| 268 | + Server->>FS: read <share_id>.json |
| 269 | + Server-->>Browser: {success:true,data} |
| 270 | + Browser->>Browser: 進入唯讀模式 |
| 271 | +``` |
| 272 | + |
| 273 | +## 5) 佈署與執行摘要 |
| 274 | + |
| 275 | +- `Dockerfile` 使用 `python:3.11-slim`,安裝依賴後以 `gunicorn` 啟動 `server:app`(5000 port)。 |
| 276 | +- `docker-compose.yml`: |
| 277 | + - `app` 服務掛載 `./shared_grades:/app/shared_grades`。 |
| 278 | + - 健康檢查透過 `GET /health`。 |
| 279 | + - `tunnel` 服務使用 `cloudflare/cloudflared` + `TUNNEL_TOKEN`。 |
| 280 | + |
| 281 | +## 6) 重要注意事項 |
| 282 | + |
| 283 | +- 後端將登入狀態保存在 Flask session(cookie-based session + server-side secure key)。 |
| 284 | +- `SESSION_COOKIE_SECURE=True`,部署需 HTTPS。 |
| 285 | +- `shared_grades` 檔案有背景清理機制:每 10 分鐘掃描,2 小時過期刪除。 |
| 286 | +- 前端會將最近一次查詢成績存放在 `localStorage.gradesData`,以提升重開頁面體驗。 |
0 commit comments