11 / WORK / CASE STUDY
股東常會電子投票系統
股東常會的電子投票平台:OTP 登入、會議與議程管理、選舉制議案與迴避條款、SHA-256 雜湊鏈稽核;9 天獨立建置,目前處於驗收與修復階段。
- TYPE
- FULL STACK
- SCOPE
- 公司內部 · 驗收修復階段
- STACK
- DJANGO · DRF · MYSQL · REACT · TYPESCRIPT · JWT
- LINKS
- 公司內部專案(不公開)
01 / PROBLEM
台灣中小型公司的股東常會,多半仰賴現場紙本投票與人工計票:流程耗時、結果難以追溯,也沒有可稽核的紀錄。這套系統讓股東以 OTP 快速登入、依身分(股東/管理員/來賓/員工)分權操作,支援一般議案與含法定迴避條款的選舉制議案;設計的優先順序放在個資保護與稽核鏈完整性這類合規要求,而不是大規模併發。
02 / CONSTRAINTS
- 公司內部委託專案,程式碼不公開。
- 單人、時程壓縮:11 個 commit 集中在 9 天內,其中 8 個在最後一天完成安全與效能加固。
- 個資保護是硬性要求(對應 ISO 27001 控制項):身分證後四碼、電話後三碼只能以雜湊儲存,稽核日誌的 IP 與 User-Agent 必須遮罩。
- 不引入 Redis/Celery 等外部基礎設施;OTP 目前以 console 模擬發送、尚未接正式簡訊/Email 服務——如實記錄。
03 / ARCHITECTURE
Django 5.2 + DRF 單體:5 個 app(accounts、meetings、voting、otp_module、audits)、11 個資料模型、5 組資源路由加 21 個自訂 action,共 51 個實測 API 端點;前端 React 19 + TypeScript + MUI,11 個路由頁面、約 4,500 行。刻意不拆微服務——單人開發、業務規模是單一公司的股東會,投票與稽核鏈需要同一個交易邊界,拆分只會增加維運成本而無實益。
認證為 JWT(含 token 黑名單)加 OTP 雙因子,另整合 Keycloak SSO(員工/股東各一組 realm、環境變數開關);權限以三個自訂 Permission 類別分流。四個核心模型套用軟刪除 Mixin(支援 restore 與 hard delete);throttle 依端點分級——OTP 請求 5/min、OTP 驗證 10/min、投票 20/min;CI 為 GitHub Actions 四階段(後端 lint → 後端測試 → 前端 lint → 前端 build),部署以 gunicorn + Nginx。
04 / RESPONSIBILITIES
獨立開發:11 個 commit 全數出自我(2026/04/04–04/12),涵蓋 Django 後端、React 前端、資料模型、個資保護與稽核鏈設計、查詢效能優化與 CI 建置。
05 / CHALLENGES → SOLUTIONS
CHALLENGE
稽核日誌必須「不可竄改」,但投票內容(誰投了什麼)是高敏感個資——把投票 payload 直接存進日誌,等於製造第二個個資外洩點。
SOLUTION
所有日誌走 create_log() 單一入口:寫入前自動遮罩敏感欄位(身分證/電話/OTP/token 一律標記 ***,投票內容只留議案類型與完成旗標)、IP 與 User-Agent 遮罩後才落地、依動作類型自動分級嚴重度;每筆記錄再以 SHA-256 串接前一筆的 hash 形成雜湊鏈——任何歷史記錄被竄改,後續整條鏈就會斷裂、可被檢測。
CHALLENGE
身分證後四碼與電話後三碼原本以明碼存庫,作為登入比對依據——資料庫一旦外洩就是直接可識別的個資;而且表裡已有既存明碼資料,不能砍掉重灌。
SOLUTION
新增 hash_pii()(salt + SHA-256),用 Django 資料遷移把既有明碼欄位就地轉成雜湊,比對全面改走雜湊;salt 從環境變數讀取、未設定直接拋錯而非靜默用預設值。同一模式後續重複用在股票代號欄位,成為可重用的 PII 遷移範式。
CHALLENGE
議程投票統計原本用 8 個獨立查詢分別算票數與加權股數:議程與股東越多、查詢數線性增加,而且沒有任何查詢數的約束或測試。
SOLUTION
改用 Case/When 條件聚合,把 8 次查詢合併成 1 次 aggregate 同時算出票數與加權股數;並補上以 CaptureQueriesContext 斷言查詢數 ≤ 10 的效能測試——讓「效能是否退化」變成 CI 會直接擋下的條件,而不是上線後靠感覺發現。
CHALLENGE
驗收複驗時發現:原始碼定義了 225 個測試,實際只有 215 個被收集、其中 9 個失敗;另有 10 個測試因為 accounts/tests/ 目錄缺 __init__.py、又與 accounts/tests.py 撞名,寫完後從未被執行過,也沒有任何提示。
SOLUTION
完成診斷與根因定位:9 個失敗可歸責到三次各自獨立的改動(登入契約改版、日誌訊息中文化、random→secrets 安全重構),每一次都沒同步更新對應測試;撞名則是測試組織的結構性缺陷。目前狀態如實記錄:206/215 通過、9 個待修、10 個待重新納入收集——修復列管中。
06 / SYSTEM FACTS
51
API 端點實測
11
資料模型(5 app)
206/215
測試通過(9 個待修)
9
天,從零到驗收
07 / LESSONS
沒有查詢數斷言的效能優化,等於沒有驗證:8 次查詢併成 1 次之後補上 ≤10 的斷言,日後任何人把聚合改回逐項查詢,CI 會直接失敗。這件事應該在最初設計時就連測試一起做,而不是優化時才回頭補。
「測試都寫了」不等於「測試都在跑」,更不等於「跑的測試還對得上程式碼」:10 個測試因缺 __init__.py 與撞名而從未執行、7 個測試因登入契約改版而過期。重來會在 CI 加一道「定義數 vs 實際收集數」的核對,並把「改端點契約必跑對應測試」列為明確流程。
NEXT
AI 開發工作流