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:
615
DEVELOPMENT_GUIDE.md
Normal file
615
DEVELOPMENT_GUIDE.md
Normal file
@@ -0,0 +1,615 @@
|
||||
# HR Portal v2.0 開發指南
|
||||
|
||||
本文件提供 HR Portal 專案的開發規範、最佳實踐和常見操作指南。
|
||||
|
||||
---
|
||||
|
||||
## 📋 目錄
|
||||
|
||||
1. [環境設置](#環境設置)
|
||||
2. [開發流程](#開發流程)
|
||||
3. [代碼規範](#代碼規範)
|
||||
4. [API 開發指南](#api-開發指南)
|
||||
5. [資料庫操作](#資料庫操作)
|
||||
6. [測試指南](#測試指南)
|
||||
7. [常見問題](#常見問題)
|
||||
|
||||
---
|
||||
|
||||
## 環境設置
|
||||
|
||||
### 前置需求
|
||||
|
||||
- Python 3.11+
|
||||
- Node.js 20+
|
||||
- Docker 24+
|
||||
- PostgreSQL 16+ (或使用 Docker)
|
||||
- Git
|
||||
|
||||
### 後端環境設置
|
||||
|
||||
```bash
|
||||
# 1. 克隆專案
|
||||
cd W:\DevOps-Workspace\3.Develop\4.HR_Portal
|
||||
|
||||
# 2. 創建 Python 虛擬環境
|
||||
cd backend
|
||||
python -m venv venv
|
||||
|
||||
# 3. 啟動虛擬環境
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
# Linux/Mac:
|
||||
source venv/bin/activate
|
||||
|
||||
# 4. 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 5. 配置環境變數
|
||||
cp .env.example .env
|
||||
# 編輯 .env 填入實際值
|
||||
|
||||
# 6. 啟動資料庫 (Docker)
|
||||
cd ../database
|
||||
docker-compose up -d
|
||||
cd ../backend
|
||||
|
||||
# 7. 啟動開發伺服器
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 資料庫初始化
|
||||
|
||||
```bash
|
||||
# 方式 1: SQLAlchemy 自動創建 (開發環境)
|
||||
# 啟動 FastAPI 時會自動創建表格
|
||||
|
||||
# 方式 2: 手動執行 SQL (生產環境推薦)
|
||||
cd database
|
||||
docker exec -i hr-portal-db-test psql -U hr_admin -d hr_portal < schema.sql
|
||||
|
||||
# 測試資料庫
|
||||
docker exec -i hr-portal-db-test psql -U hr_admin -d hr_portal < test_schema.sql
|
||||
```
|
||||
|
||||
### 前端環境設置 (待創建)
|
||||
|
||||
```bash
|
||||
# 待前端專案建立後補充
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 開發流程
|
||||
|
||||
### 1. 創建新功能
|
||||
|
||||
#### Step 1: 規劃
|
||||
1. 閱讀相關設計文件 (`2.專案設計區/4.HR_Portal/`)
|
||||
2. 確認需求和業務規則
|
||||
3. 設計 API 端點和資料結構
|
||||
|
||||
#### Step 2: 資料庫
|
||||
1. 更新 Schema (如需要)
|
||||
2. 創建 SQLAlchemy Model
|
||||
3. 測試 Model 關聯
|
||||
|
||||
#### Step 3: 資料驗證
|
||||
1. 創建 Pydantic Schemas
|
||||
2. 定義 Create/Update/Response Schemas
|
||||
3. 添加驗證規則和範例
|
||||
|
||||
#### Step 4: API 開發
|
||||
1. 創建 API 路由文件
|
||||
2. 實作端點邏輯
|
||||
3. 添加錯誤處理
|
||||
4. 測試 API
|
||||
|
||||
#### Step 5: 文檔
|
||||
1. 更新 API 文檔
|
||||
2. 添加使用範例
|
||||
3. 更新 PROGRESS.md
|
||||
|
||||
### 2. Git 工作流程
|
||||
|
||||
```bash
|
||||
# 1. 創建功能分支
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# 2. 開發和提交
|
||||
git add .
|
||||
git commit -m "feat: add your feature description"
|
||||
|
||||
# 3. 推送到遠端
|
||||
git push origin feature/your-feature-name
|
||||
|
||||
# 4. 創建 Pull Request
|
||||
# 在 GitHub/Gitea 上創建 PR
|
||||
|
||||
# 5. Code Review 後合併
|
||||
git checkout main
|
||||
git merge feature/your-feature-name
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 3. Commit 訊息規範
|
||||
|
||||
遵循 [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
**Type**:
|
||||
- `feat`: 新功能
|
||||
- `fix`: Bug 修復
|
||||
- `docs`: 文檔更新
|
||||
- `style`: 代碼格式調整
|
||||
- `refactor`: 重構
|
||||
- `test`: 測試
|
||||
- `chore`: 構建/工具相關
|
||||
|
||||
**範例**:
|
||||
```
|
||||
feat(api): add employee search endpoint
|
||||
|
||||
- Add keyword search for employee list
|
||||
- Support filtering by status
|
||||
- Add pagination support
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代碼規範
|
||||
|
||||
### Python 代碼風格
|
||||
|
||||
遵循 [PEP 8](https://pep8.org/) 和專案慣例:
|
||||
|
||||
```python
|
||||
# 1. 導入順序
|
||||
# 標準庫
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
# 第三方庫
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# 本地模組
|
||||
from app.db.session import get_db
|
||||
from app.models.employee import Employee
|
||||
from app.schemas.employee import EmployeeCreate
|
||||
|
||||
# 2. 函數命名: snake_case
|
||||
def get_employee_list():
|
||||
pass
|
||||
|
||||
# 3. 類別命名: PascalCase
|
||||
class EmployeeService:
|
||||
pass
|
||||
|
||||
# 4. 常數命名: UPPER_CASE
|
||||
MAX_PAGE_SIZE = 100
|
||||
|
||||
# 5. 型別提示
|
||||
def create_employee(
|
||||
employee_data: EmployeeCreate,
|
||||
db: Session = Depends(get_db)
|
||||
) -> EmployeeResponse:
|
||||
pass
|
||||
|
||||
# 6. Docstring (Google Style)
|
||||
def get_employee(employee_id: int, db: Session):
|
||||
"""
|
||||
獲取員工詳情
|
||||
|
||||
Args:
|
||||
employee_id: 員工 ID
|
||||
db: 資料庫 Session
|
||||
|
||||
Returns:
|
||||
Employee: 員工物件
|
||||
|
||||
Raises:
|
||||
HTTPException: 員工不存在時拋出 404
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 代碼組織
|
||||
|
||||
```python
|
||||
# API 端點結構
|
||||
@router.get("/", response_model=PaginatedResponse)
|
||||
def get_employees(
|
||||
db: Session = Depends(get_db),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
# 查詢參數
|
||||
status_filter: Optional[EmployeeStatus] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
):
|
||||
"""
|
||||
1. Docstring
|
||||
2. 查詢構建
|
||||
3. 分頁處理
|
||||
4. 返回結果
|
||||
"""
|
||||
# 構建查詢
|
||||
query = db.query(Employee)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(Employee.status == status_filter)
|
||||
|
||||
# 分頁
|
||||
total = query.count()
|
||||
offset = (pagination.page - 1) * pagination.page_size
|
||||
items = query.offset(offset).limit(pagination.page_size).all()
|
||||
|
||||
# 返回
|
||||
return PaginatedResponse(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 開發指南
|
||||
|
||||
### 1. 創建新的 API 端點
|
||||
|
||||
#### 範例: 創建「員工統計」端點
|
||||
|
||||
**Step 1: 創建 Schema**
|
||||
```python
|
||||
# app/schemas/employee.py
|
||||
class EmployeeStats(BaseSchema):
|
||||
total_employees: int
|
||||
active_employees: int
|
||||
by_business_unit: Dict[str, int]
|
||||
```
|
||||
|
||||
**Step 2: 實作端點**
|
||||
```python
|
||||
# app/api/v1/employees.py
|
||||
@router.get("/stats", response_model=EmployeeStats)
|
||||
def get_employee_stats(db: Session = Depends(get_db)):
|
||||
"""獲取員工統計"""
|
||||
total = db.query(Employee).count()
|
||||
active = db.query(Employee).filter(
|
||||
Employee.status == EmployeeStatus.ACTIVE
|
||||
).count()
|
||||
|
||||
# 按事業部統計
|
||||
from sqlalchemy import func
|
||||
by_bu = db.query(
|
||||
BusinessUnit.name,
|
||||
func.count(EmployeeIdentity.id)
|
||||
).join(EmployeeIdentity).group_by(BusinessUnit.name).all()
|
||||
|
||||
return EmployeeStats(
|
||||
total_employees=total,
|
||||
active_employees=active,
|
||||
by_business_unit={name: count for name, count in by_bu}
|
||||
)
|
||||
```
|
||||
|
||||
**Step 3: 測試**
|
||||
```bash
|
||||
curl http://localhost:8000/api/v1/employees/stats
|
||||
```
|
||||
|
||||
### 2. 錯誤處理
|
||||
|
||||
```python
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
# 404 - 資源不存在
|
||||
if not employee:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Employee with id {employee_id} not found"
|
||||
)
|
||||
|
||||
# 400 - 請求驗證失敗
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Username '{username}' already exists"
|
||||
)
|
||||
|
||||
# 403 - 權限不足
|
||||
if not has_permission:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Permission denied"
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 分頁實作
|
||||
|
||||
```python
|
||||
from app.schemas.base import PaginationParams, PaginatedResponse
|
||||
from app.api.deps import get_pagination_params
|
||||
|
||||
@router.get("/", response_model=PaginatedResponse)
|
||||
def get_items(
|
||||
db: Session = Depends(get_db),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
):
|
||||
query = db.query(Model)
|
||||
|
||||
# 總數
|
||||
total = query.count()
|
||||
|
||||
# 分頁
|
||||
offset = (pagination.page - 1) * pagination.page_size
|
||||
items = query.offset(offset).limit(pagination.page_size).all()
|
||||
|
||||
# 總頁數
|
||||
total_pages = (total + pagination.page_size - 1) // pagination.page_size
|
||||
|
||||
return PaginatedResponse(
|
||||
total=total,
|
||||
page=pagination.page,
|
||||
page_size=pagination.page_size,
|
||||
total_pages=total_pages,
|
||||
items=items
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 資料庫操作
|
||||
|
||||
### 1. 查詢範例
|
||||
|
||||
```python
|
||||
# 簡單查詢
|
||||
employee = db.query(Employee).filter(Employee.id == 1).first()
|
||||
|
||||
# 多條件查詢
|
||||
employees = db.query(Employee).filter(
|
||||
Employee.status == EmployeeStatus.ACTIVE,
|
||||
Employee.hire_date >= date(2020, 1, 1)
|
||||
).all()
|
||||
|
||||
# Join 查詢
|
||||
results = db.query(Employee, EmployeeIdentity).join(
|
||||
EmployeeIdentity,
|
||||
Employee.id == EmployeeIdentity.employee_id
|
||||
).all()
|
||||
|
||||
# 搜尋 (LIKE)
|
||||
search_pattern = f"%{keyword}%"
|
||||
employees = db.query(Employee).filter(
|
||||
Employee.legal_name.ilike(search_pattern)
|
||||
).all()
|
||||
|
||||
# 排序
|
||||
employees = db.query(Employee).order_by(
|
||||
Employee.hire_date.desc()
|
||||
).all()
|
||||
|
||||
# 分組統計
|
||||
from sqlalchemy import func
|
||||
stats = db.query(
|
||||
Employee.status,
|
||||
func.count(Employee.id)
|
||||
).group_by(Employee.status).all()
|
||||
```
|
||||
|
||||
### 2. 創建/更新/刪除
|
||||
|
||||
```python
|
||||
# 創建
|
||||
employee = Employee(
|
||||
employee_id="EMP001",
|
||||
username_base="john.doe",
|
||||
legal_name="John Doe"
|
||||
)
|
||||
db.add(employee)
|
||||
db.commit()
|
||||
db.refresh(employee) # 重新載入以獲取自動生成的欄位
|
||||
|
||||
# 更新
|
||||
employee.legal_name = "Jane Doe"
|
||||
db.commit()
|
||||
|
||||
# 批量更新
|
||||
db.query(Employee).filter(
|
||||
Employee.status == EmployeeStatus.ACTIVE
|
||||
).update({"status": EmployeeStatus.INACTIVE})
|
||||
db.commit()
|
||||
|
||||
# 刪除 (實際刪除,慎用!)
|
||||
db.delete(employee)
|
||||
db.commit()
|
||||
|
||||
# 軟刪除 (推薦)
|
||||
employee.is_active = False
|
||||
db.commit()
|
||||
```
|
||||
|
||||
### 3. 事務處理
|
||||
|
||||
```python
|
||||
try:
|
||||
# 開始事務
|
||||
employee = Employee(...)
|
||||
db.add(employee)
|
||||
db.flush() # 獲取 ID 但不提交
|
||||
|
||||
identity = EmployeeIdentity(
|
||||
employee_id=employee.id,
|
||||
...
|
||||
)
|
||||
db.add(identity)
|
||||
|
||||
# 提交事務
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
# 回滾
|
||||
db.rollback()
|
||||
raise
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試指南
|
||||
|
||||
### 1. 單元測試 (待實作)
|
||||
|
||||
```python
|
||||
# tests/test_employees.py
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
def test_create_employee(client: TestClient, db: Session):
|
||||
response = client.post(
|
||||
"/api/v1/employees/",
|
||||
json={
|
||||
"username_base": "test.user",
|
||||
"legal_name": "Test User",
|
||||
"hire_date": "2020-01-01"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["username_base"] == "test.user"
|
||||
|
||||
def test_get_employee_not_found(client: TestClient):
|
||||
response = client.get("/api/v1/employees/99999")
|
||||
assert response.status_code == 404
|
||||
```
|
||||
|
||||
### 2. 整合測試 (待實作)
|
||||
|
||||
```python
|
||||
def test_employee_full_lifecycle(client: TestClient, db: Session):
|
||||
# 1. 創建員工
|
||||
# 2. 創建身份
|
||||
# 3. 創建 NAS
|
||||
# 4. 查詢驗證
|
||||
# 5. 更新
|
||||
# 6. 停用
|
||||
# 7. 驗證所有資源已停用
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常見問題
|
||||
|
||||
### Q1: 如何添加新的 API 端點?
|
||||
|
||||
1. 在對應的路由文件中添加端點函數
|
||||
2. 定義 Pydantic Schema (如需要)
|
||||
3. 實作業務邏輯
|
||||
4. 測試端點
|
||||
5. 更新文檔
|
||||
|
||||
### Q2: 如何修改資料庫 Schema?
|
||||
|
||||
**開發環境**:
|
||||
1. 修改 `database/schema.sql`
|
||||
2. 修改對應的 SQLAlchemy Model
|
||||
3. 重新創建資料庫
|
||||
|
||||
**生產環境** (使用 Alembic,待實作):
|
||||
1. 創建遷移腳本: `alembic revision --autogenerate -m "description"`
|
||||
2. 檢查遷移腳本
|
||||
3. 執行遷移: `alembic upgrade head`
|
||||
|
||||
### Q3: 如何處理循環導入?
|
||||
|
||||
使用字串引用型別:
|
||||
```python
|
||||
# 不要這樣
|
||||
from app.models.employee import Employee
|
||||
|
||||
class EmployeeIdentity(Base):
|
||||
employee: Mapped["Employee"] = relationship(...)
|
||||
|
||||
# 應該這樣
|
||||
class EmployeeIdentity(Base):
|
||||
employee: Mapped["Employee"] = relationship(...)
|
||||
```
|
||||
|
||||
### Q4: 如何調試 API?
|
||||
|
||||
1. **使用 Swagger UI**: http://localhost:8000/docs
|
||||
2. **查看日誌**: 終端輸出
|
||||
3. **使用 Python 調試器**:
|
||||
```python
|
||||
import pdb; pdb.set_trace()
|
||||
```
|
||||
4. **檢查 SQL 查詢**:
|
||||
```python
|
||||
# 在 config.py 設置
|
||||
DATABASE_ECHO = True
|
||||
```
|
||||
|
||||
### Q5: 如何重置資料庫?
|
||||
|
||||
```bash
|
||||
# 停止並刪除容器和資料
|
||||
cd database
|
||||
docker-compose down -v
|
||||
|
||||
# 重新啟動
|
||||
docker-compose up -d
|
||||
|
||||
# 重新初始化
|
||||
docker exec -i hr-portal-db-test psql -U hr_admin -d hr_portal < schema.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳實踐
|
||||
|
||||
### 1. API 設計
|
||||
- ✅ 使用 RESTful 規範
|
||||
- ✅ 返回正確的 HTTP 狀態碼
|
||||
- ✅ 提供清晰的錯誤訊息
|
||||
- ✅ 支援分頁和篩選
|
||||
- ✅ 使用 Pydantic 驗證請求
|
||||
|
||||
### 2. 資料庫
|
||||
- ✅ 使用外鍵約束
|
||||
- ✅ 添加索引優化查詢
|
||||
- ✅ 使用軟刪除
|
||||
- ✅ 記錄審計日誌
|
||||
- ✅ 避免 N+1 查詢問題
|
||||
|
||||
### 3. 安全
|
||||
- ✅ 使用 JWT Token 認證
|
||||
- ✅ 實作權限控制
|
||||
- ✅ 驗證所有輸入
|
||||
- ✅ 使用 HTTPS
|
||||
- ✅ 不在日誌中記錄敏感資訊
|
||||
|
||||
### 4. 性能
|
||||
- ✅ 使用連線池
|
||||
- ✅ 添加適當的索引
|
||||
- ✅ 使用分頁
|
||||
- ✅ 考慮使用快取
|
||||
- ✅ 優化 SQL 查詢
|
||||
|
||||
---
|
||||
|
||||
## 相關資源
|
||||
|
||||
- [FastAPI 官方文檔](https://fastapi.tiangolo.com/)
|
||||
- [SQLAlchemy 文檔](https://docs.sqlalchemy.org/)
|
||||
- [Pydantic 文檔](https://docs.pydantic.dev/)
|
||||
- [PostgreSQL 文檔](https://www.postgresql.org/docs/)
|
||||
|
||||
---
|
||||
|
||||
**最後更新**: 2026-02-11
|
||||
**維護者**: Porsche Chen
|
||||
Reference in New Issue
Block a user