feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage

Major Features:
-  Multi-tenant architecture (tenant isolation)
-  Employee CRUD with lifecycle management (onboarding/offboarding)
-  Department tree structure with email domain management
-  Company info management (single-record editing)
-  System functions CRUD (permission management)
-  Email account management (multi-account per employee)
-  Keycloak SSO integration (auth.lab.taipei)
-  Redis session storage (10.1.0.254:6379)
  - Solves Cookie 4KB limitation
  - Cross-system session sharing
  - Sliding expiration (8 hours)
  - Automatic token refresh

Technical Stack:
Backend:
- FastAPI + SQLAlchemy
- PostgreSQL 16 (10.1.0.20:5433)
- Keycloak Admin API integration
- Docker Mailserver integration (SSH)
- Alembic migrations

Frontend:
- Next.js 14 (App Router)
- NextAuth 4 with Keycloak Provider
- Redis session storage (ioredis)
- Tailwind CSS

Infrastructure:
- Redis 7 (10.1.0.254:6379) - Session + Cache
- Keycloak 26.1.0 (auth.lab.taipei)
- Docker Mailserver (10.1.0.254)

Architecture Highlights:
- Session管理由 Keycloak + Redis 統一控制
- 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session
- Token 自動刷新,異質服務整合
- 未來可無縫遷移到雲端

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 20:12:43 +08:00
commit 360533393f
386 changed files with 70353 additions and 0 deletions

390
test_onboarding.md Normal file
View File

