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>
616 lines
12 KiB
Markdown
616 lines
12 KiB
Markdown
# 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
|