角色與使用情境
| 角色 | 可以做什麼 | 看不到什麼 | 主要入口 |
|---|---|---|---|
| 未登入訪客 | 看到登入提示 | 所有提案、公告、通知、留言 | 登入面板 |
| 校內一般使用者 | 看公共議題、設備提案、公告;建立提案;留言;附議公共議題 | 其他人的學生權益維護提案、其他人的通知、其他人的私有作者資料 | 提案看板、公告頁 |
| 提案作者 | 管理自己的提案;在「我的提案」看到自己發過的內容 | 其他人的私密提案 | 我的提案分頁、提案詳情 |
| 公共議題附議者 | 對公共議題附議或取消附議 | 其他使用者是否附議 | 公共議題卡片、提案詳情 |
| 管理員 | 調整提案狀態、建立公告、刪除不適合內容、看平台統計 | 管理員狀態選單、公告管理、Dashboard |
使用者功能總覽
| 功能 | 使用者會看到的行為 | 背後邏輯 |
|---|---|---|
| Google 校內登入 | 只能用已驗證的校內 Google 帳號登入 | 前端與 Cloud Functions 都檢查 Google provider、email verified、指定網域 |
| 提案分類分頁 | 公共議題、學生權益維護、設備、我的提案 | 路由 /issues/:filter 控制目前分頁,不靠單一畫面內部狀態硬切 |
| 提案看板 | 卡片與列表模式切換、搜尋、分頁 | Firestore 即時訂閱加上前端分頁與搜尋狀態 |
| 新增提案 | 填標題、內容、分類,可插入圖片 | 送出時統一呼叫 backendAction,由後端驗證與寫入 |
| 公共議題附議 | 按下附議後數字立即更新,失敗會回復 | 前端 optimistic UI,後端 transaction 防止重複附議 |
| 提案留言 | 在提案詳情內留言,可附圖片 | 留言讀寫都經 Cloud Functions,Firestore client 不直接讀寫留言集合 |
| 狀態通知 | 狀態變更、留言、附議達標會通知 | user\_notifications 只讓收件人讀取,最多保留最新 15 則 |
| 公告 | 校內使用者可看公告、留言、按讚 | 公告由管理員建立,互動也走 callable |
| 管理員 Dashboard | 看使用者、提案、留言、附議與分類統計 | 後端累加平台統計,Dashboard callable 僅 admin 可取 |
| Notion 備份 | 提案、公告與互動事件可同步到 Notion | Cloud Tasks 排隊,失敗可重試並記錄狀態 |
提案分類與差異
| 分類 | 適合內容 | 誰看得到 | 是否可附議 | 作者顯示 | 期限邏輯 |
|---|---|---|---|---|---|
| 公共議題 | 影響多數學生的制度、環境、流程問題 | 校內使用者都可看 | 是,目標 50 人 | 一般使用者看不到作者真實資訊;本人與管理員可看私有作者資料 | 14 天內未達 50 人會自動轉為「未通過」;達標後有 7 天回覆期限 |
| 學生權益維護 | 個別權益、敏感事件、需要私下處理的問題 | 只有作者本人與管理員可看 | 否 | 只在本人與管理員情境可辨識 | 建立後直接進入待回覆流程,回覆期限 7 天 |
| 設備 | 設備損壞、空間問題、修繕需求 | 校內使用者都可看 | 否 | 可顯示校內作者資訊 | 建立後直接進入待回覆流程,回覆期限 7 天 |
| 我的提案 | 使用者自己發出的全部提案 | 只列出目前登入者自己的提案 | 依原分類規則 | 使用者可以知道哪些是自己送出的 | 用路由 filter 顯示,不是獨立分類 |
提案狀態
| 狀態代碼 | 顯示文字 | 代表意義 | 誰能造成狀態變化 |
|---|---|---|---|
pending | 未回覆 | 新提案等待管理方處理;公共議題可能仍在附議期 | 使用者建立提案後產生 |
processing | 處理中 | 管理方已接手處理 | 管理員 |
auto\-rejected | 未通過 | 公共議題附議期到期但未達門檻 | 排程函式或維護 callable |
infeasible | 無法實行 | 管理方判定目前無法執行 | 管理員 |
completed | 已完成 | 已處理或完成回覆 | 管理員 |
提案生命週期
| 階段 | 公共議題 | 學生權益維護 | 設備 |
|---|---|---|---|
| 建立 | 檢查標題、內容、分類、圖片;產生附議期限 | 檢查標題、內容、分類、圖片;標記私密可見性 | 檢查標題、內容、分類、圖片 |
| 初始狀態 | pending | pending | pending |
| 附議 | 可附議,可取消,重複附議被後端拒絕 | 不開放 | 不開放 |
| 達標 | 達 50 人後記錄 support\_met\_at,設定回覆期限 | 不適用 | 不適用 |
| 未達標 | 14 天後自動轉為 auto\-rejected | 不適用 | 不適用 |
| 管理員處理 | 可改為處理中、無法實行、已完成 | 可改為處理中、無法實行、已完成 | 可改為處理中、無法實行、已完成 |
| 通知 | 留言、狀態更新、附議達標都可通知 | 留言與狀態更新通知作者 | 留言與狀態更新通知作者 |
| 備份 | Notion 事件同步 | Notion 事件同步,但依資料設計保護私密資訊 | Notion 事件同步 |
權限模型總表
| 動作 | 未登入 | 校內使用者 | 作者本人 | 管理員 |
|---|---|---|---|---|
| 讀取公共議題 | 否 | 是 | 是 | 是 |
| 讀取學生權益維護提案 | 否 | 否 | 是 | 是 |
| 讀取設備提案 | 否 | 是 | 是 | 是 |
| 新增提案 | 否 | 是 | 是 | 是 |
| 刪除自己的提案 | 否 | 否 | 否 | 是 |
| 調整提案狀態 | 否 | 否 | 否 | 是 |
| 附議公共議題 | 否 | 是 | 是 | 是 |
| 附議學生權益維護或設備 | 否 | 否 | 否 | 否 |
| 讀取留言 | 否 | 透過 callable 依提案權限讀取 | 是 | 是 |
| 新增留言 | 否 | 透過 callable 依提案權限新增 | 是 | 是 |
| 讀取通知 | 否 | 只能讀自己的 | 只能讀自己的 | 只能讀自己的 |
| 建立公告 | 否 | 否 | 否 | 是 |
| 讀取 Dashboard | 否 | 否 | 否 | 是 |
登入與校內身分驗證
| 檢查項目 | 前端用途 | 後端用途 | 原因 |
|---|---|---|---|
| Google 登入 provider | 引導使用者用正確方式登入 | validateAuthAndDomain 拒絕非 Google provider | 避免用其他 provider 繞過校內帳號政策 |
| Email 已驗證 | 顯示登入狀態前先驗證 | callable 每次操作都驗證 token | 避免未驗證 email 被當成有效校內身分 |
| Email 網域 | 顯示校內網域限制 | 比對 ALLOWED\_DOMAIN | 平台只服務指定學校網域 |
| 管理員 email | 顯示管理 UI | 比對 ADMIN\_EMAILS | 管理員身份不靠前端判斷 |
| UID | 訂閱自己的通知與提案 | 寫入作者、附議、留言紀錄 | 保持每個操作可追蹤 |
資料讀寫邊界
| 資料 | 前端可直接讀嗎 | 前端可直接寫嗎 | 實際寫入方式 |
|---|---|---|---|
issues | 可讀,但學生權益維護只限作者與管理員 | 否 | backendAction\.createIssue、deleteIssue、moderateIssueStatus |
issues/\{issueId\}/votes/\{uid\} | 只可讀自己的投票狀態或管理員讀取 | 否 | backendAction\.toggleSupport、removeSupport |
private\_issue\_authors | 只限作者與管理員 | 否 | 建立公共議題時由後端同步寫入 |
issue\_comments | 否 | 否 | backendAction\.listComments、createComment、deleteComment |
announcements | 校內使用者可讀 | 否 | 管理員 callable 建立、更新、刪除 |
announcements/\{id\}/likes/\{uid\} | 只可讀自己的讚狀態或管理員讀取 | 否 | backendAction\.toggleAnnouncementLike |
announcement\_comments | 否 | 否 | 公告留言 callable |
uploads | 否 | 否 | 圖片上傳、解析、刪除 callable |
user\_notifications | 只可讀自己的通知 | 否 | 後端事件建立通知 |
platform\_stats | 否 | 否 | 管理員 Dashboard callable |
為什麼所有寫入都走 Cloud Functions
| 風險 | 如果讓前端直接寫入會怎樣 | 現在的做法 |
|---|---|---|
| 偽造作者 | 使用者可能改掉 author\_uid 或管理欄位 | 後端從 Firebase Auth token 取 uid/email/name |
| 偽造管理員 | 前端 UI 隱藏不代表安全 | 後端每次 callable 都比對 ADMIN\_EMAILS |
| 重複附議 | 同一人可能送出多次寫入 | Firestore transaction 與 votes 子集合防重 |
| 私密提案外洩 | client query 寫錯就可能列出敏感資料 | Firestore Rules 先擋,再由查詢服務按分類取資料 |
| 圖片任意讀寫 | Storage URL 若公開會擴散 | Storage 規則拒絕 uploads 直接讀寫,只用短效 signed URL |
| Notion 同步失敗 | 直接同步容易讓使用者操作卡住 | Cloud Tasks 排隊、重試、限速 |
圖片與 Markdown
| 項目 | 設計 |
|---|---|
| 圖片格式 | 上傳前端先壓縮,後端接受 WebP |
| 單張大小 | 後端限制最大 2 MB |
| 預覽圖 | 後端用 sharp 產生最大 480px preview |
| Markdown 語法 | 支援 `![alt |
| Storage 權限 | /uploads/\*\* 不允許前端直接 read/write/delete |
| 圖片讀取 | 前端解析 srp\-upload:// 後,透過 callable 換短效 signed URL |
| signed URL 有效期 | 15 分鐘 |
| 可見性 | school 或 private,由 registry 與權限 helper 判斷 |
| 刪除 | 刪提案、留言或公告時,後端清理關聯圖片 |
公共議題附議邏輯
| 規則 | 說明 |
|---|---|
| 只限公共議題 | 學生權益維護與設備不需要附議流程 |
| 目標人數 | PUBLIC\_SUPPORT\_GOAL = 50 |
| 附議期限 | 建立後 14 天 |
| 達標效果 | 寫入 support\_met\_at,設定 7 天回覆期限,通知作者 |
| 未達標效果 | 排程每 60 分鐘掃描,到期且未達標轉成 auto\-rejected |
| 防止重複 | votes 子集合以 uid 當文件 ID,交易內同步 support\_count |
| 使用者狀態 | users\.supported\_issue\_ids 保留使用者目前附議過的提案 ID |
通知與公告
| 模組 | 功能 | 權限 |
|---|---|---|
| 通知鈴鐺 | 顯示最新通知與未讀數 | 只讀目前登入者的通知 |
| 提案留言通知 | 有人留言時通知相關使用者 | 後端決定收件人 |
| 狀態變更通知 | 管理員改狀態後通知作者 | 後端建立 |
| 附議達標通知 | 公共議題達標時通知作者 | 後端建立 |
| 公告列表 | 校內使用者可讀全站公告 | 管理員才可新增、編輯、刪除 |
| 公告留言與讚 | 校內使用者互動 | 透過 callable 寫入,非 client 直寫 |
Notion 備份邏輯
| 事件類型 | 備份內容 | 同步方式 |
|---|---|---|
| 提案建立 | 標題、分類、狀態、作者資訊、內容 | 建立或更新 Notion page |
| 提案狀態變更 | 舊狀態、新狀態、時間 | 追加事件紀錄 |
| 提案留言 | 留言摘要、圖片 block | 追加到 Notion |
| 公告建立或更新 | 公告標題、內容、互動狀態 | 建立或更新 Notion page |
| 公告留言或讚 | 互動事件 | 追加到 Notion |
| 刪除事件 | 將 Notion 對應頁或事件標記處理 | 透過 event handler |
Notion 同步不直接卡住使用者操作。後端會先寫入 Firestore,再把同步事件丟進 Cloud Tasks。任務最多重試 5 次,限速為每秒 1 個、同時最多 2 個,避免打爆 Notion API 或因短暫失敗造成資料遺失。
主要資料集合
| Collection | 主要用途 | 讀取者 | 寫入者 |
|---|---|---|---|
issues | 提案主資料 | 校內使用者依分類與作者權限讀取 | Cloud Functions |
private\_issue\_authors | 公共議題私有作者資訊 | 作者本人、管理員 | Cloud Functions |
issue\_comments | 提案留言 | 只能透過 callable 取得 | Cloud Functions |
announcements | 公告 | 校內使用者 | Cloud Functions |
announcement\_comments | 公告留言 | 只能透過 callable 取得 | Cloud Functions |
users | 使用者附議 ID、頭像快取等 | 本人 | Cloud Functions |
uploads | 圖片 registry | 不開放 client 直接讀 | Cloud Functions |
user\_notifications | 站內通知 | 收件人本人 | Cloud Functions |
platform\_stats | 平台成果統計 | 管理員 callable | Cloud Functions |
notion\_pages / notion\_issue\_pages | Firestore 與 Notion 頁面對應 | 後端內部 | Cloud Functions |
notion\_sync\_jobs | Notion 同步任務狀態 | 後端內部 | Cloud Functions |
rate\_limits | 發文與留言限流 bucket | 後端內部 | Cloud Functions |
使用者常見問題
| 問題 | 回答 |
|---|---|
| 為什麼我看不到別人的學生權益維護提案? | 這個分類是私密用途,只能看到自己發出的提案;如果目前是空的,代表你還沒有在這個分類送出提案。 |
| 為什麼公共議題看不到作者? | 公共議題希望降低提案者被針對的風險,因此一般使用者看議題內容與附議狀態即可,作者資訊由後端保存給本人與管理員使用。 |
| 為什麼設備提案不能附議? | 設備問題通常是明確修繕或回報流程,不需要用附議達標來判斷是否處理。 |
| 為什麼圖片有時需要重新載入? | 圖片不是永久公開 URL,而是短效 signed URL;過期後前端會再向後端解析。 |
| 為什麼送出失敗時圖片不會留下公開連結? | 圖片先進 registry,送出失敗或刪除內容時後端會清理,降低孤兒檔案與外洩風險。 |
| 管理員是不是可以在前端直接改資料? | 不行。前端只能呼叫後端 action,真正的 admin 判斷在 Cloud Functions。 |
| 提案內容支援 Markdown 嗎? | 支援,並會用 DOMPurify 過濾渲染後 HTML,降低 XSS 風險。 |
前端模組責任
| 模組 | 責任 |
|---|---|
src/router | 將提案、公告、管理員頁面路由化;提案 filter 由 URL 控制 |
src/views | 頁面入口,只組合資料流程與主要元件 |
src/components | UI 呈現與互動元件,避免直接寫 Firestore 邏輯 |
src/components/ui | 純展示共用元件,不 import composable 或 service |
src/composables | 狀態、表單、訂閱、分頁、搜尋、對話框、通知等邏輯 |
src/services | Firestore 讀取與 Cloud Functions callable 包裝 |
src/lib | 無 Vue 相依的純函式,例如 Markdown 圖片解析、日期格式化、搜尋正規化 |
src/constants | 分類、狀態、路由選項 |
src/types | TypeScript 型別,作為資料模型文件 |
後端模組責任
| 模組 | 責任 |
|---|---|
functions/src/index\.ts | 只匯出部署入口 |
functions/src/entrypoints/backend\-action\.ts | 單一 callable router,依 action 分派 handler |
functions/src/core | Firebase Admin 初始化、環境變數、身分驗證、限流 |
functions/src/issues | 提案建立、刪除、留言、附議、狀態管理、過期同步 |
functions/src/announcements | 公告 CRUD、公告留言、讚、Notion retry |
functions/src/uploads | 圖片建立、解析、刪除、Storage 與 registry 權限 |
functions/src/notifications | 通知建立與已讀時間 |
functions/src/stats | 平台統計與 Dashboard |
functions/src/notion | Notion client、Markdown 轉 block、事件同步與 page mapping |
functions/src/scheduled | 每小時自動駁回、每日清理 Secret Manager 舊版本 |
一句話流程圖
flowchart TD
A["校內 Google 登入"] --> B["選擇提案分類"]
B --> C["送出提案與圖片"]
C --> D["Cloud Functions 驗證與寫入"]
D --> E{"公共議題?"}
E -->|是| F["14 天附議期"]
F --> G{"達 50 人?"}
G -->|是| H["設定 7 天回覆期限並通知"]
G -->|否| I["排程自動轉未通過"]
E -->|否| J["直接等待管理員回覆"]
H --> K["管理員處理狀態"]
I --> K
J --> K
K --> L["通知使用者並同步 Notion"]