@@ -0,0 +1,390 @@
# 員工到職流程測試指南 (v3.1)
## 測試環境
- **Migration 版本**: 0012 (已執行)
- **資料庫**: hr_portal @ 10.1.0.20:5433
- **後端 API**: http://localhost:10181/api/v1
- **前端**: http://localhost:10180
---
## API 端點清單
### 1. 個人化服務管理
```bash
# 取得所有可用服務
GET /api/v1/personal-services/services
# 查詢使用者已啟用服務
GET /api/v1/personal-services/users/{keycloak_user_id}/services
# 啟用單一服務
POST /api/v1/personal-services/users/{keycloak_user_id}/services
{
"service_id": 1,
"quota_gb": 20,
"quota_mb": 5120
}
# 批次啟用所有服務
POST /api/v1/personal-services/users/{keycloak_user_id}/services/batch-enable
{
"storage_quota_gb": 20,
"email_quota_mb": 5120
}
# 停用服務
DELETE /api/v1/personal-services/users/{keycloak_user_id}/services/{service_id}
```
---
### 2. 員工到職流程 (完整)
```bash
# 員工到職
POST /api/v1/emp-lifecycle/onboard
Content-Type: application/json
{
"resume_id": 1,
"keycloak_user_id": "550e8400-e29b-41d4-a716-446655440000",
"keycloak_username": "wang.ming",
"hire_date": "2026-02-20",
"departments": [
{
"department_id": 9,
"position": "資深工程師",
"membership_type": "permanent"
},
{
"department_id": 12,
"position": "專案經理",
"membership_type": "project"
}
],
"role_ids": [1, 2],
"storage_quota_gb": 20,
"email_quota_mb": 5120
}
```
**預期回應**:
```json
{
"message": "Employee onboarded successfully",
"employee": {
"tenant_id": 1,
"seq_no": 1,
"tenant_emp_code": "PWD0001",
"keycloak_user_id": "550e8400-e29b-41d4-a716-446655440000",
"keycloak_username": "wang.ming",
"name": "王明",
"hire_date": "2026-02-20"
},
"summary": {
"departments_assigned": 2,
"roles_assigned": 2,
"services_enabled": 5
}
}
```
---
### 3. 查詢員工狀態
```bash
# 查詢完整狀態
GET /api/v1/emp-lifecycle/1/1/status
# 回應範例
{
"employee": {
"tenant_id": 1,
"seq_no": 1,
"tenant_emp_code": "PWD0001",
"name": "王明",
"keycloak_user_id": "550e8400-e29b-41d4-a716-446655440000",
"keycloak_username": "wang.ming",
"hire_date": "2026-02-20",
"resign_date": null,
"employment_status": "active",
"storage_quota_gb": 20,
"email_quota_mb": 5120
},
"departments": [
{
"department_id": 9,
"department_name": "玄鐵風能",
"position": "資深工程師",
"membership_type": "permanent",
"joined_at": "2026-02-20T10:00:00"
}
],
"roles": [
{
"role_id": 1,
"role_name": "HR管理員",
"role_code": "HR_ADMIN",
"assigned_at": "2026-02-20T10:00:00"
}
],
"services": [
{
"service_id": 1,
"service_name": "單一簽入",
"service_code": "SSO",
"quota_gb": null,
"quota_mb": null,
"enabled_at": "2026-02-20T10:00:00"
},
{
"service_id": 4,
"service_name": "網路硬碟",
"service_code": "Drive",
"quota_gb": 20,
"quota_mb": null,
"enabled_at": "2026-02-20T10:00:00"
}
]
}
```
---
### 4. 員工離職流程
```bash
# 員工離職
POST /api/v1/emp-lifecycle/1/1/offboard
# 回應範例
{
"message": "Employee offboarded successfully",
"employee": {
"tenant_emp_code": "PWD0001",
"resign_date": "2026-02-20"
},
"summary": {
"departments_removed": 2,
"roles_revoked": 2,
"services_disabled": 5
}
}
```
---
## 資料庫驗證查詢
### 檢查員工任用設定
```sql
SELECT
tenant_id,
seq_no,
tenant_emp_code,
tenant_keycloak_user_id,
tenant_keycloak_username,
hire_at,
storage_quota_gb,
email_quota_mb,
employment_status,
is_active
FROM tenant_emp_settings
WHERE tenant_id = 1;
```
### 檢查部門歸屬
```sql
SELECT
dm.id,
dm.employee_id,
dm.department_id,
d.name AS department_name,
dm.position,
dm.membership_type,
dm.joined_at,
dm.ended_at,
dm.assigned_by,
dm.is_active
FROM tenant_dept_members dm
LEFT JOIN tenant_departments d ON dm.department_id = d.id
WHERE dm.tenant_id = 1
ORDER BY dm.employee_id, dm.joined_at;
```
### 檢查角色分配
```sql
SELECT
ra.id,
ra.keycloak_user_id,
ra.role_id,
r.role_code,
r.role_name,
ra.assigned_at,
ra.revoked_at,
ra.assigned_by,
ra.is_active
FROM tenant_user_role_assignments ra
LEFT JOIN tenant_user_roles r ON ra.role_id = r.id
WHERE ra.tenant_id = 1
ORDER BY ra.keycloak_user_id, ra.assigned_at;
```
### 檢查個人化服務
```sql
SELECT
ss.id,
ss.tenant_keycloak_user_id,
ss.service_id,
s.service_name,
s.service_code,
ss.quota_gb,
ss.quota_mb,
ss.enabled_at,
ss.disabled_at,
ss.enabled_by,
ss.is_active
FROM tenant_emp_personal_service_settings ss
LEFT JOIN personal_services s ON ss.service_id = s.id
WHERE ss.tenant_id = 1
ORDER BY ss.tenant_keycloak_user_id, ss.service_id;
```
---
## 測試步驟
### 前置準備
1. **確認資料庫狀態**:
```bash
cd q:/porscheworld_develop/hr-portal/backend
python -m alembic current
# 應顯示: 0012 (head)
```
2. **啟動後端服務**:
```bash
cd q:/porscheworld_develop/hr-portal
START_BACKEND.bat
```
3. **確認服務運行**:
```bash
curl http://localhost:10181/api/v1/docs
```
### 測試流程
#### Step 1: 建立測試用人員基本資料
```sql
-- 手動建立測試資料 (或透過 API)
INSERT INTO tenant_emp_resumes (tenant_id, name_tw, name_eng)
VALUES (1, '王明', 'Ming Wang')
RETURNING id;
-- 假設回傳 id = 1
```
#### Step 2: 執行到職流程
使用 Postman 或 curl:
```bash
curl -X POST http://localhost:10181/api/v1/emp-lifecycle/onboard \
-H "Content-Type: application/json" \
-d '{
"resume_id": 1,
"keycloak_user_id": "550e8400-e29b-41d4-a716-446655440000",
"keycloak_username": "wang.ming",
"hire_date": "2026-02-20",
"departments": [
{"department_id": 1, "position": "工程師"}
],
"role_ids": [1],
"storage_quota_gb": 20,
"email_quota_mb": 5120
}'
```
#### Step 3: 驗證資料
```bash
# 查詢狀態
curl http://localhost:10181/api/v1/emp-lifecycle/1/1/status
```
#### Step 4: 執行離職流程
```bash
curl -X POST http://localhost:10181/api/v1/emp-lifecycle/1/1/offboard
```
---
## 預期結果
### 到職後應建立的資料
1. ✅ `tenant_emp_settings`: 1 筆記錄
2. ✅ `tenant_dept_members`: N 筆記錄(依 departments 數量)
3. ✅ `tenant_user_role_assignments`: N 筆記錄(依 role_ids 數量)
4. ✅ `tenant_emp_personal_service_settings`: 5 筆記錄(所有服務)
### 離職後應更新的資料
1. ✅ `tenant_emp_settings.employment_status` = 'resigned'
2. ✅ `tenant_emp_settings.resign_date` = 當天日期
3. ✅ `tenant_dept_members.is_active` = false, `ended_at` = 當前時間
4. ✅ `tenant_user_role_assignments.is_active` = false, `revoked_at` = 當前時間
5. ✅ `tenant_emp_personal_service_settings.is_active` = false, `disabled_at` = 當前時間
---
## 故障排除
### 問題 1: Port 被占用
```bash
# Windows
netstat -ano | findstr :10181
taskkill /PID <PID> /F
```
### 問題 2: 資料庫連線失敗
檢查連線字串:
```
postgresql://admin:DC1qaz2wsx@10.1.0.20:5433/hr_portal
```
### 問題 3: Migration 版本不對
```bash
cd q:/porscheworld_develop/hr-portal/backend
python -m alembic upgrade head
```
---
## 注意事項
⚠️ **重要**:
- 此為測試環境,請勿在正式資料上測試
- 測試前請備份資料庫
- Keycloak User ID 必須是有效的 UUID 格式
- 部門 ID 和角色 ID 必須在資料庫中存在
---
**測試文件版本**: v1.0
**建立日期**: 2026-02-20
**適用架構**: HR Portal v3.1 (多租戶 + 關聯表)