2668 lines
89 KiB
Plaintext
2668 lines
89 KiB
Plaintext
夥伴對齊系統 - 軟體設計文件 (SDD) v2.0
|
||
文件修訂記錄
|
||
版本
|
||
日期
|
||
修改者
|
||
變更內容
|
||
1.0
|
||
2025-10-15
|
||
-
|
||
初版SDD文件建立
|
||
2.0
|
||
2025-10-15
|
||
-
|
||
新增評分競賽功能與權限管理系統
|
||
|
||
? v2.0 新增功能概覽
|
||
1. 評分競賽儀表板
|
||
* 個人積分即時顯示
|
||
* 部門排名百分比計算
|
||
* 視覺化競賽排行榜
|
||
* 成就徽章系統
|
||
* 積分趨勢圖表
|
||
2. 完整權限管理系統
|
||
* 三層角色架構(超級管理員/管理者/使用者)
|
||
* 使用者認證與授權
|
||
* 細粒度權限控制
|
||
* 操作日誌追蹤
|
||
* 安全的Session管理
|
||
|
||
1. 文件資訊
|
||
版本: 2.0
|
||
最後更新: 2025年10月
|
||
文件狀態: 修訂版
|
||
專案類型: 內部管理系統
|
||
|
||
2. 執行摘要
|
||
2.1 專案目標
|
||
建立一套完整的夥伴能力評估與對齊管理平台,透過視覺化拖拉介面簡化能力評估流程,並整合STAR回饋機制、遊戲化積分競賽系統與完整權限管理,提升組織內部人才發展與管理效率。
|
||
2.2 核心價值主張
|
||
* 效率提升: 拖拉式操作減少50%以上評估時間
|
||
* 結構化回饋: STAR框架確保回饋品質與可追蹤性
|
||
* 遊戲化激勵: ?? 競賽排名與百分比顯示促進良性競爭
|
||
* 安全管控: ?? 完整的權限管理確保資料安全
|
||
* 資料驅動: 完整的評估資料分析與匯出功能
|
||
2.3 關鍵成功指標
|
||
* 評估完成率提升30%
|
||
* 回饋品質標準化達90%
|
||
* 系統使用率達80%以上
|
||
* 用戶活躍度提升40%(遊戲化激勵)
|
||
* 安全事件零發生(權限管理)
|
||
|
||
3. 系統架構設計
|
||
3.1 技術架構概覽
|
||
┌─────────────────────────────────────────┐
|
||
│ 前端層 (Presentation) │
|
||
│ HTML5 + Bootstrap 5 + JavaScript │
|
||
│ + Chart.js (圖表) + 動態儀表板 │
|
||
└─────────────┬───────────────────────────┘
|
||
│ HTTP/REST API + JWT Token
|
||
┌─────────────▼───────────────────────────┐
|
||
│ 應用層 (Application) │
|
||
│ Python Flask + SQLAlchemy │
|
||
│ + Flask-Login (認證) │
|
||
│ + Flask-JWT-Extended (Token) │
|
||
│ + 權限裝飾器 (Authorization) │
|
||
└─────────────┬───────────────────────────┘
|
||
│ ORM
|
||
┌─────────────▼───────────────────────────┐
|
||
│ 資料層 (Data) │
|
||
│ MySQL 5.7+ / 8.0+ │
|
||
│ + 用戶認證表 + 權限控制表 │
|
||
└─────────────────────────────────────────┘
|
||
3.2 系統分層說明
|
||
3.2.1 前端層(新增功能)
|
||
* 職責: 使用者介面、互動邏輯、資料展示、即時儀表板
|
||
* 技術選型:
|
||
o Bootstrap 5: 響應式UI框架
|
||
o 原生JavaScript: 拖拉功能、表單驗證
|
||
o Chart.js: 積分趨勢圖表與排名視覺化
|
||
o Counter.js: 數字動畫效果
|
||
o HTML5 Drag & Drop API: 能力評估拖拉介面
|
||
* 關鍵模組:
|
||
o app.js: 全域工具函數與API通訊
|
||
o assessment.js: 能力評估拖拉邏輯
|
||
o admin.js: 後台管理介面邏輯
|
||
o dashboard.js: ?? 個人儀表板與競賽排名
|
||
o auth.js: ?? 登入/登出/權限驗證
|
||
3.2.2 應用層(新增功能)
|
||
* 職責: 業務邏輯、資料處理、API路由、認證授權
|
||
* 技術選型:
|
||
o Flask 2.x: 輕量化Web框架
|
||
o SQLAlchemy: ORM資料庫抽象層
|
||
o Flask-Login: 用戶Session管理
|
||
o Flask-JWT-Extended: Token認證
|
||
o Flask-Bcrypt: 密碼加密
|
||
o Flask-CORS: 跨域請求處理
|
||
o pandas + openpyxl: 資料匯出
|
||
* 安全機制:
|
||
o 環境變數管理敏感資訊
|
||
o CORS跨域限制
|
||
o 輸入驗證與SQL注入防護
|
||
o 錯誤處理機制
|
||
o 密碼雜湊(Bcrypt)
|
||
o JWT Token認證
|
||
o 角色權限檢查裝飾器
|
||
o 操作日誌記錄
|
||
3.2.3 資料層(新增功能)
|
||
* 職責: 資料持久化、查詢優化、權限資料管理
|
||
* 技術選型: MySQL 5.7+ / 8.0+
|
||
* 關鍵特性:
|
||
o 事務處理保證資料一致性
|
||
o 索引優化查詢效能
|
||
o JSON欄位儲存彈性資料
|
||
o 用戶認證資料加密儲存
|
||
o 權限資料關聯查詢優化
|
||
|
||
4. 資料庫設計(含新增表格)
|
||
4.1 ER關係圖(v2.0)
|
||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||
│ users │──────│ user_roles │──────│ roles │
|
||
│ (用戶) │ M:N │ (用戶角色) │ M:N │ (角色) │
|
||
└──────┬───────┘ └──────────────┘ └──────┬───────┘
|
||
│ │
|
||
│ │ M:N
|
||
│ ┌──────▼────────┐
|
||
│ │role_permissions│
|
||
│ │ (角色權限) │
|
||
│ └──────┬────────┘
|
||
│ │
|
||
│ ┌──────▼────────┐
|
||
│ │ permissions │
|
||
│ │ (權限) │
|
||
│ └───────────────┘
|
||
│
|
||
├─────────────┐
|
||
│ │
|
||
┌───▼────┐ ┌───▼─────────┐ ┌──────────────────┐
|
||
│ audit_ │ │ assessments │ │ capabilities │
|
||
│ logs │ │ (能力評估) │ │ (能力項目) │
|
||
│(日誌) │ └─────────────┘ └──────────────────┘
|
||
└────────┘
|
||
│
|
||
▼
|
||
┌─────────────────┐ ┌──────────────────┐
|
||
│ star_feedbacks │─────?│ employee_points │
|
||
│ (STAR回饋) │ │ (員工積分) │
|
||
└─────────────────┘ └──────────┬───────┘
|
||
│ │
|
||
│ ▼
|
||
│ ┌──────────────────┐
|
||
└──────────────?│ monthly_rankings │
|
||
│ (月度排名) │
|
||
└──────────────────┘
|
||
4.2 新增資料表結構
|
||
4.2.1 users (用戶表) ??
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
id
|
||
INT
|
||
主鍵
|
||
PK, AUTO_INCREMENT
|
||
username
|
||
VARCHAR(50)
|
||
用戶名稱
|
||
NOT NULL, UNIQUE
|
||
email
|
||
VARCHAR(100)
|
||
電子郵件
|
||
NOT NULL, UNIQUE
|
||
password_hash
|
||
VARCHAR(255)
|
||
密碼雜湊
|
||
NOT NULL
|
||
full_name
|
||
VARCHAR(100)
|
||
真實姓名
|
||
NOT NULL
|
||
department
|
||
VARCHAR(100)
|
||
部門
|
||
NOT NULL
|
||
position
|
||
VARCHAR(100)
|
||
職位
|
||
NOT NULL
|
||
employee_id
|
||
VARCHAR(50)
|
||
員工編號
|
||
UNIQUE
|
||
is_active
|
||
BOOLEAN
|
||
帳號狀態
|
||
DEFAULT TRUE
|
||
last_login_at
|
||
DATETIME
|
||
最後登入時間
|
||
NULL
|
||
created_at
|
||
DATETIME
|
||
建立時間
|
||
DEFAULT CURRENT_TIMESTAMP
|
||
updated_at
|
||
DATETIME
|
||
更新時間
|
||
ON UPDATE CURRENT_TIMESTAMP
|
||
索引策略:
|
||
* idx_username: (username) UNIQUE
|
||
* idx_email: (email) UNIQUE
|
||
* idx_department: (department)
|
||
* idx_employee_id: (employee_id) UNIQUE
|
||
密碼規則:
|
||
* 最少8字元
|
||
* 包含大小寫字母
|
||
* 包含數字
|
||
* 包含特殊符號
|
||
4.2.2 roles (角色表) ??
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
id
|
||
INT
|
||
主鍵
|
||
PK, AUTO_INCREMENT
|
||
name
|
||
VARCHAR(50)
|
||
角色名稱
|
||
NOT NULL, UNIQUE
|
||
display_name
|
||
VARCHAR(100)
|
||
顯示名稱
|
||
NOT NULL
|
||
description
|
||
TEXT
|
||
角色說明
|
||
NULL
|
||
level
|
||
INT
|
||
角色層級
|
||
NOT NULL
|
||
is_active
|
||
BOOLEAN
|
||
是否啟用
|
||
DEFAULT TRUE
|
||
created_at
|
||
DATETIME
|
||
建立時間
|
||
DEFAULT CURRENT_TIMESTAMP
|
||
預設角色:
|
||
INSERT INTO roles (name, display_name, description, level) VALUES
|
||
('super_admin', '超級管理員', '系統最高權限,可管理所有功能與用戶', 100),
|
||
('admin', '管理者', '部門管理權限,可管理部門內用戶與資料', 50),
|
||
('user', '一般使用者', '基本使用權限,可查看個人資料與排名', 10);
|
||
角色層級說明:
|
||
* Level 100: 超級管理員
|
||
* Level 50: 管理者
|
||
* Level 10: 一般使用者
|
||
4.2.3 permissions (權限表) ??
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
id
|
||
INT
|
||
主鍵
|
||
PK, AUTO_INCREMENT
|
||
name
|
||
VARCHAR(100)
|
||
權限名稱
|
||
NOT NULL, UNIQUE
|
||
display_name
|
||
VARCHAR(100)
|
||
顯示名稱
|
||
NOT NULL
|
||
resource
|
||
VARCHAR(50)
|
||
資源類型
|
||
NOT NULL
|
||
action
|
||
VARCHAR(50)
|
||
動作
|
||
NOT NULL
|
||
description
|
||
TEXT
|
||
權限說明
|
||
NULL
|
||
created_at
|
||
DATETIME
|
||
建立時間
|
||
DEFAULT CURRENT_TIMESTAMP
|
||
權限命名規範: resource:action
|
||
* 範例: user:create, assessment:read, report:export
|
||
預設權限清單:
|
||
-- 用戶管理權限
|
||
INSERT INTO permissions (name, display_name, resource, action, description) VALUES
|
||
('user:create', '建立用戶', 'user', 'create', '建立新用戶帳號'),
|
||
('user:read', '查看用戶', 'user', 'read', '查看用戶資料'),
|
||
('user:update', '更新用戶', 'user', 'update', '更新用戶資料'),
|
||
('user:delete', '刪除用戶', 'user', 'delete', '刪除用戶帳號'),
|
||
('user:manage_roles', '管理角色', 'user', 'manage_roles', '分配用戶角色'),
|
||
|
||
-- 評估管理權限
|
||
('assessment:create', '建立評估', 'assessment', 'create', '建立能力評估'),
|
||
('assessment:read', '查看評估', 'assessment', 'read', '查看評估資料'),
|
||
('assessment:read_all', '查看所有評估', 'assessment', 'read_all', '查看所有部門評估'),
|
||
('assessment:update', '更新評估', 'assessment', 'update', '更新評估資料'),
|
||
('assessment:delete', '刪除評估', 'assessment', 'delete', '刪除評估記錄'),
|
||
|
||
-- STAR回饋權限
|
||
('feedback:create', '建立回饋', 'feedback', 'create', '建立STAR回饋'),
|
||
('feedback:read', '查看回饋', 'feedback', 'read', '查看回饋資料'),
|
||
('feedback:read_all', '查看所有回饋', 'feedback', 'read_all', '查看所有部門回饋'),
|
||
|
||
-- 排名權限
|
||
('ranking:read', '查看排名', 'ranking', 'read', '查看個人排名'),
|
||
('ranking:read_department', '查看部門排名', 'ranking', 'read_department', '查看部門排名'),
|
||
('ranking:read_all', '查看所有排名', 'ranking', 'read_all', '查看全公司排名'),
|
||
('ranking:calculate', '計算排名', 'ranking', 'calculate', '手動觸發排名計算'),
|
||
|
||
-- 報表權限
|
||
('report:export', '匯出報表', 'report', 'export', '匯出資料報表'),
|
||
('report:export_all', '匯出所有報表', 'report', 'export_all', '匯出全公司報表'),
|
||
|
||
-- 系統管理權限
|
||
('system:config', '系統設定', 'system', 'config', '修改系統設定'),
|
||
('system:logs', '查看日誌', 'system', 'logs', '查看系統操作日誌'),
|
||
('system:backup', '資料備份', 'system', 'backup', '執行資料備份');
|
||
4.2.4 role_permissions (角色權限關聯表) ??
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
id
|
||
INT
|
||
主鍵
|
||
PK, AUTO_INCREMENT
|
||
role_id
|
||
INT
|
||
角色ID
|
||
FK → roles.id
|
||
permission_id
|
||
INT
|
||
權限ID
|
||
FK → permissions.id
|
||
created_at
|
||
DATETIME
|
||
建立時間
|
||
DEFAULT CURRENT_TIMESTAMP
|
||
索引策略:
|
||
* unique_role_permission: (role_id, permission_id) UNIQUE
|
||
* idx_role_id: (role_id)
|
||
* idx_permission_id: (permission_id)
|
||
預設角色權限配置:
|
||
# 超級管理員: 所有權限
|
||
super_admin_permissions = [所有權限]
|
||
|
||
# 管理者: 部門管理權限
|
||
admin_permissions = [
|
||
'user:read', 'user:update',
|
||
'assessment:create', 'assessment:read', 'assessment:read_all',
|
||
'assessment:update', 'assessment:delete',
|
||
'feedback:create', 'feedback:read', 'feedback:read_all',
|
||
'ranking:read', 'ranking:read_department', 'ranking:read_all',
|
||
'report:export'
|
||
]
|
||
|
||
# 使用者: 基本查看權限
|
||
user_permissions = [
|
||
'assessment:read',
|
||
'feedback:read',
|
||
'ranking:read'
|
||
]
|
||
4.2.5 user_roles (用戶角色關聯表) ??
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
id
|
||
INT
|
||
主鍵
|
||
PK, AUTO_INCREMENT
|
||
user_id
|
||
INT
|
||
用戶ID
|
||
FK → users.id
|
||
role_id
|
||
INT
|
||
角色ID
|
||
FK → roles.id
|
||
assigned_by
|
||
INT
|
||
分配者ID
|
||
FK → users.id, NULL
|
||
assigned_at
|
||
DATETIME
|
||
分配時間
|
||
DEFAULT CURRENT_TIMESTAMP
|
||
索引策略:
|
||
* unique_user_role: (user_id, role_id) UNIQUE
|
||
* idx_user_id: (user_id)
|
||
* idx_role_id: (role_id)
|
||
業務規則:
|
||
* 一個用戶可擁有多個角色
|
||
* 權限取所有角色的聯集
|
||
* 預設新用戶自動分配 user 角色
|
||
4.2.6 audit_logs (操作日誌表) ??
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
id
|
||
INT
|
||
主鍵
|
||
PK, AUTO_INCREMENT
|
||
user_id
|
||
INT
|
||
用戶ID
|
||
FK → users.id, NULL
|
||
action
|
||
VARCHAR(100)
|
||
操作動作
|
||
NOT NULL
|
||
resource_type
|
||
VARCHAR(50)
|
||
資源類型
|
||
NOT NULL
|
||
resource_id
|
||
INT
|
||
資源ID
|
||
NULL
|
||
details
|
||
JSON
|
||
操作詳情
|
||
NULL
|
||
ip_address
|
||
VARCHAR(45)
|
||
IP位址
|
||
NULL
|
||
user_agent
|
||
VARCHAR(255)
|
||
用戶代理
|
||
NULL
|
||
status
|
||
ENUM
|
||
狀態
|
||
'success', 'failed'
|
||
error_message
|
||
TEXT
|
||
錯誤訊息
|
||
NULL
|
||
created_at
|
||
DATETIME
|
||
建立時間
|
||
DEFAULT CURRENT_TIMESTAMP
|
||
索引策略:
|
||
* idx_user_id: (user_id)
|
||
* idx_action: (action)
|
||
* idx_created_at: (created_at)
|
||
* idx_resource: (resource_type, resource_id)
|
||
記錄操作類型:
|
||
* login: 登入
|
||
* logout: 登出
|
||
* create: 建立資源
|
||
* read: 查看資源
|
||
* update: 更新資源
|
||
* delete: 刪除資源
|
||
* export: 匯出資料
|
||
* permission_change: 權限變更
|
||
4.3 原有資料表更新
|
||
4.3.1 employee_points (員工積分表) - 新增欄位
|
||
新增欄位:
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
user_id
|
||
INT
|
||
關聯用戶ID
|
||
FK → users.id, NULL
|
||
department_rank
|
||
INT
|
||
部門排名
|
||
NULL
|
||
department_percentile
|
||
DECIMAL(5,2)
|
||
部門百分比
|
||
NULL
|
||
total_rank
|
||
INT
|
||
總排名
|
||
NULL
|
||
total_percentile
|
||
DECIMAL(5,2)
|
||
總百分比
|
||
NULL
|
||
百分比計算說明:
|
||
# 百分比 = (勝過的人數 / 總人數) × 100
|
||
# 例如: 部門10人,排名第3
|
||
# department_percentile = (7 / 10) × 100 = 70.00
|
||
# 表示勝過70%的同事
|
||
4.3.2 star_feedbacks (STAR回饋表) - 新增欄位
|
||
新增欄位:
|
||
欄位名稱
|
||
資料型別
|
||
說明
|
||
約束
|
||
evaluator_user_id
|
||
INT
|
||
評分者用戶ID
|
||
FK → users.id, NULL
|
||
evaluatee_user_id
|
||
INT
|
||
受評者用戶ID
|
||
FK → users.id, NULL
|
||
|
||
5. ?? 評分競賽功能設計
|
||
5.1 個人儀表板 (Dashboard)
|
||
5.1.1 功能概述
|
||
頁面路由: /dashboard
|
||
權限: 所有已登入用戶
|
||
更新頻率: 實時(每次頁面載入時計算)
|
||
5.1.2 儀表板佈局
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ ?? 歡迎回來,張三! 最後登入: 10/15 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||
│ │ 總積分 │ │ 部門排名 │ │ 本月新增 │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ 450 pts │ │ #3 / 15 │ │ +80 pts │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ ?? Top 20% │ │ 勝過 80% │ │ ?? +21% │ │
|
||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 積分趨勢 │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ ?? [折線圖: 過去6個月積分趨勢] │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 部門排行榜 (Top 10) │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ 排名 姓名 積分 徽章 │ │
|
||
│ │ ?? 1 王五 580 ????? │ │
|
||
│ │ ?? 2 李四 520 ??? │ │
|
||
│ │ ?? 3 張三 450 ?? (你) │ │
|
||
│ │ 4 趙六 420 │ │
|
||
│ │ 5 ... │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 最近收到的回饋 (最新5筆) │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ 10/14 | 王主管 | ????? | "專案交付優秀..." │ │
|
||
│ │ 10/10 | 李經理 | ???? | "團隊合作良好..." │ │
|
||
│ │ ... │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
5.1.3 核心指標說明
|
||
1. 總積分 (Total Points)
|
||
* 顯示: 累積總積分
|
||
* 計算: 所有STAR回饋積分總和
|
||
* 顏色編碼:
|
||
o ?? 綠色: Top 20% (優秀)
|
||
o ?? 黃色: 21-50% (良好)
|
||
o ?? 橙色: 51-80% (普通)
|
||
o ?? 紅色: Bottom 20% (需努力)
|
||
2. 部門排名 (Department Rank)
|
||
* 顯示: #當前排名 / 部門總人數
|
||
* 百分比: 勝過X%的部門同事
|
||
* 計算公式:
|
||
department_percentile = ((total_count - rank) / total_count) × 100
|
||
# 例如: 15人中排名第3
|
||
# percentile = ((15 - 3) / 15) × 100 = 80%
|
||
3. 本月新增積分
|
||
* 顯示: 本月累積積分
|
||
* 趨勢: 與上月比較的百分比變化
|
||
* 圖示:
|
||
o ?? 上升
|
||
o ?? 下降
|
||
o ?? 持平
|
||
5.1.4 徽章系統
|
||
積分徽章:
|
||
徽章
|
||
名稱
|
||
條件
|
||
描述
|
||
??
|
||
金牌選手
|
||
部門第1名
|
||
部門積分冠軍
|
||
??
|
||
銀牌選手
|
||
部門第2名
|
||
部門積分亞軍
|
||
??
|
||
銅牌選手
|
||
部門第3名
|
||
部門積分季軍
|
||
??
|
||
頂尖表現
|
||
Top 10%
|
||
位於前10%
|
||
?
|
||
優秀表現
|
||
Top 20%
|
||
位於前20%
|
||
??
|
||
連勝王
|
||
連續3個月第1
|
||
持續領先
|
||
??
|
||
百分百
|
||
獲得5分評分10次
|
||
卓越品質
|
||
??
|
||
進步之星
|
||
月增長>50%
|
||
快速成長
|
||
成就徽章:
|
||
徽章
|
||
名稱
|
||
條件
|
||
描述
|
||
??
|
||
首次達成
|
||
首次獲得回饋
|
||
踏出第一步
|
||
??
|
||
堅持不懈
|
||
連續6個月有回饋
|
||
持續表現
|
||
??
|
||
團隊之星
|
||
收到跨部門回饋
|
||
跨域合作
|
||
??
|
||
全能選手
|
||
5個L等級都有能力
|
||
全面發展
|
||
5.2 競賽排行榜頁面
|
||
5.2.1 功能概述
|
||
頁面路由: /leaderboard
|
||
權限: 所有已登入用戶
|
||
篩選選項:
|
||
* 全公司排名
|
||
* 部門排名
|
||
* 月度排名
|
||
* 年度排名
|
||
5.2.2 排行榜設計
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ ?? 競賽排行榜 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 篩選: [全公司▼] [本月▼] [技術部▼] ?? 搜尋 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ ?? 本月冠軍 │ │
|
||
│ │ ┌────────────────────────────────────────────┐ │ │
|
||
│ │ │ ?? 王五 (技術部 - 資深工程師) │ │ │
|
||
│ │ │ ?? 580 積分 │ │ │
|
||
│ │ │ ????? 勝過 98% 的同事 │ │ │
|
||
│ │ └────────────────────────────────────────────┘ │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ 排名 姓名 部門 職位 積分 百分比 │ │
|
||
│ │ ─────────────────────────────────────────────── │ │
|
||
│ │ ?? 1 王五 技術部 資深工程師 580 98% ????│ │
|
||
│ │ ?? 2 李四 業務部 業務經理 520 96% ???│ │
|
||
│ │ ?? 3 張三 技術部 工程師 450 94% ?? │ │
|
||
│ │ 4 趙六 技術部 工程師 420 92% ? │ │
|
||
│ │ 5 孫七 人資部 專員 380 90% │ │
|
||
│ │ ... │ │
|
||
│ │ 42 你 技術部 工程師 150 45% │ │
|
||
│ │ ... │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ [1] [2] [3] ... [10] 下一頁 → │
|
||
└─────────────────────────────────────────────────────────┘
|
||
5.2.3 互動功能
|
||
點擊排名條目:
|
||
* 顯示該員工的詳細積分分佈
|
||
* 查看最近的STAR回饋
|
||
* 積分趨勢圖表
|
||
排名變化顯示:
|
||
* ↑ 綠色向上箭頭: 排名上升
|
||
* ↓ 紅色向下箭頭: 排名下降
|
||
* ?? 灰色橫線: 排名不變
|
||
* ?? 藍色標籤: 新進榜
|
||
排名規則:
|
||
1. 按積分降序排列
|
||
2. 積分相同時按最近回饋時間排序
|
||
3. 零積分不顯示排名,顯示為「尚未評分」
|
||
4. 可切換查看「本月」、「本季」、「本年」、「總積分」
|
||
5.3 百分比計算邏輯
|
||
5.3.1 部門百分比計算
|
||
def calculate_department_percentile(user_id, department):
|
||
"""
|
||
計算用戶在部門中的百分比排名
|
||
|
||
Args:
|
||
user_id: 用戶ID
|
||
department: 部門名稱
|
||
|
||
Returns:
|
||
dict: {
|
||
'rank': 排名,
|
||
'total': 部門總人數,
|
||
'percentile': 百分比,
|
||
'better_than_count': 勝過的人數
|
||
}
|
||
"""
|
||
# 1. 查詢部門所有員工積分
|
||
employees = db.session.query(EmployeePoints)\
|
||
.filter_by(department=department)\
|
||
.filter(EmployeePoints.total_points > 0)\
|
||
.order_by(EmployeePoints.total_points.desc())\
|
||
.all()
|
||
|
||
total_count = len(employees)
|
||
|
||
# 2. 找到用戶排名
|
||
user_rank = None
|
||
for idx, emp in enumerate(employees, start=1):
|
||
if emp.user_id == user_id:
|
||
user_rank = idx
|
||
break
|
||
|
||
if user_rank is None:
|
||
return {
|
||
'rank': 0,
|
||
'total': total_count,
|
||
'percentile': 0.0,
|
||
'better_than_count': 0
|
||
}
|
||
|
||
# 3. 計算百分比
|
||
better_than_count = total_count - user_rank
|
||
percentile = (better_than_count / total_count * 100) if total_count > 0 else 0
|
||
|
||
return {
|
||
'rank': user_rank,
|
||
'total': total_count,
|
||
'percentile': round(percentile, 2),
|
||
'better_than_count': better_than_count
|
||
}
|
||
|
||
# 使用範例
|
||
result = calculate_department_percentile(user_id=1, department="技術部")
|
||
print(f"排名: #{result['rank']} / {result['total']}")
|
||
print(f"勝過 {result['percentile']}% 的同事")
|
||
print(f"勝過 {result['better_than_count']} 人")
|
||
5.3.2 全公司百分比計算
|
||
def calculate_total_percentile(user_id):
|
||
"""
|
||
計算用戶在全公司的百分比排名
|
||
|
||
Returns:
|
||
dict: 全公司排名資訊
|
||
"""
|
||
# 查詢所有員工積分
|
||
all_employees = db.session.query(EmployeePoints)\
|
||
.filter(EmployeePoints.total_points > 0)\
|
||
.order_by(EmployeePoints.total_points.desc())\
|
||
.all()
|
||
|
||
total_count = len(all_employees)
|
||
|
||
# 找到用戶排名
|
||
user_rank = None
|
||
for idx, emp in enumerate(all_employees, start=1):
|
||
if emp.user_id == user_id:
|
||
user_rank = idx
|
||
break
|
||
|
||
if user_rank is None:
|
||
return {
|
||
'rank': 0,
|
||
'total': total_count,
|
||
'percentile': 0.0,
|
||
'tier': 'unranked'
|
||
}
|
||
|
||
# 計算百分比與層級
|
||
better_than_count = total_count - user_rank
|
||
percentile = (better_than_count / total_count * 100) if total_count > 0 else 0
|
||
|
||
# 判斷層級
|
||
if percentile >= 80:
|
||
tier = 'top_20' # 頂尖 20%
|
||
elif percentile >= 50:
|
||
tier = 'top_50' # 前 50%
|
||
elif percentile >= 20:
|
||
tier = 'middle' # 中段
|
||
else:
|
||
tier = 'bottom_20' # 後 20%
|
||
|
||
return {
|
||
'rank': user_rank,
|
||
'total': total_count,
|
||
'percentile': round(percentile, 2),
|
||
'tier': tier
|
||
}
|
||
5.3.3 定期更新排名任務
|
||
# 定時任務: 每小時更新排名與百分比
|
||
from apscheduler.schedulers.background import BackgroundScheduler
|
||
|
||
def update_all_rankings():
|
||
"""更新所有員工的排名與百分比"""
|
||
try:
|
||
# 1. 更新部門排名
|
||
departments = db.session.query(EmployeePoints.department)\
|
||
.distinct().all()
|
||
|
||
for (dept,) in departments:
|
||
employees = db.session.query(EmployeePoints)\
|
||
.filter_by(department=dept)\
|
||
.filter(EmployeePoints.total_points > 0)\
|
||
.order_by(EmployeePoints.total_points.desc())\
|
||
.all()
|
||
|
||
total = len(employees)
|
||
for rank, emp in enumerate(employees, start=1):
|
||
better_than = total - rank
|
||
percentile = (better_than / total * 100) if total > 0 else 0
|
||
|
||
emp.department_rank = rank
|
||
emp.department_percentile = round(percentile, 2)
|
||
|
||
# 2. 更新全公司排名
|
||
all_employees = db.session.query(EmployeePoints)\
|
||
.filter(EmployeePoints.total_points > 0)\
|
||
.order_by(EmployeePoints.total_points.desc())\
|
||
.all()
|
||
|
||
total = len(all_employees)
|
||
for rank, emp in enumerate(all_employees, start=1):
|
||
better_than = total - rank
|
||
percentile = (better_than / total * 100) if total > 0 else 0
|
||
|
||
emp.total_rank = rank
|
||
emp.total_percentile = round(percentile, 2)
|
||
|
||
db.session.commit()
|
||
print(f"? 排名更新完成: {total} 位員工")
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
print(f"? 排名更新失敗: {str(e)}")
|
||
|
||
# 啟動定時任務
|
||
scheduler = BackgroundScheduler()
|
||
scheduler.add_job(update_all_rankings, 'interval', hours=1)
|
||
scheduler.start()
|
||
5.4 即時通知系統
|
||
5.4.1 通知觸發時機
|
||
事件
|
||
通知內容
|
||
優先級
|
||
收到新回饋
|
||
"?? 您收到來自XXX的新回饋,獲得XX積分!"
|
||
高
|
||
排名上升
|
||
"?? 恭喜!您的排名上升至第X名!"
|
||
中
|
||
進入前X%
|
||
"?? 太棒了!您已進入部門前10%!"
|
||
高
|
||
獲得徽章
|
||
"? 解鎖新成就:XXX"
|
||
中
|
||
被超越
|
||
"?? 您的排名下降至第X名"
|
||
低
|
||
月度排名公布
|
||
"?? 本月排名已公布,您排名第X"
|
||
中
|
||
5.4.2 通知實作
|
||
# 通知資料表 (可選實作)
|
||
class Notification(db.Model):
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||
type = db.Column(db.String(50)) # 'new_feedback', 'rank_up', 'badge'
|
||
title = db.Column(db.String(100))
|
||
message = db.Column(db.Text)
|
||
is_read = db.Column(db.Boolean, default=False)
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
|
||
def send_notification(user_id, notification_type, title, message):
|
||
"""發送通知給用戶"""
|
||
notification = Notification(
|
||
user_id=user_id,
|
||
type=notification_type,
|
||
title=title,
|
||
message=message
|
||
)
|
||
db.session.add(notification)
|
||
db.session.commit()
|
||
|
||
# 可擴展: Email、Slack、推播通知等
|
||
|
||
6. ?? 權限管理系統設計
|
||
6.1 認證流程設計
|
||
6.1.1 用戶註冊流程
|
||
用戶註冊請求
|
||
↓
|
||
驗證輸入資料 (email格式、密碼強度、必填欄位)
|
||
↓
|
||
檢查用戶名/Email是否已存在
|
||
↓
|
||
密碼加密 (Bcrypt)
|
||
↓
|
||
建立用戶記錄
|
||
↓
|
||
自動分配「一般使用者」角色
|
||
↓
|
||
發送歡迎郵件 (可選)
|
||
↓
|
||
返回成功訊息
|
||
API端點: POST /api/auth/register
|
||
請求Body:
|
||
{
|
||
"username": "zhangsan",
|
||
"email": "zhangsan@company.com",
|
||
"password": "SecurePass123!",
|
||
"full_name": "張三",
|
||
"department": "技術部",
|
||
"position": "工程師",
|
||
"employee_id": "EMP001"
|
||
}
|
||
密碼驗證規則:
|
||
import re
|
||
|
||
def validate_password(password):
|
||
"""
|
||
密碼強度驗證
|
||
|
||
規則:
|
||
- 至少8字元
|
||
- 包含大寫字母
|
||
- 包含小寫字母
|
||
- 包含數字
|
||
- 包含特殊符號
|
||
"""
|
||
if len(password) < 8:
|
||
return False, "密碼至少需要8個字元"
|
||
|
||
if not re.search(r'[A-Z]', password):
|
||
return False, "密碼需包含大寫字母"
|
||
|
||
if not re.search(r'[a-z]', password):
|
||
return False, "密碼需包含小寫字母"
|
||
|
||
if not re.search(r'\d', password):
|
||
return False, "密碼需包含數字"
|
||
|
||
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
||
return False, "密碼需包含特殊符號"
|
||
|
||
return True, "密碼強度符合要求"
|
||
6.1.2 用戶登入流程
|
||
用戶登入請求 (username + password)
|
||
↓
|
||
查詢用戶是否存在
|
||
↓
|
||
驗證密碼 (Bcrypt verify)
|
||
↓
|
||
檢查帳號狀態 (is_active)
|
||
↓
|
||
查詢用戶角色與權限
|
||
↓
|
||
生成JWT Token
|
||
↓
|
||
更新最後登入時間
|
||
↓
|
||
記錄登入日誌
|
||
↓
|
||
返回Token + 用戶資訊
|
||
API端點: POST /api/auth/login
|
||
請求Body:
|
||
{
|
||
"username": "zhangsan",
|
||
"password": "SecurePass123!"
|
||
}
|
||
成功回應:
|
||
{
|
||
"success": true,
|
||
"message": "登入成功",
|
||
"data": {
|
||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
"token_type": "Bearer",
|
||
"expires_in": 3600,
|
||
"user": {
|
||
"id": 1,
|
||
"username": "zhangsan",
|
||
"email": "zhangsan@company.com",
|
||
"full_name": "張三",
|
||
"department": "技術部",
|
||
"position": "工程師",
|
||
"roles": ["user"],
|
||
"permissions": ["assessment:read", "feedback:read", "ranking:read"]
|
||
}
|
||
}
|
||
}
|
||
6.1.3 JWT Token設計
|
||
Token結構:
|
||
# Access Token (有效期1小時)
|
||
{
|
||
"user_id": 1,
|
||
"username": "zhangsan",
|
||
"roles": ["user"],
|
||
"permissions": ["assessment:read", "feedback:read"],
|
||
"exp": 1729000000, # 過期時間
|
||
"iat": 1728996400 # 簽發時間
|
||
}
|
||
|
||
# Refresh Token (有效期7天)
|
||
{
|
||
"user_id": 1,
|
||
"type": "refresh",
|
||
"exp": 1729601200,
|
||
"iat": 1728996400
|
||
}
|
||
Token實作:
|
||
from flask_jwt_extended import (
|
||
JWTManager, create_access_token, create_refresh_token,
|
||
jwt_required, get_jwt_identity, get_jwt
|
||
)
|
||
|
||
jwt = JWTManager(app)
|
||
|
||
def generate_tokens(user):
|
||
"""生成Access Token和Refresh Token"""
|
||
|
||
# 準備Token payload
|
||
additional_claims = {
|
||
"roles": [role.name for role in user.roles],
|
||
"permissions": get_user_permissions(user),
|
||
"department": user.department
|
||
}
|
||
|
||
# 生成tokens
|
||
access_token = create_access_token(
|
||
identity=user.id,
|
||
additional_claims=additional_claims,
|
||
expires_delta=timedelta(hours=1)
|
||
)
|
||
|
||
refresh_token = create_refresh_token(
|
||
identity=user.id,
|
||
expires_delta=timedelta(days=7)
|
||
)
|
||
|
||
return {
|
||
"access_token": access_token,
|
||
"refresh_token": refresh_token,
|
||
"token_type": "Bearer",
|
||
"expires_in": 3600
|
||
}
|
||
6.1.4 Token刷新流程
|
||
API端點: POST /api/auth/refresh
|
||
請求Header:
|
||
Authorization: Bearer <refresh_token>
|
||
回應:
|
||
{
|
||
"access_token": "new_access_token...",
|
||
"expires_in": 3600
|
||
}
|
||
6.2 授權檢查機制
|
||
6.2.1 權限裝飾器
|
||
from functools import wraps
|
||
from flask import jsonify
|
||
from flask_jwt_extended import get_jwt
|
||
|
||
def permission_required(*required_permissions):
|
||
"""
|
||
權限檢查裝飾器
|
||
|
||
用法:
|
||
@permission_required('user:create', 'user:update')
|
||
def create_user():
|
||
...
|
||
"""
|
||
def decorator(fn):
|
||
@wraps(fn)
|
||
@jwt_required()
|
||
def wrapper(*args, **kwargs):
|
||
# 從JWT獲取用戶權限
|
||
jwt_data = get_jwt()
|
||
user_permissions = jwt_data.get('permissions', [])
|
||
|
||
# 檢查是否擁有所需權限
|
||
has_permission = all(
|
||
perm in user_permissions
|
||
for perm in required_permissions
|
||
)
|
||
|
||
if not has_permission:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '權限不足',
|
||
'required_permissions': list(required_permissions)
|
||
}), 403
|
||
|
||
return fn(*args, **kwargs)
|
||
|
||
return wrapper
|
||
return decorator
|
||
|
||
def role_required(*required_roles):
|
||
"""
|
||
角色檢查裝飾器
|
||
|
||
用法:
|
||
@role_required('admin', 'super_admin')
|
||
def admin_function():
|
||
...
|
||
"""
|
||
def decorator(fn):
|
||
@wraps(fn)
|
||
@jwt_required()
|
||
def wrapper(*args, **kwargs):
|
||
jwt_data = get_jwt()
|
||
user_roles = jwt_data.get('roles', [])
|
||
|
||
has_role = any(role in user_roles for role in required_roles)
|
||
|
||
if not has_role:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '角色權限不足',
|
||
'required_roles': list(required_roles)
|
||
}), 403
|
||
|
||
return fn(*args, **kwargs)
|
||
|
||
return wrapper
|
||
return decorator
|
||
6.2.2 資源所有權檢查
|
||
def resource_owner_or_permission(permission):
|
||
"""
|
||
檢查是否為資源擁有者或擁有特定權限
|
||
|
||
用於: 用戶只能修改自己的資料,除非有管理權限
|
||
"""
|
||
def decorator(fn):
|
||
@wraps(fn)
|
||
@jwt_required()
|
||
def wrapper(resource_user_id, *args, **kwargs):
|
||
current_user_id = get_jwt_identity()
|
||
jwt_data = get_jwt()
|
||
user_permissions = jwt_data.get('permissions', [])
|
||
|
||
# 是資源擁有者 OR 有管理權限
|
||
is_owner = (current_user_id == resource_user_id)
|
||
has_permission = permission in user_permissions
|
||
|
||
if not (is_owner or has_permission):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '無權存取此資源'
|
||
}), 403
|
||
|
||
return fn(resource_user_id, *args, **kwargs)
|
||
|
||
return wrapper
|
||
return decorator
|
||
|
||
# 使用範例
|
||
@app.route('/api/users/<int:user_id>/profile', methods=['PUT'])
|
||
@resource_owner_or_permission('user:update')
|
||
def update_user_profile(user_id):
|
||
"""更新用戶資料 - 只能更新自己或有管理權限"""
|
||
# ... 更新邏輯
|
||
pass
|
||
6.2.3 部門資料存取控制
|
||
def department_access_required(fn):
|
||
"""
|
||
部門資料存取控制
|
||
|
||
規則:
|
||
- 一般用戶: 只能看自己部門
|
||
- 管理者: 可看自己管理的部門
|
||
- 超級管理員: 可看所有部門
|
||
"""
|
||
@wraps(fn)
|
||
@jwt_required()
|
||
def wrapper(*args, **kwargs):
|
||
jwt_data = get_jwt()
|
||
user_roles = jwt_data.get('roles', [])
|
||
user_department = jwt_data.get('department')
|
||
|
||
# 從請求參數獲取要存取的部門
|
||
request_department = request.args.get('department') or request.json.get('department')
|
||
|
||
# 超級管理員: 全部通過
|
||
if 'super_admin' in user_roles:
|
||
return fn(*args, **kwargs)
|
||
|
||
# 管理者: 檢查是否為其管理的部門
|
||
if 'admin' in user_roles:
|
||
managed_departments = get_managed_departments(get_jwt_identity())
|
||
if request_department in managed_departments:
|
||
return fn(*args, **kwargs)
|
||
|
||
# 一般用戶: 只能存取自己部門
|
||
if request_department == user_department:
|
||
return fn(*args, **kwargs)
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '無權存取其他部門資料'
|
||
}), 403
|
||
|
||
return wrapper
|
||
6.3 角色權限配置
|
||
6.3.1 角色定義與權限矩陣
|
||
功能模組
|
||
權限
|
||
一般使用者
|
||
管理者
|
||
超級管理員
|
||
用戶管理
|
||
|
||
|
||
|
||
|
||
|
||
查看用戶清單
|
||
?
|
||
? (部門內)
|
||
? (全部)
|
||
|
||
建立用戶
|
||
?
|
||
?
|
||
?
|
||
|
||
更新用戶
|
||
? (自己)
|
||
? (部門內)
|
||
? (全部)
|
||
|
||
刪除用戶
|
||
?
|
||
?
|
||
?
|
||
|
||
管理角色
|
||
?
|
||
?
|
||
?
|
||
能力評估
|
||
|
||
|
||
|
||
|
||
|
||
建立評估
|
||
?
|
||
?
|
||
?
|
||
|
||
查看評估
|
||
? (自己)
|
||
? (部門內)
|
||
? (全部)
|
||
|
||
更新評估
|
||
? (自己)
|
||
? (部門內)
|
||
? (全部)
|
||
|
||
刪除評估
|
||
?
|
||
? (部門內)
|
||
?
|
||
STAR回饋
|
||
|
||
|
||
|
||
|
||
|
||
建立回饋
|
||
?
|
||
?
|
||
?
|
||
|
||
查看回饋
|
||
? (自己)
|
||
? (部門內)
|
||
? (全部)
|
||
排名系統
|
||
|
||
|
||
|
||
|
||
|
||
查看個人排名
|
||
?
|
||
?
|
||
?
|
||
|
||
查看部門排名
|
||
? (部門內)
|
||
? (部門內)
|
||
? (全部)
|
||
|
||
查看全公司排名
|
||
?
|
||
?
|
||
?
|
||
|
||
手動計算排名
|
||
?
|
||
?
|
||
?
|
||
報表匯出
|
||
|
||
|
||
|
||
|
||
|
||
匯出個人資料
|
||
?
|
||
?
|
||
?
|
||
|
||
匯出部門資料
|
||
?
|
||
?
|
||
?
|
||
|
||
匯出全公司資料
|
||
?
|
||
?
|
||
?
|
||
系統管理
|
||
|
||
|
||
|
||
|
||
|
||
查看系統日誌
|
||
?
|
||
?
|
||
?
|
||
|
||
系統設定
|
||
?
|
||
?
|
||
?
|
||
|
||
資料備份
|
||
?
|
||
?
|
||
?
|
||
6.3.2 權限初始化腳本
|
||
def initialize_roles_and_permissions():
|
||
"""初始化角色與權限"""
|
||
|
||
# 1. 建立權限
|
||
permissions_data = [
|
||
# 用戶管理
|
||
('user:create', '建立用戶', 'user', 'create'),
|
||
('user:read', '查看用戶', 'user', 'read'),
|
||
('user:update', '更新用戶', 'user', 'update'),
|
||
('user:delete', '刪除用戶', 'user', 'delete'),
|
||
('user:manage_roles', '管理角色', 'user', 'manage_roles'),
|
||
|
||
# 評估管理
|
||
('assessment:create', '建立評估', 'assessment', 'create'),
|
||
('assessment:read', '查看評估', 'assessment', 'read'),
|
||
('assessment:read_all', '查看所有評估', 'assessment', 'read_all'),
|
||
('assessment:update', '更新評估', 'assessment', 'update'),
|
||
('assessment:delete', '刪除評估', 'assessment', 'delete'),
|
||
|
||
# STAR回饋
|
||
('feedback:create', '建立回饋', 'feedback', 'create'),
|
||
('feedback:read', '查看回饋', 'feedback', 'read'),
|
||
('feedback:read_all', '查看所有回饋', 'feedback', 'read_all'),
|
||
|
||
# 排名
|
||
('ranking:read', '查看排名', 'ranking', 'read'),
|
||
('ranking:read_department', '查看部門排名', 'ranking', 'read_department'),
|
||
('ranking:read_all', '查看所有排名', 'ranking', 'read_all'),
|
||
('ranking:calculate', '計算排名', 'ranking', 'calculate'),
|
||
|
||
# 報表
|
||
('report:export', '匯出報表', 'report', 'export'),
|
||
('report:export_all', '匯出所有報表', 'report', 'export_all'),
|
||
|
||
# 系統
|
||
('system:config', '系統設定', 'system', 'config'),
|
||
('system:logs', '查看日誌', 'system', 'logs'),
|
||
('system:backup', '資料備份', 'system', 'backup'),
|
||
]
|
||
|
||
permissions = {}
|
||
for name, display_name, resource, action in permissions_data:
|
||
perm = Permission(
|
||
name=name,
|
||
display_name=display_name,
|
||
resource=resource,
|
||
action=action
|
||
)
|
||
db.session.add(perm)
|
||
permissions[name] = perm
|
||
|
||
db.session.flush()
|
||
|
||
# 2. 建立角色
|
||
roles_data = [
|
||
('super_admin', '超級管理員', '系統最高權限', 100),
|
||
('admin', '管理者', '部門管理權限', 50),
|
||
('user', '一般使用者', '基本使用權限', 10),
|
||
]
|
||
|
||
roles = {}
|
||
for name, display_name, description, level in roles_data:
|
||
role = Role(
|
||
name=name,
|
||
display_name=display_name,
|
||
description=description,
|
||
level=level
|
||
)
|
||
db.session.add(role)
|
||
roles[name] = role
|
||
|
||
db.session.flush()
|
||
|
||
# 3. 分配權限給角色
|
||
# 超級管理員: 所有權限
|
||
for perm in permissions.values():
|
||
roles['super_admin'].permissions.append(perm)
|
||
|
||
# 管理者: 部門管理權限
|
||
admin_perms = [
|
||
'user:read', 'user:update',
|
||
'assessment:create', 'assessment:read', 'assessment:read_all',
|
||
'assessment:update', 'assessment:delete',
|
||
'feedback:create', 'feedback:read', 'feedback:read_all',
|
||
'ranking:read', 'ranking:read_department', 'ranking:read_all',
|
||
'report:export'
|
||
]
|
||
for perm_name in admin_perms:
|
||
if perm_name in permissions:
|
||
roles['admin'].permissions.append(permissions[perm_name])
|
||
|
||
# 一般使用者: 基本權限
|
||
user_perms = [
|
||
'assessment:read',
|
||
'feedback:read',
|
||
'ranking:read'
|
||
]
|
||
for perm_name in user_perms:
|
||
if perm_name in permissions:
|
||
roles['user'].permissions.append(permissions[perm_name])
|
||
|
||
db.session.commit()
|
||
print("? 角色與權限初始化完成")
|
||
6.4 權限管理後台
|
||
6.4.1 用戶管理介面
|
||
頁面路由: /admin/users
|
||
權限要求: super_admin
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ ?? 用戶管理 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ [+ 新增用戶] ?? 搜尋: [_______] │
|
||
│ 篩選: [所有部門▼] [所有角色▼] [狀態▼] │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ ? 用戶名 姓名 部門 職位 角色 狀態 操作 │
|
||
│ ───────────────────────────────────────────────────── │
|
||
│ □ zhangsan 張三 技術部 工程師 user ? [???]│
|
||
│ □ lisi 李四 業務部 經理 admin ? [???]│
|
||
│ □ wangwu 王五 技術部 資深 admin ? [???]│
|
||
│ □ zhaoliu 趙六 人資部 專員 user ? [???]│
|
||
│ ... │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 批次操作: [啟用] [停用] [刪除] │
|
||
│ [1] [2] [3] ... 下一頁 → │
|
||
└─────────────────────────────────────────────────────────┘
|
||
用戶編輯對話框:
|
||
┌─────────────────────────────────────┐
|
||
│ 編輯用戶: 張三 │
|
||
├─────────────────────────────────────┤
|
||
│ 用戶名: zhangsan │
|
||
│ 郵箱: zhangsan@company.com │
|
||
│ 姓名: 張三 │
|
||
│ 部門: [技術部▼] │
|
||
│ 職位: [工程師▼] │
|
||
│ 員工ID: EMP001 │
|
||
│ │
|
||
│ 角色: □ 超級管理員 │
|
||
│ ? 管理者 │
|
||
│ ? 一般使用者 │
|
||
│ │
|
||
│ 狀態: ● 啟用 ○ 停用 │
|
||
│ │
|
||
│ [重設密碼] │
|
||
│ │
|
||
│ [取消] [儲存] │
|
||
└─────────────────────────────────────┘
|
||
6.4.2 角色管理介面
|
||
頁面路由: /admin/roles
|
||
權限要求: super_admin
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ ?? 角色管理 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ [+ 新增角色] │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 角色名稱 顯示名稱 層級 狀態 操作 │
|
||
│ ───────────────────────────────────────────────────── │
|
||
│ super_admin 超級管理員 100 ? [???權限] │
|
||
│ admin 管理者 50 ? [???權限] │
|
||
│ user 一般使用者 10 ? [???權限] │
|
||
│ ... │
|
||
└─────────────────────────────────────────────────────────┘
|
||
權限配置對話框:
|
||
┌─────────────────────────────────────────┐
|
||
│ 配置角色權限: 管理者 │
|
||
├─────────────────────────────────────────┤
|
||
│ ?? 用戶管理 │
|
||
│ □ user:create 建立用戶 │
|
||
│ ? user:read 查看用戶 │
|
||
│ ? user:update 更新用戶 │
|
||
│ □ user:delete 刪除用戶 │
|
||
│ □ user:manage_roles 管理角色 │
|
||
│ │
|
||
│ ?? 評估管理 │
|
||
│ ? assessment:create │
|
||
│ ? assessment:read │
|
||
│ ? assessment:read_all │
|
||
│ ? assessment:update │
|
||
│ ? assessment:delete │
|
||
│ │
|
||
│ ?? STAR回饋 │
|
||
│ ? feedback:create │
|
||
│ ? feedback:read │
|
||
│ ? feedback:read_all │
|
||
│ │
|
||
│ [全選] [反選] [重設] │
|
||
│ │
|
||
│ [取消] [儲存] │
|
||
└─────────────────────────────────────────┘
|
||
6.4.3 操作日誌介面
|
||
頁面路由: /admin/audit-logs
|
||
權限要求: system:logs
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ ?? 操作日誌 │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 時間範圍: [2025-10-01] 至 [2025-10-15] │
|
||
│ 篩選: [所有用戶▼] [所有動作▼] [所有狀態▼] │
|
||
│ ?? 搜尋: [____________] │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 時間 用戶 動作 資源 狀態 IP │
|
||
│ ───────────────────────────────────────────────────── │
|
||
│ 10/15 14:23 張三 login - ? 192... │
|
||
│ 10/15 14:25 張三 create assessment ? 192... │
|
||
│ 10/15 14:30 李四 update user ? 192... │
|
||
│ 10/15 14:31 王五 delete feedback ? 192... │
|
||
│ ... │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ [匯出日誌] [清除舊日誌] │
|
||
│ [1] [2] [3] ... 下一頁 → │
|
||
└─────────────────────────────────────────────────────────┘
|
||
日誌詳情:
|
||
┌─────────────────────────────────────┐
|
||
│ 操作詳情 │
|
||
├─────────────────────────────────────┤
|
||
│ 時間: 2025-10-15 14:25:33 │
|
||
│ 用戶: 張三 (zhangsan) │
|
||
│ 動作: 建立評估 │
|
||
│ 資源: assessment #123 │
|
||
│ 狀態: ? 成功 │
|
||
│ IP位址: 192.168.1.100 │
|
||
│ 瀏覽器: Chrome 118.0 │
|
||
│ │
|
||
│ 請求資料: │
|
||
│ { │
|
||
│ "department": "技術部", │
|
||
│ "position": "工程師", │
|
||
│ "assessment_data": {...} │
|
||
│ } │
|
||
│ │
|
||
│ 回應資料: │
|
||
│ { │
|
||
│ "success": true, │
|
||
│ "assessment_id": 123 │
|
||
│ } │
|
||
│ │
|
||
│ [關閉] │
|
||
└─────────────────────────────────────┘
|
||
6.5 安全增強措施
|
||
6.5.1 登入安全
|
||
# 登入失敗計數
|
||
class LoginAttempt(db.Model):
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
username = db.Column(db.String(50))
|
||
ip_address = db.Column(db.String(45))
|
||
attempt_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||
success = db.Column(db.Boolean, default=False)
|
||
|
||
def check_login_attempts(username, ip_address):
|
||
"""
|
||
檢查登入失敗次數
|
||
|
||
規則:
|
||
- 5次失敗: 鎖定15分鐘
|
||
- 10次失敗: 鎖定1小時
|
||
- 20次失敗: 永久鎖定,需管理員解鎖
|
||
"""
|
||
# 查詢最近15分鐘的失敗次數
|
||
recent_attempts = LoginAttempt.query.filter(
|
||
LoginAttempt.username == username,
|
||
LoginAttempt.ip_address == ip_address,
|
||
LoginAttempt.success == False,
|
||
LoginAttempt.attempt_time >= datetime.utcnow() - timedelta(minutes=15)
|
||
).count()
|
||
|
||
if recent_attempts >= 5:
|
||
return False, "登入失敗次數過多,請15分鐘後再試"
|
||
|
||
return True, None
|
||
|
||
def record_login_attempt(username, ip_address, success):
|
||
"""記錄登入嘗試"""
|
||
attempt = LoginAttempt(
|
||
username=username,
|
||
ip_address=ip_address,
|
||
success=success
|
||
)
|
||
db.session.add(attempt)
|
||
db.session.commit()
|
||
6.5.2 Session管理
|
||
# Session資料表
|
||
class UserSession(db.Model):
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||
token_jti = db.Column(db.String(36), unique=True) # JWT ID
|
||
ip_address = db.Column(db.String(45))
|
||
user_agent = db.Column(db.String(255))
|
||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||
expires_at = db.Column(db.DateTime)
|
||
is_active = db.Column(db.Boolean, default=True)
|
||
|
||
def revoke_all_user_sessions(user_id):
|
||
"""撤銷用戶所有Session (例如: 密碼變更時)"""
|
||
UserSession.query.filter_by(user_id=user_id).update(
|
||
{'is_active': False}
|
||
)
|
||
db.session.commit()
|
||
|
||
# JWT Token撤銷檢查
|
||
@jwt.token_in_blocklist_loader
|
||
def check_if_token_revoked(jwt_header, jwt_payload):
|
||
jti = jwt_payload['jti']
|
||
session = UserSession.query.filter_by(token_jti=jti).first()
|
||
|
||
if not session:
|
||
return True # Token不存在,視為已撤銷
|
||
|
||
return not session.is_active # 檢查Session是否啟用
|
||
6.5.3 敏感操作二次驗證
|
||
def require_password_confirmation(fn):
|
||
"""
|
||
敏感操作需要二次密碼驗證
|
||
|
||
適用於:
|
||
- 刪除用戶
|
||
- 修改權限
|
||
- 匯出敏感資料
|
||
"""
|
||
@wraps(fn)
|
||
@jwt_required()
|
||
def wrapper(*args, **kwargs):
|
||
# 檢查是否提供密碼
|
||
password = request.json.get('confirm_password')
|
||
|
||
if not password:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '此操作需要密碼確認'
|
||
}), 401
|
||
|
||
# 驗證密碼
|
||
user_id = get_jwt_identity()
|
||
user = User.query.get(user_id)
|
||
|
||
if not user or not bcrypt.check_password_hash(user.password_hash, password):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '密碼驗證失敗'
|
||
}), 401
|
||
|
||
return fn(*args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
# 使用範例
|
||
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
|
||
@permission_required('user:delete')
|
||
@require_password_confirmation
|
||
def delete_user(user_id):
|
||
"""刪除用戶 - 需要密碼確認"""
|
||
# ... 刪除邏輯
|
||
pass
|
||
|
||
7. API端點設計(更新與新增)
|
||
7.1 認證相關API ??
|
||
POST /api/auth/register
|
||
註冊新用戶
|
||
請求Body:
|
||
{
|
||
"username": "zhangsan",
|
||
"email": "zhangsan@company.com",
|
||
"password": "SecurePass123!",
|
||
"full_name": "張三",
|
||
"department": "技術部",
|
||
"position": "工程師",
|
||
"employee_id": "EMP001"
|
||
}
|
||
回應 (201):
|
||
{
|
||
"success": true,
|
||
"message": "註冊成功",
|
||
"user_id": 1
|
||
}
|
||
POST /api/auth/login
|
||
用戶登入
|
||
請求Body:
|
||
{
|
||
"username": "zhangsan",
|
||
"password": "SecurePass123!"
|
||
}
|
||
回應 (200):
|
||
{
|
||
"success": true,
|
||
"access_token": "eyJhbGci...",
|
||
"refresh_token": "eyJhbGci...",
|
||
"user": {
|
||
"id": 1,
|
||
"username": "zhangsan",
|
||
"full_name": "張三",
|
||
"department": "技術部",
|
||
"roles": ["user"],
|
||
"permissions": ["assessment:read", "feedback:read"]
|
||
}
|
||
}
|
||
POST /api/auth/logout
|
||
用戶登出
|
||
請求Header:
|
||
Authorization: Bearer <access_token>
|
||
回應 (200):
|
||
{
|
||
"success": true,
|
||
"message": "登出成功"
|
||
}
|
||
POST /api/auth/refresh
|
||
刷新Token
|
||
請求Header:
|
||
Authorization: Bearer <refresh_token>
|
||
回應 (200):
|
||
{
|
||
"access_token": "new_token..."
|
||
}
|
||
GET /api/auth/me
|
||
獲取當前用戶資訊
|
||
回應 (200):
|
||
{
|
||
"id": 1,
|
||
"username": "zhangsan",
|
||
"email": "zhangsan@company.com",
|
||
"full_name": "張三",
|
||
"department": "技術部",
|
||
"position": "工程師",
|
||
"roles": ["user"],
|
||
"permissions": ["assessment:read", "feedback:read"],
|
||
"last_login_at": "2025-10-15T14:23:00Z"
|
||
}
|
||
7.2 儀表板相關API ??
|
||
GET /api/dashboard
|
||
獲取個人儀表板資料
|
||
權限: 已登入用戶
|
||
回應 (200):
|
||
{
|
||
"user": {
|
||
"id": 1,
|
||
"full_name": "張三",
|
||
"department": "技術部",
|
||
"position": "工程師"
|
||
},
|
||
"points": {
|
||
"total_points": 450,
|
||
"monthly_points": 80,
|
||
"monthly_change_percent": 21.5
|
||
},
|
||
"department_ranking": {
|
||
"rank": 3,
|
||
"total": 15,
|
||
"percentile": 80.0,
|
||
"better_than_count": 12
|
||
},
|
||
"total_ranking": {
|
||
"rank": 15,
|
||
"total": 120,
|
||
"percentile": 87.5,
|
||
"tier": "top_20"
|
||
},
|
||
"badges": [
|
||
{
|
||
"icon": "??",
|
||
"name": "頂尖表現",
|
||
"description": "位於前10%"
|
||
}
|
||
],
|
||
"recent_feedbacks": [
|
||
{
|
||
"id": 123,
|
||
"evaluator_name": "王主管",
|
||
"score": 5,
|
||
"points_earned": 50,
|
||
"feedback_date": "2025-10-14",
|
||
"result_preview": "專案交付優秀,超越預期..."
|
||
}
|
||
],
|
||
"points_trend": [
|
||
{"month": "2025-05", "points": 320},
|
||
{"month": "2025-06", "points": 350},
|
||
{"month": "2025-07", "points": 380},
|
||
{"month": "2025-08", "points": 410},
|
||
{"month": "2025-09", "points": 420},
|
||
{"month": "2025-10", "points": 450}
|
||
]
|
||
}
|
||
GET /api/leaderboard
|
||
獲取排行榜資料
|
||
權限: 已登入用戶
|
||
請求參數:
|
||
* scope: company | department (預設: company)
|
||
* period: total | monthly | yearly (預設: total)
|
||
* department: 部門名稱 (當scope=department時)
|
||
* year: 年份 (當period=monthly/yearly時)
|
||
* month: 月份 (當period=monthly時)
|
||
* page: 頁碼 (預設: 1)
|
||
* per_page: 每頁筆數 (預設: 50)
|
||
回應 (200):
|
||
{
|
||
"scope": "company",
|
||
"period": "total",
|
||
"rankings": [
|
||
{
|
||
"rank": 1,
|
||
"employee_name": "王五",
|
||
"department": "技術部",
|
||
"position": "資深工程師",
|
||
"total_points": 580,
|
||
"percentile": 98.0,
|
||
"badges": ["??", "??", "??"],
|
||
"rank_change": 0,
|
||
"is_current_user": false
|
||
},
|
||
{
|
||
"rank": 2,
|
||
"employee_name": "李四",
|
||
"department": "業務部",
|
||
"position": "業務經理",
|
||
"total_points": 520,
|
||
"percentile": 96.0,
|
||
"badges": ["??", "??", "?"],
|
||
"rank_change": 1,
|
||
"is_current_user": false
|
||
},
|
||
{
|
||
"rank": 3,
|
||
"employee_name": "張三",
|
||
"department": "技術部",
|
||
"position": "工程師",
|
||
"total_points": 450,
|
||
"percentile": 94.0,
|
||
"badges": ["??", "??"],
|
||
"rank_change": -1,
|
||
"is_current_user": true
|
||
}
|
||
],
|
||
"total": 120,
|
||
"page": 1,
|
||
"per_page": 50,
|
||
"pages": 3
|
||
}
|
||
7.3 用戶管理API ??
|
||
GET /api/admin/users
|
||
獲取用戶清單
|
||
權限: user:read
|
||
請求參數:
|
||
* page: 頁碼
|
||
* per_page: 每頁筆數
|
||
* department: 部門篩選
|
||
* role: 角色篩選
|
||
* is_active: 狀態篩選
|
||
* search: 搜尋關鍵字
|
||
回應 (200):
|
||
{
|
||
"users": [
|
||
{
|
||
"id": 1,
|
||
"username": "zhangsan",
|
||
"full_name": "張三",
|
||
"email": "zhangsan@company.com",
|
||
"department": "技術部",
|
||
"position": "工程師",
|
||
"roles": ["user"],
|
||
"is_active": true,
|
||
"last_login_at": "2025-10-15T14:23:00Z",
|
||
"created_at": "2025-01-01T00:00:00Z"
|
||
}
|
||
],
|
||
"total": 50,
|
||
"page": 1,
|
||
"per_page": 20
|
||
}
|
||
POST /api/admin/users
|
||
建立新用戶
|
||
權限: user:create
|
||
PUT /api/admin/users/:user_id
|
||
更新用戶資訊
|
||
權限: user:update 或資源擁有者
|
||
DELETE /api/admin/users/:user_id
|
||
刪除用戶
|
||
權限: user:delete
|
||
需要: 密碼確認
|
||
PUT /api/admin/users/:user_id/roles
|
||
更新用戶角色
|
||
權限: user:manage_roles
|
||
請求Body:
|
||
{
|
||
"role_ids": [1, 2],
|
||
"confirm_password": "password"
|
||
}
|
||
7.4 角色權限管理API ??
|
||
GET /api/admin/roles
|
||
獲取角色清單
|
||
權限: super_admin
|
||
POST /api/admin/roles
|
||
建立新角色
|
||
權限: super_admin
|
||
PUT /api/admin/roles/:role_id/permissions
|
||
更新角色權限
|
||
權限: super_admin
|
||
請求Body:
|
||
{
|
||
"permission_ids": [1, 2, 3, 5, 8],
|
||
"confirm_password": "password"
|
||
}
|
||
7.5 操作日誌API ??
|
||
GET /api/admin/audit-logs
|
||
獲取操作日誌
|
||
權限: system:logs
|
||
請求參數:
|
||
* start_date: 開始日期
|
||
* end_date: 結束日期
|
||
* user_id: 用戶ID篩選
|
||
* action: 動作篩選
|
||
* status: 狀態篩選
|
||
回應 (200):
|
||
{
|
||
"logs": [
|
||
{
|
||
"id": 1,
|
||
"user_id": 1,
|
||
"username": "zhangsan",
|
||
"action": "login",
|
||
"resource_type": null,
|
||
"resource_id": null,
|
||
"status": "success",
|
||
"ip_address": "192.168.1.100",
|
||
"created_at": "2025-10-15T14:23:00Z"
|
||
}
|
||
],
|
||
"total": 1000,
|
||
"page": 1,
|
||
"per_page": 50
|
||
}
|
||
|
||
8. 前端頁面更新
|
||
8.1 新增頁面清單
|
||
頁面路由
|
||
頁面名稱
|
||
權限要求
|
||
說明
|
||
/login
|
||
登入頁面
|
||
公開
|
||
用戶登入
|
||
/register
|
||
註冊頁面
|
||
公開
|
||
新用戶註冊(可選關閉)
|
||
/dashboard
|
||
個人儀表板
|
||
已登入
|
||
積分、排名、趨勢
|
||
/leaderboard
|
||
競賽排行榜
|
||
已登入
|
||
全公司/部門排名
|
||
/profile
|
||
個人資料
|
||
已登入
|
||
查看/編輯個人資料
|
||
/admin/users
|
||
用戶管理
|
||
user:read
|
||
用戶清單與管理
|
||
/admin/roles
|
||
角色管理
|
||
super_admin
|
||
角色與權限配置
|
||
/admin/logs
|
||
操作日誌
|
||
system:logs
|
||
系統操作記錄
|
||
8.2 導航選單更新
|
||
<!-- 主導航選單 -->
|
||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="/">夥伴對齊系統</a>
|
||
|
||
<div class="collapse navbar-collapse">
|
||
<ul class="navbar-nav me-auto">
|
||
<!-- 一般用戶可見 -->
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/dashboard">
|
||
?? 儀表板
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/assessment">
|
||
?? 能力評估
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="/leaderboard">
|
||
?? 排行榜
|
||
</a>
|
||
</li>
|
||
|
||
<!-- 管理者可見 -->
|
||
<li class="nav-item dropdown" v-if="hasPermission('feedback:create')">
|
||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
|
||
?? 管理
|
||
</a>
|
||
<ul class="dropdown-menu">
|
||
<li><a class="dropdown-item" href="/star-feedback">STAR回饋</a></li>
|
||
<li><a class="dropdown-item" href="/admin">資料管理</a></li>
|
||
</ul>
|
||
</li>
|
||
|
||
<!-- 超級管理員可見 -->
|
||
<li class="nav-item dropdown" v-if="hasRole('super_admin')">
|
||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
|
||
?? 系統
|
||
</a>
|
||
<ul class="dropdown-menu">
|
||
<li><a class="dropdown-item" href="/admin/users">用戶管理</a></li>
|
||
<li><a class="dropdown-item" href="/admin/roles">角色管理</a></li>
|
||
<li><a class="dropdown-item" href="/admin/logs">操作日誌</a></li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- 用戶選單 -->
|
||
<ul class="navbar-nav">
|
||
<li class="nav-item dropdown">
|
||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
|
||
<img src="/api/users/avatar" class="rounded-circle" width="32">
|
||
{{ currentUser.full_name }}
|
||
<span class="badge bg-warning">{{ currentUser.total_points }} pts</span>
|
||
</a>
|
||
<ul class="dropdown-menu dropdown-menu-end">
|
||
<li><a class="dropdown-item" href="/profile">?? 個人資料</a></li>
|
||
<li><a class="dropdown-item" href="/settings">?? 設定</a></li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="#" @click="logout">?? 登出</a></li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
9. 部署與環境配置更新
|
||
9.1 環境變數更新
|
||
.env 範例:
|
||
# 資料庫設定
|
||
DB_HOST=localhost
|
||
DB_PORT=3306
|
||
DB_NAME=partner_alignment
|
||
DB_USER=db_user
|
||
DB_PASSWORD=strong_password
|
||
|
||
# Flask設定
|
||
FLASK_ENV=production
|
||
FLASK_DEBUG=False
|
||
FLASK_HOST=127.0.0.1
|
||
FLASK_PORT=5000
|
||
|
||
# 安全設定
|
||
SECRET_KEY=your_64_char_random_secret_key_here
|
||
JWT_SECRET_KEY=your_64_char_jwt_secret_key_here
|
||
JWT_ACCESS_TOKEN_EXPIRES=3600 # 1小時
|
||
JWT_REFRESH_TOKEN_EXPIRES=604800 # 7天
|
||
|
||
# CORS設定
|
||
CORS_ORIGINS=https://your-domain.com
|
||
|
||
# 郵件設定 (用於通知)
|
||
MAIL_SERVER=smtp.gmail.com
|
||
MAIL_PORT=587
|
||
MAIL_USE_TLS=True
|
||
MAIL_USERNAME=your_email@company.com
|
||
MAIL_PASSWORD=your_email_password
|
||
|
||
# 系統設定
|
||
ENABLE_REGISTRATION=False # 是否開放註冊
|
||
DEFAULT_ROLE=user # 新用戶預設角色
|
||
SESSION_TIMEOUT=3600 # Session過期時間(秒)
|
||
9.2 初始化腳本
|
||
init_system.py:
|
||
"""系統初始化腳本"""
|
||
|
||
from app import create_app, db
|
||
from app.models import User, Role, Permission
|
||
from flask_bcrypt import Bcrypt
|
||
|
||
def init_system():
|
||
"""初始化系統"""
|
||
app = create_app()
|
||
bcrypt = Bcrypt(app)
|
||
|
||
with app.app_context():
|
||
print("?? 開始初始化系統...")
|
||
|
||
# 1. 建立資料庫表格
|
||
print("?? 建立資
|
||
料庫表格...") db.create_all() print("? 資料庫表格建立完成")
|
||
# 2. 初始化角色與權限
|
||
print("?? 初始化角色與權限...")
|
||
initialize_roles_and_permissions()
|
||
print("? 角色與權限初始化完成")
|
||
|
||
# 3. 建立預設管理員帳號
|
||
print("?? 建立預設管理員...")
|
||
admin_exists = User.query.filter_by(username='admin').first()
|
||
|
||
if not admin_exists:
|
||
admin_password = 'Admin@123456' # 建議首次登入後立即修改
|
||
admin_user = User(
|
||
username='admin',
|
||
email='admin@company.com',
|
||
password_hash=bcrypt.generate_password_hash(admin_password).decode('utf-8'),
|
||
full_name='系統管理員',
|
||
department='系統管理',
|
||
position='超級管理員',
|
||
employee_id='ADMIN001',
|
||
is_active=True
|
||
)
|
||
|
||
# 分配超級管理員角色
|
||
super_admin_role = Role.query.filter_by(name='super_admin').first()
|
||
admin_user.roles.append(super_admin_role)
|
||
|
||
db.session.add(admin_user)
|
||
db.session.commit()
|
||
|
||
print("? 預設管理員建立完成")
|
||
print(f" 用戶名: admin")
|
||
print(f" 密碼: {admin_password}")
|
||
print(" ?? 請立即登入並修改密碼!")
|
||
else:
|
||
print("?? 管理員帳號已存在,跳過建立")
|
||
|
||
# 4. 初始化預設能力項目
|
||
print("?? 初始化能力項目...")
|
||
init_capabilities()
|
||
print("? 能力項目初始化完成")
|
||
|
||
# 5. 建立測試資料(可選)
|
||
if app.config.get('TESTING'):
|
||
print("?? 建立測試資料...")
|
||
create_test_data()
|
||
print("? 測試資料建立完成")
|
||
|
||
print("?? 系統初始化完成!")
|
||
print("\n下一步:")
|
||
print("1. 使用 admin/Admin@123456 登入系統")
|
||
print("2. 立即修改管理員密碼")
|
||
print("3. 建立其他管理者與用戶帳號")
|
||
print("4. 開始使用系統")
|
||
def initialize_roles_and_permissions(): """初始化角色與權限(如前面定義)""" # ... (參考前面 6.3.2 的實作) pass
|
||
def init_capabilities(): """初始化能力項目""" from app.models import Capability
|
||
capabilities_data = [
|
||
{
|
||
"name": "程式設計與開發",
|
||
"l1_description": "能撰寫基本程式碼,完成簡單功能開發",
|
||
"l2_description": "能獨立開發中等複雜度功能,遵循編碼規範與最佳實踐",
|
||
"l3_description": "能設計系統架構,優化程式效能,處理複雜技術問題",
|
||
"l4_description": "能指導團隊技術方向,制定開發標準,進行技術評審",
|
||
"l5_description": "能規劃技術策略,引領技術創新,影響組織技術方向"
|
||
},
|
||
{
|
||
"name": "系統分析與設計",
|
||
"l1_description": "能理解基本需求,參與需求討論",
|
||
"l2_description": "能獨立進行需求分析,設計簡單系統",
|
||
"l3_description": "能處理複雜需求,設計可擴展的系統架構",
|
||
"l4_description": "能主導大型專案設計,指導團隊架構決策",
|
||
"l5_description": "能制定企業級架構標準,推動組織架構演進"
|
||
},
|
||
{
|
||
"name": "專案管理",
|
||
"l1_description": "能執行分配的任務,按時回報進度",
|
||
"l2_description": "能獨立管理小型專案,協調資源與時程",
|
||
"l3_description": "能處理複雜專案,解決跨部門協作問題,控制風險",
|
||
"l4_description": "能領導多個專案,培養專案經理,優化管理流程",
|
||
"l5_description": "能制定專案管理策略,建立PMO體系,推動組織成熟度"
|
||
},
|
||
{
|
||
"name": "技術領導與指導",
|
||
"l1_description": "能學習新技術,分享基本經驗",
|
||
"l2_description": "能指導新人,協助團隊成員解決問題",
|
||
"l3_description": "能主導技術分享,培訓團隊成員,提升團隊能力",
|
||
"l4_description": "能建立技術團隊,制定培訓計畫,發展人才梯隊",
|
||
"l5_description": "能建立技術文化,影響組織學習氛圍,培養技術領導者"
|
||
}
|
||
]
|
||
|
||
for cap_data in capabilities_data:
|
||
existing = Capability.query.filter_by(name=cap_data['name']).first()
|
||
if not existing:
|
||
capability = Capability(**cap_data)
|
||
db.session.add(capability)
|
||
|
||
db.session.commit()
|
||
def create_test_data(): """建立測試資料(開發/測試環境用)""" from app.models import User, EmployeePoints, StarFeedback import random from datetime import datetime, timedelta
|
||
bcrypt = Bcrypt()
|
||
|
||
# 建立測試用戶
|
||
departments = ['技術部', '業務部', '人資部', '財務部']
|
||
positions = ['工程師', '資深工程師', '專員', '資深專員', '經理']
|
||
|
||
test_users = []
|
||
for i in range(1, 21): # 建立20個測試用戶
|
||
username = f'user{i:02d}'
|
||
user = User(
|
||
username=username,
|
||
email=f'{username}@company.com',
|
||
password_hash=bcrypt.generate_password_hash('Test@1234').decode('utf-8'),
|
||
full_name=f'測試員工{i:02d}',
|
||
department=random.choice(departments),
|
||
position=random.choice(positions),
|
||
employee_id=f'TEST{i:03d}',
|
||
is_active=True
|
||
)
|
||
|
||
# 分配角色
|
||
if i <= 2:
|
||
# 前2個為管理者
|
||
admin_role = Role.query.filter_by(name='admin').first()
|
||
user.roles.append(admin_role)
|
||
|
||
user_role = Role.query.filter_by(name='user').first()
|
||
user.roles.append(user_role)
|
||
|
||
db.session.add(user)
|
||
test_users.append(user)
|
||
|
||
db.session.commit()
|
||
|
||
# 建立測試回饋與積分
|
||
for user in test_users:
|
||
# 建立員工積分記錄
|
||
emp_points = EmployeePoints(
|
||
user_id=user.id,
|
||
employee_name=user.full_name,
|
||
department=user.department,
|
||
position=user.position,
|
||
total_points=0,
|
||
monthly_points=0
|
||
)
|
||
db.session.add(emp_points)
|
||
|
||
# 隨機建立3-8筆回饋
|
||
num_feedbacks = random.randint(3, 8)
|
||
for j in range(num_feedbacks):
|
||
evaluator = random.choice(test_users)
|
||
score = random.randint(3, 5)
|
||
points = score * 10
|
||
|
||
feedback_date = datetime.now() - timedelta(days=random.randint(1, 90))
|
||
|
||
feedback = StarFeedback(
|
||
evaluator_user_id=evaluator.id,
|
||
evaluator_name=evaluator.full_name,
|
||
evaluatee_user_id=user.id,
|
||
evaluatee_name=user.full_name,
|
||
evaluatee_department=user.department,
|
||
evaluatee_position=user.position,
|
||
situation=f"測試情境描述{j+1}: 專案中遇到技術挑戰需要解決",
|
||
task=f"測試任務說明{j+1}: 需要在限定時間內完成特定目標",
|
||
action=f"測試行動描述{j+1}: 採取具體步驟進行問題分析與解決",
|
||
result=f"測試結果說明{j+1}: 成功達成目標並獲得正面回饋",
|
||
score=score,
|
||
points_earned=points,
|
||
feedback_date=feedback_date.date()
|
||
)
|
||
db.session.add(feedback)
|
||
|
||
# 累加積分
|
||
emp_points.total_points += points
|
||
emp_points.monthly_points += points
|
||
|
||
db.session.commit()
|
||
|
||
# 更新排名
|
||
from app.services import update_all_rankings
|
||
update_all_rankings()
|
||
if name == 'main': init_system()
|
||
|
||
### 9.3 資料庫遷移腳本
|
||
|
||
**migrations/add_auth_tables.sql**:
|
||
```sql
|
||
-- 用戶表
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
username VARCHAR(50) NOT NULL UNIQUE,
|
||
email VARCHAR(100) NOT NULL UNIQUE,
|
||
password_hash VARCHAR(255) NOT NULL,
|
||
full_name VARCHAR(100) NOT NULL,
|
||
department VARCHAR(100) NOT NULL,
|
||
position VARCHAR(100) NOT NULL,
|
||
employee_id VARCHAR(50) UNIQUE,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
last_login_at DATETIME NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
INDEX idx_username (username),
|
||
INDEX idx_email (email),
|
||
INDEX idx_department (department),
|
||
INDEX idx_employee_id (employee_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- 角色表
|
||
CREATE TABLE IF NOT EXISTS roles (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(50) NOT NULL UNIQUE,
|
||
display_name VARCHAR(100) NOT NULL,
|
||
description TEXT,
|
||
level INT NOT NULL,
|
||
is_active BOOLEAN DEFAULT TRUE,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- 權限表
|
||
CREATE TABLE IF NOT EXISTS permissions (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(100) NOT NULL UNIQUE,
|
||
display_name VARCHAR(100) NOT NULL,
|
||
resource VARCHAR(50) NOT NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
description TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_resource (resource),
|
||
INDEX idx_action (action)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- 角色權限關聯表
|
||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
role_id INT NOT NULL,
|
||
permission_id INT NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE,
|
||
UNIQUE KEY unique_role_permission (role_id, permission_id),
|
||
INDEX idx_role_id (role_id),
|
||
INDEX idx_permission_id (permission_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- 用戶角色關聯表
|
||
CREATE TABLE IF NOT EXISTS user_roles (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT NOT NULL,
|
||
role_id INT NOT NULL,
|
||
assigned_by INT,
|
||
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE SET NULL,
|
||
UNIQUE KEY unique_user_role (user_id, role_id),
|
||
INDEX idx_user_id (user_id),
|
||
INDEX idx_role_id (role_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- 操作日誌表
|
||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT,
|
||
action VARCHAR(100) NOT NULL,
|
||
resource_type VARCHAR(50) NOT NULL,
|
||
resource_id INT,
|
||
details JSON,
|
||
ip_address VARCHAR(45),
|
||
user_agent VARCHAR(255),
|
||
status ENUM('success', 'failed') DEFAULT 'success',
|
||
error_message TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||
INDEX idx_user_id (user_id),
|
||
INDEX idx_action (action),
|
||
INDEX idx_created_at (created_at),
|
||
INDEX idx_resource (resource_type, resource_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
-- 更新 employee_points 表,新增排名欄位
|
||
ALTER TABLE employee_points
|
||
ADD COLUMN user_id INT AFTER id,
|
||
ADD COLUMN department_rank INT,
|
||
ADD COLUMN department_percentile DECIMAL(5,2),
|
||
ADD COLUMN total_rank INT,
|
||
ADD COLUMN total_percentile DECIMAL(5,2),
|
||
ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||
ADD INDEX idx_user_id (user_id),
|
||
ADD INDEX idx_department_rank (department, department_rank),
|
||
ADD INDEX idx_total_rank (total_rank);
|
||
|
||
-- 更新 star_feedbacks 表,新增用戶ID關聯
|
||
ALTER TABLE star_feedbacks
|
||
ADD COLUMN evaluator_user_id INT AFTER id,
|
||
ADD COLUMN evaluatee_user_id INT AFTER evaluator_name,
|
||
ADD FOREIGN KEY (evaluator_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||
ADD FOREIGN KEY (evaluatee_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||
ADD INDEX idx_evaluator_user_id (evaluator_user_id),
|
||
ADD INDEX idx_evaluatee_user_id (evaluatee_user_id);
|
||
|
||
10. 非功能需求(更新)
|
||
10.1 效能需求
|
||
指標
|
||
目標值
|
||
說明
|
||
頁面載入時間
|
||
< 2秒
|
||
包含儀表板圖表渲染
|
||
API回應時間
|
||
< 500ms
|
||
95百分位數
|
||
並發使用者
|
||
100+
|
||
同時在線使用者數
|
||
資料庫查詢
|
||
< 100ms
|
||
單次查詢時間
|
||
檔案匯出
|
||
< 5秒
|
||
1000筆資料以內
|
||
排名計算
|
||
< 30秒
|
||
更新所有員工排名
|
||
儀表板載入
|
||
< 1.5秒
|
||
包含圖表與統計
|
||
登入驗證
|
||
< 200ms
|
||
JWT驗證時間
|
||
效能優化策略(新增):
|
||
* 排名資料快取(Redis)
|
||
* 儀表板資料預計算
|
||
* 圖表資料增量載入
|
||
* Token驗證本地快取
|
||
* 分散式Session管理
|
||
10.2 安全需求(強化)
|
||
10.2.1 認證安全
|
||
? 已實作:
|
||
* Bcrypt密碼加密(成本因子12)
|
||
* JWT Token認證
|
||
* Refresh Token機制
|
||
* Session管理與撤銷
|
||
* 登入失敗鎖定機制
|
||
* IP白名單(可選)
|
||
10.2.2 授權安全
|
||
? 已實作:
|
||
* 基於角色的存取控制(RBAC)
|
||
* 細粒度權限檢查
|
||
* 資源所有權驗證
|
||
* 部門資料隔離
|
||
* 敏感操作二次驗證
|
||
10.2.3 資料安全
|
||
? 已實作:
|
||
* SQL注入防護(參數化查詢)
|
||
* XSS防護(輸入清理)
|
||
* CSRF防護(Token驗證)
|
||
* 敏感資料加密
|
||
* 操作日誌完整記錄
|
||
10.2.4 通訊安全
|
||
? 已實作:
|
||
* HTTPS強制使用
|
||
* 安全標頭設定
|
||
* CORS跨域限制
|
||
* Token傳輸加密
|
||
10.3 可用性需求
|
||
目標: 系統可用性 ? 99.5%
|
||
容錯機制(強化):
|
||
* 資料庫主從複製
|
||
* Redis故障轉移
|
||
* 優雅降級策略
|
||
* 健康檢查端點
|
||
* 自動重啟機制
|
||
監控指標:
|
||
* 服務可用性
|
||
* API回應時間
|
||
* 資料庫連線數
|
||
* 記憶體使用率
|
||
* 錯誤率統計
|
||
10.4 可維護性需求
|
||
程式碼品質(強化):
|
||
* 模組化架構設計
|
||
* 依賴注入模式
|
||
* 單元測試覆蓋率 ? 80%
|
||
* 整合測試覆蓋核心流程
|
||
* 程式碼審查流程
|
||
文件完整性:
|
||
* API文件(Swagger/OpenAPI)
|
||
* 架構設計文件
|
||
* 部署運維手冊
|
||
* 故障排除指南
|
||
* 用戶使用手冊
|
||
10.5 擴展性需求
|
||
水平擴展能力:
|
||
* 無狀態API設計
|
||
* Session外部儲存(Redis)
|
||
* 資料庫讀寫分離
|
||
* 負載平衡支援
|
||
* 微服務化準備
|
||
資料增長預估:
|
||
資料類型
|
||
預估增長
|
||
保留政策
|
||
用戶資料
|
||
200人/年
|
||
永久保留
|
||
評估記錄
|
||
5000筆/年
|
||
永久保留
|
||
STAR回饋
|
||
10000筆/年
|
||
永久保留
|
||
操作日誌
|
||
100萬筆/年
|
||
保留2年
|
||
排名記錄
|
||
2400筆/年
|
||
保留5年
|
||
|
||
11. 測試策略(更新)
|
||
11.1 新增測試類型
|
||
11.1.1 認證授權測試
|
||
class TestAuthentication:
|
||
"""認證功能測試"""
|
||
|
||
def test_login_with_valid_credentials(self):
|
||
"""測試: 正確帳密登入成功"""
|
||
response = self.client.post('/api/auth/login', json={
|
||
'username': 'testuser',
|
||
'password': 'Test@1234'
|
||
})
|
||
|
||
assert response.status_code == 200
|
||
data = response.get_json()
|
||
assert 'access_token' in data
|
||
assert 'refresh_token' in data
|
||
|
||
def test_login_with_invalid_password(self):
|
||
"""測試: 錯誤密碼登入失敗"""
|
||
response = self.client.post('/api/auth/login', json={
|
||
'username': 'testuser',
|
||
'password': 'WrongPassword'
|
||
})
|
||
|
||
assert response.status_code == 401
|
||
|
||
def test_access_protected_route_without_token(self):
|
||
"""測試: 未提供Token存取受保護路由"""
|
||
response = self.client.get('/api/dashboard')
|
||
assert response.status_code == 401
|
||
|
||
def test_access_protected_route_with_valid_token(self):
|
||
"""測試: 有效Token存取受保護路由"""
|
||
token = self.get_auth_token()
|
||
response = self.client.get(
|
||
'/api/dashboard',
|
||
headers={'Authorization': f'Bearer {token}'}
|
||
)
|
||
assert response.status_code == 200
|
||
|
||
class TestAuthorization:
|
||
"""授權功能測試"""
|
||
|
||
def test_user_cannot_access_admin_route(self):
|
||
"""測試: 一般用戶無法存取管理員路由"""
|
||
user_token = self.get_user_token()
|
||
response = self.client.get(
|
||
'/api/admin/users',
|
||
headers={'Authorization': f'Bearer {user_token}'}
|
||
)
|
||
assert response.status_code == 403
|
||
|
||
def test_admin_can_access_department_data(self):
|
||
"""測試: 管理者可存取部門資料"""
|
||
admin_token = self.get_admin_token()
|
||
response = self.client.get(
|
||
'/api/assessments?department=技術部',
|
||
headers={'Authorization': f'Bearer {admin_token}'}
|
||
)
|
||
assert response.status_code == 200
|
||
|
||
def test_user_cannot_access_other_department_data(self):
|
||
"""測試: 一般用戶無法存取其他部門資料"""
|
||
user_token = self.get_user_token(department='技術部')
|
||
response = self.client.get(
|
||
'/api/assessments?department=業務部',
|
||
headers={'Authorization': f'Bearer {user_token}'}
|
||
)
|
||
assert response.status_code == 403
|
||
11.1.2 儀表板功能測試
|
||
class TestDashboard:
|
||
"""儀表板功能測試"""
|
||
|
||
def test_get_dashboard_data(self):
|
||
"""測試: 獲取儀表板資料"""
|
||
token = self.get_auth_token()
|
||
response = self.client.get(
|
||
'/api/dashboard',
|
||
headers={'Authorization': f'Bearer {token}'}
|
||
)
|
||
|
||
assert response.status_code == 200
|
||
data = response.get_json()
|
||
|
||
# 驗證資料結構
|
||
assert 'user' in data
|
||
assert 'points' in data
|
||
assert 'department_ranking' in data
|
||
assert 'total_ranking' in data
|
||
assert 'badges' in data
|
||
assert 'recent_feedbacks' in data
|
||
|
||
def test_dashboard_ranking_calculation(self):
|
||
"""測試: 儀表板排名計算正確性"""
|
||
# 建立測試資料
|
||
self.create_test_employees_with_points([
|
||
('員工A', 100),
|
||
('員工B', 80),
|
||
('測試用戶', 60),
|
||
('員工C', 40)
|
||
])
|
||
|
||
token = self.get_auth_token()
|
||
response = self.client.get(
|
||
'/api/dashboard',
|
||
headers={'Authorization': f'Bearer {token}'}
|
||
)
|
||
|
||
data = response.get_json()
|
||
ranking = data['department_ranking']
|
||
|
||
assert ranking['rank'] == 3
|
||
assert ranking['total'] == 4
|
||
assert ranking['percentile'] == 25.0 # 勝過1人 / 4人
|
||
11.1.3 排行榜測試
|
||
class TestLeaderboard:
|
||
"""排行榜功能測試"""
|
||
|
||
def test_get_company_leaderboard(self):
|
||
"""測試: 獲取全公司排行榜"""
|
||
token = self.get_auth_token()
|
||
response = self.client.get(
|
||
'/api/leaderboard?scope=company',
|
||
headers={'Authorization': f'Bearer {token}'}
|
||
)
|
||
|
||
assert response.status_code == 200
|
||
data = response.get_json()
|
||
|
||
assert 'rankings' in data
|
||
assert len(data['rankings']) > 0
|
||
|
||
# 驗證排序
|
||
rankings = data['rankings']
|
||
for i in range(len(rankings) - 1):
|
||
assert rankings[i]['total_points'] >= rankings[i+1]['total_points']
|
||
|
||
def test_leaderboard_with_ties(self):
|
||
"""測試: 排行榜並列排名處理"""
|
||
# 建立並列積分的員工
|
||
self.create_test_employees_with_points([
|
||
('員工A', 100),
|
||
('員工B', 80),
|
||
('員工C', 80), # 並列
|
||
('員工D', 60)
|
||
])
|
||
|
||
token = self.get_auth_token()
|
||
response = self.client.get(
|
||
'/api/leaderboard',
|
||
headers={'Authorization': f'Bearer {token}'}
|
||
)
|
||
|
||
data = response.get_json()
|
||
rankings = data['rankings']
|
||
|
||
assert rankings[0]['rank'] == 1
|
||
assert rankings[1]['rank'] == 2
|
||
assert rankings[2]['rank'] == 2 # 並列第2
|
||
assert rankings[3]['rank'] == 4 # 跳號
|
||
11.2 安全測試
|
||
class TestSecurity:
|
||
"""安全功能測試"""
|
||
|
||
def test_sql_injection_prevention(self):
|
||
"""測試: SQL注入防護"""
|
||
malicious_input = "admin' OR '1'='1"
|
||
response = self.client.post('/api/auth/login', json={
|
||
'username': malicious_input,
|
||
'password': 'test'
|
||
})
|
||
|
||
assert response.status_code == 401
|
||
# 不應該成功登入
|
||
|
||
def test_xss_prevention(self):
|
||
"""測試: XSS防護"""
|
||
xss_script = "<script>alert('XSS')</script>"
|
||
token = self.get_admin_token()
|
||
|
||
response = self.client.post(
|
||
'/api/assessments',
|
||
headers={'Authorization': f'Bearer {token}'},
|
||
json={
|
||
'department': xss_script,
|
||
'position': '工程師',
|
||
'assessment_data': {}
|
||
}
|
||
)
|
||
|
||
# 應該被拒絕或清理
|
||
assert response.status_code in [400, 422]
|
||
|
||
def test_password_strength_validation(self):
|
||
"""測試: 密碼強度驗證"""
|
||
weak_passwords = [
|
||
'short', # 太短
|
||
'alllowercase', # 沒有大寫
|
||
'ALLUPPERCASE', # 沒有小寫
|
||
'NoNumbers!', # 沒有數字
|
||
'NoSpecial123' # 沒有特殊符號
|
||
]
|
||
|
||
for weak_pwd in weak_passwords:
|
||
response = self.client.post('/api/auth/register', json={
|
||
'username': 'testuser',
|
||
'email': 'test@test.com',
|
||
'password': weak_pwd,
|
||
'full_name': '測試',
|
||
'department': '技術部',
|
||
'position': '工程師'
|
||
})
|
||
|
||
assert response.status_code == 400
|
||
|
||
def test_rate_limiting(self):
|
||
"""測試: 登入失敗次數限制"""
|
||
for i in range(6): # 嘗試6次
|
||
response = self.client.post('/api/auth/login', json={
|
||
'username': 'testuser',
|
||
'password': 'wrong_password'
|
||
})
|
||
|
||
# 第6次應該被鎖定
|
||
assert response.status_code == 429 # Too Many Requests
|
||
|
||
12. 監控與維運
|
||
12.1 監控指標
|
||
12.1.1 系統監控
|
||
# 健康檢查端點
|
||
@app.route('/health')
|
||
def health_check():
|
||
"""系統健康檢查"""
|
||
checks = {
|
||
'status': 'healthy',
|
||
'timestamp': datetime.utcnow().isoformat(),
|
||
'checks': {}
|
||
}
|
||
|
||
# 資料庫連線檢查
|
||
try:
|
||
db.session.execute('SELECT 1')
|
||
checks['checks']['database'] = 'ok'
|
||
except Exception as e:
|
||
checks['status'] = 'unhealthy'
|
||
checks['checks']['database'] = f'error: {str(e)}'
|
||
|
||
# Redis連線檢查(如有使用)
|
||
try:
|
||
redis_client.ping()
|
||
checks['checks']['redis'] = 'ok'
|
||
except Exception as e:
|
||
checks['checks']['redis'] = f'error: {str(e)}'
|
||
|
||
# 磁碟空間檢查
|
||
import shutil
|
||
disk_usage = shutil.disk_usage('/')
|
||
disk_percent = (disk_usage.used / disk_usage.total) * 100
|
||
|
||
if disk_percent > 90:
|
||
checks['status'] = 'warning'
|
||
checks['checks']['disk'] = f'high usage: {disk_percent:.1f}%'
|
||
else:
|
||
checks['checks']['disk'] = f'ok: {disk_percent:.1f}%'
|
||
|
||
status_code = 200 if checks['status'] == 'healthy' else 503
|
||
return jsonify(checks), status_code
|
||
|
||
# 系統指標端點
|
||
@app.route('/metrics')
|
||
@permission_required('system:logs')
|
||
def system_metrics():
|
||
"""系統指標"""
|
||
import psutil
|
||
|
||
return jsonify({
|
||
'cpu_percent': psutil.cpu_percent(),
|
||
'memory_percent': psutil.virtual_memory().percent,
|
||
'disk_percent': psutil.disk_usage('/').percent,
|
||
'active_users': get_active_user_count(),
|
||
'api_calls_today': get_api_call_count_today(),
|
||
'average_response_time': get_average_response_time()
|
||
})
|
||
12.1.2 業務監控
|
||
# 業務指標端點
|
||
@app.route('/api/admin/statistics')
|
||
@permission_required('system:logs')
|
||
def business_statistics():
|
||
"""業務統計資料"""
|
||
from datetime import datetime, timedelta
|
||
|
||
today = datetime.now().date()
|
||
week_ago = today - timedelta(days=7)
|
||
month_ago = today - timedelta(days=30)
|
||
|
||
return jsonify({
|
||
# 用戶統計
|
||
'users': {
|
||
'total': User.query.filter_by(is_active=True).count(),
|
||
'new_this_week': User.query.filter(
|
||
User.created_at >= week_ago
|
||
).count(),
|
||
'active_today': get_active_users_today()
|
||
},
|
||
|
||
# 評估統計
|
||
'assessments': {
|
||
'total': Assessment.query.count(),
|
||
'this_month': Assessment.query.filter(
|
||
Assessment.created_at >= month_ago
|
||
).count()
|
||
},
|
||
|
||
# 回饋統計
|
||
'feedbacks': {
|
||
'total': StarFeedback.query.count(),
|
||
'this_month': StarFeedback.query.filter(
|
||
StarFeedback.feedback_date >= month_ago
|
||
).count(),
|
||
'average_score': db.session.query(
|
||
func.avg(StarFeedback.score)
|
||
).scalar()
|
||
},
|
||
|
||
# 排名統計
|
||
'rankings': {
|
||
'top_performer': get_top_performer(),
|
||
'most_improved': get_most_improved_employee()
|
||
}
|
||
})
|
||
12.2 日誌管理
|
||
# 日誌配置
|
||
import logging
|
||
from logging.handlers import RotatingFileHandler
|
||
|
||
def setup_logging(app):
|
||
"""設定日誌"""
|
||
|
||
# 應用程式日誌
|
||
if not app.debug:
|
||
file_handler = RotatingFileHandler(
|
||
'logs/app.log',
|
||
maxBytes=10485760, # 10MB
|
||
backupCount=10
|
||
)
|
||
file_handler.setFormatter(logging.Formatter(
|
||
'%(asctime)s %(levelname)s: %(message)s '
|
||
'[in %(pathname)s:%(lineno)d]'
|
||
))
|
||
file_handler.setLevel(logging.INFO)
|
||
app.logger.addHandler(file_handler)
|
||
app.logger.setLevel(logging.INFO)
|
||
app.logger.info('系統啟動')
|
||
|
||
# 安全日誌
|
||
security_handler = RotatingFileHandler(
|
||
'logs/security.log',
|
||
maxBytes=10485760,
|
||
backupCount=10
|
||
)
|
||
security_handler.setLevel(logging.WARNING)
|
||
security_logger = logging.getLogger('security')
|
||
security_logger.addHandler(security_handler)
|
||
|
||
return app
|
||
12.3 備份策略
|
||
#!/bin/bash
|
||
# backup.sh - 資料庫備份腳本
|
||
|
||
# 設定
|
||
DB_HOST="localhost"
|
||
DB_NAME="partner_alignment"
|
||
DB_USER="backup_user"
|
||
DB_PASS="backup_password"
|
||
BACKUP_DIR="/backup/mysql"
|
||
DATE=$(date +%Y%m%d_%H%M%S)
|
||
RETENTION_DAYS=30
|
||
|
||
# 建立備份目錄
|
||
mkdir -p $BACKUP_DIR
|
||
|
||
# 備份資料庫
|
||
echo "開始備份資料庫..."
|
||
mysqldump -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME \
|
||
| gzip > $BACKUP_DIR/backup_$DATE.sql.gz
|
||
|
||
# 檢查備份結果
|
||
if [ $? -eq 0 ]; then
|
||
echo "? 備份成功: backup_$DATE.sql.gz"
|
||
|
||
# 刪除舊備份
|
||
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
|
||
echo "? 清理超過${RETENTION_DAYS}天的舊備份"
|
||
else
|
||
echo "? 備份失敗"
|
||
exit 1
|
||
fi
|
||
|
||
# 上傳至雲端儲存(可選)
|
||
# aws s3 cp $BACKUP_DIR/backup_$DATE.sql.gz s3://your-bucket/backups/
|
||
|
||
echo "備份完成"
|
||
Crontab設定:
|
||
# 每天凌晨2點執行備份
|
||
0 2 * * * /path/to/backup.sh >> /var/log/backup.log 2>&1
|
||
|
||
13. 故障排除指南
|
||
13.1 常見問題
|
||
問題1: 無法登入系統
|
||
症狀: 登入時顯示「用戶名或密碼錯誤」
|
||
可能原因:
|
||
1. 密碼錯誤
|
||
2. 帳號被停用
|
||
3. 登入失敗次數過多被鎖定
|
||
解決方案:
|
||
-- 檢查帳號狀態
|
||
SELECT id, username, is_active, last_login_at
|
||
FROM users
|
||
WHERE username = 'username';
|
||
|
||
-- 重設密碼(管理員操作)
|
||
UPDATE users
|
||
SET password_hash = '$bcrypt_hash'
|
||
WHERE username = 'username';
|
||
|
||
-- 清除登入失敗記錄
|
||
DELETE FROM login_attempts
|
||
WHERE username = 'username'
|
||
AND attempt_time < NOW() - INTERVAL 1 HOUR;
|
||
|
||
-- 啟用帳號
|
||
UPDATE users
|
||
SET is_active = TRUE
|
||
WHERE username = 'username';
|
||
問題2: 排名顯示不正確
|
||
症狀: 儀表板排名與實際積分不符
|
||
可能原因:
|
||
1. 排名未及時更新
|
||
2. 積分計算錯誤
|
||
3. 快取問題
|
||
解決方案:
|
||
# 手動觸發排名更新
|
||
from app.services import update_all_rankings
|
||
update_all_rankings()
|
||
|
||
# 清除排名快取
|
||
redis_client.delete('rankings:*')
|
||
|
||
# 重新計算所有積分
|
||
from app.services import recalculate_all_points
|
||
recalculate_all_points()
|
||
問題3: 系統效能緩慢
|
||
症狀: API回應時間過長,頁面載入緩慢
|
||
診斷步驟:
|
||
# 1. 檢查慢查詢
|
||
SELECT * FROM mysql.slow_log ORDER BY query_time DESC LIMIT 10;
|
||
|
||
# 2. 檢查資料庫連線數
|
||
SHOW PROCESSLIST;
|
||
|
||
# 3. 檢查系統資源
|
||
# CPU、記憶體、磁碟I/O
|
||
|
||
# 4. 檢查應用程式日誌
|
||
tail -f logs/app.log | grep ERROR
|
||
優化措施:
|
||
* 新增資料庫索引
|
||
* 啟用查詢快取
|
||
* 增加伺服器資源
|
||
* 優化慢查詢
|
||
13.2 緊急處理流程
|
||
系統故障發生
|
||
↓
|
||
1. 確認影響範圍
|
||
- 部分功能異常 vs 系統完全無法存取
|
||
- 影響用戶數量
|
||
↓
|
||
2. 啟動應急措施
|
||
- 切換至備用系統(如有)
|
||
- 顯示維護公告
|
||
- 通知相關人員
|
||
↓
|
||
3. 問題診斷
|
||
- 查看錯誤日誌
|
||
- 檢查系統資源
|
||
- 確認最近變更
|
||
↓
|
||
4. 問題修復
|
||
- 回滾最近變更
|
||
- 修復程式碼錯誤
|
||
- 恢復資料庫
|
||
↓
|
||
5. 驗證測試
|
||
- 功能測試
|
||
- 效能測試
|
||
- 安全檢查
|
||
↓
|
||
6. 恢復服務
|
||
- 逐步開放存取
|
||
- 監控系統狀態
|
||
- 記錄事件報告
|
||
↓
|
||
7. 事後檢討
|
||
- 分析根本原因
|
||
- 改進預防措施
|
||
- 更新文件
|
||
|
||
14. 版本更新計畫
|
||
14.1 v2.1 規劃(短期)
|
||
預計時程: 1-2個月
|
||
功能清單:
|
||
* [ ] 行動裝置專用介面優化
|
||
* [ ] 推播通知系統
|
||
* [ ] 成就系統擴充(更多徽章)
|
||
* [ ] 社交功能(點讚、評論)
|
||
* [ ] 匯出PDF報表
|
||
* [ ] 多語言支援(英文)
|
||
14.2 v3.0 規劃(中期)
|
||
預計時程: 3-6個月
|
||
功能清單:
|
||
* [ ] AI智能推薦(能力發展建議)
|
||
* [ ] 360度回饋系統
|
||
* [ ] 職涯發展路徑規劃
|
||
* [ ] 整合HR系統(如有)
|
||
* [ ] 數據分析儀表板(管理層)
|
||
* [ ] 自訂KPI指標
|
||
14.3 v4.0 規劃(長期)
|
||
預計時程: 6-12個月
|
||
功能清單:
|
||
* [ ] 微服務架構重構
|
||
* [ ] 機器學習預測模型
|
||
* [ ] 區塊鏈認證(能力證書)
|
||
* [ ] 開放API平台
|
||
* [ ] 第三方整合(Slack、Teams)
|
||
* [ ] 雲端部署方案
|
||
|
||
15. 附錄
|
||
15.1 術語表(更新)
|
||
術語
|
||
定義
|
||
能力對齊
|
||
將員工能力與組織目標或職位要求進行匹配的過程
|
||
STAR框架
|
||
Situation-Task-Action-Result,結構化回饋方法
|
||
L1-L5
|
||
能力等級,從L1(執行者)到L5(策略制定者)
|
||
拖拉式介面
|
||
使用滑鼠拖拉操作的直觀式使用者介面
|
||
ORM
|
||
Object-Relational Mapping,物件關聯映射
|
||
JWT
|
||
JSON Web Token,用於身份驗證的Token標準
|
||
RBAC
|
||
Role-Based Access Control,基於角色的存取控制
|
||
百分比排名
|
||
表示勝過多少百分比同事的競賽指標
|
||
徽章系統
|
||
遊戲化激勵機制,透過成就徽章鼓勵參與
|
||
二次驗證
|
||
敏感操作需要再次輸入密碼確認
|
||
15.2 權限速查表
|
||
快速查詢常用權限:
|
||
# 一般用戶
|
||
USER_PERMISSIONS = [
|
||
'assessment:read', # 查看自己的評估
|
||
'feedback:read', # 查看自己的回饋
|
||
'ranking:read' # 查看排名
|
||
]
|
||
|
||
# 管理者
|
||
ADMIN_PERMISSIONS = USER_PERMISSIONS + [
|
||
'user:read', # 查看用戶
|
||
'user:update', # 更新用戶(部門內)
|
||
'assessment:create', # 建立評估
|
||
'assessment:read_all', # 查看所有評估(部門內)
|
||
'assessment:update', # 更新評估
|
||
'assessment:delete', # 刪除評估
|
||
'feedback:create', # 建立回饋
|
||
'feedback:read_all', # 查看所有回饋(部門內)
|
||
'ranking:read_department', # 查看部門排名
|
||
'ranking:read_all', # 查看全公司排名
|
||
'report:export' # 匯出報表
|
||
]
|
||
|
||
# 超級管理員
|
||
SUPER_ADMIN_PERMISSIONS = ADMIN_PERMISSIONS + [
|
||
'user:create', # 建立用戶
|
||
'user:delete', # 刪除用戶
|
||
'user:manage_roles', # 管理角色
|
||
'ranking:calculate', # 計算排名
|
||
'report:export_all', # 匯出所有報表
|
||
'system:config', # 系統設定
|
||
'system:logs', # 查看日誌
|
||
'system:backup' # 資料備份
|
||
]
|
||
15.3 API快速參考
|
||
認證相關:
|
||
* POST /api/auth/register - 註冊
|
||
* POST /api/auth/login - 登入
|
||
* POST /api/auth/logout - 登出
|
||
* POST /api/auth/refresh - 刷新Token
|
||
* GET /api/auth/me - 當前用戶資訊
|
||
儀表板相關:
|
||
* GET /api/dashboard - 個人儀表板
|
||
* GET /api/leaderboard - 排行榜
|
||
用戶管理:
|
||
* GET /api/admin/users - 用戶清單
|
||
* POST /api/admin/users - 建立用戶
|
||
* PUT /api/admin/users/:id - 更新用戶
|
||
* DELETE /api/admin/users/:id - 刪除用戶
|
||
評估管理:
|
||
* GET /api/capabilities - 能力項目清單
|
||
* POST /api/assessments - 提交評估
|
||
* GET /api/assessments - 查詢評估
|
||
回饋管理:
|
||
* POST /api/star-feedbacks - 提交回饋
|
||
* GET /api/star-feedbacks - 查詢回饋
|
||
排名系統:
|
||
* GET /api/rankings/total - 總積分排名
|
||
* GET /api/rankings/monthly - 月度排名
|
||
* POST /api/rankings/calculate - 計算排名
|
||
15.4 配置檢查清單(更新)
|
||
部署前檢查:
|
||
□ 已生成安全的 SECRET_KEY 和 JWT_SECRET_KEY
|
||
□ 已設定強密碼的資料庫帳號
|
||
□ 已關閉生產環境 DEBUG 模式
|
||
□ 已配置正確的 CORS_ORIGINS
|
||
□ 已啟用 HTTPS(Let's Encrypt或其他)
|
||
□ 已設定資料庫備份機制
|
||
□ 已建立預設管理員帳號
|
||
□ 已初始化角色與權限
|
||
□ 已設定定時任務(排名更新)
|
||
□ 已配置日誌記錄
|
||
□ 已設定監控告警
|
||
□ 已測試所有核心功能
|
||
□ 已準備故障恢復計畫
|
||
□ 已通知用戶系統上線
|
||
|
||
16. 核准簽署
|
||
角色
|
||
姓名
|
||
簽名
|
||
日期
|
||
專案經理
|
||
|
||
|
||
|
||
技術負責人
|
||
|
||
|
||
|
||
產品負責人
|
||
|
||
|
||
|
||
資訊安全負責人
|
||
|
||
|
||
|
||
品質保證
|
||
|
||
|
||
|
||
|
||
文件結束
|
||
版本: 2.0
|
||
最後更新: 2025年10月15日
|
||
下次審查: 2025年11月15日
|
||
|
||
?? 變更摘要 (v1.0 → v2.0)
|
||
?? 新增功能
|
||
1. 評分競賽系統
|
||
o 個人儀表板與即時排名
|
||
o 部門百分比排名顯示
|
||
o 徽章成就系統
|
||
o 積分趨勢圖表
|
||
o 競賽排行榜
|
||
2. 完整權限管理
|
||
o 用戶認證系統(JWT)
|
||
o 三層角色架構
|
||
o 細粒度權限控制
|
||
o 用戶管理後台
|
||
o 角色權限配置
|
||
o 操作日誌追蹤
|
||
?? 架構更新
|
||
* 新增6個資料表(users, roles, permissions等)
|
||
* 更新現有表格(新增用戶關聯)
|
||
* 新增20+個API端點
|
||
* 前端新增5個主要頁面
|
||
* 強化安全機制
|
||
?? 效能與安全
|
||
* 密碼加密(Bcrypt)
|
||
* Token認證(JWT)
|
||
* 登入失敗鎖定
|
||
* 二次密碼驗證
|
||
* 操作日誌完整記錄
|
||
* 健康檢查與監控
|
||
|