Files
hr-portal/DEVELOPMENT_GUIDE.md
Porsche Chen 360533393f 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>
2026-02-23 20:12:43 +08:00

12 KiB

HR Portal v2.0 開發指南

本文件提供 HR Portal 專案的開發規範、最佳實踐和常見操作指南。


📋 目錄

  1. 環境設置
  2. 開發流程
  3. 代碼規範
  4. API 開發指南
  5. 資料庫操作
  6. 測試指南
  7. 常見問題

環境設置

前置需求

  • Python 3.11+
  • Node.js 20+
  • Docker 24+
  • PostgreSQL 16+ (或使用 Docker)
  • Git

後端環境設置

# 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

資料庫初始化

# 方式 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

前端環境設置 (待創建)

# 待前端專案建立後補充

開發流程

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 工作流程

# 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:

<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 和專案慣例:

# 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

代碼組織

# 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

# app/schemas/employee.py
class EmployeeStats(BaseSchema):
    total_employees: int
    active_employees: int
    by_business_unit: Dict[str, int]

Step 2: 實作端點

# 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: 測試

curl http://localhost:8000/api/v1/employees/stats

2. 錯誤處理

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. 分頁實作

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. 查詢範例

# 簡單查詢
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. 創建/更新/刪除

# 創建
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. 事務處理

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. 單元測試 (待實作)

# 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. 整合測試 (待實作)

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: 如何處理循環導入?

使用字串引用型別:

# 不要這樣
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 調試器:
    import pdb; pdb.set_trace()
    
  4. 檢查 SQL 查詢:
    # 在 config.py 設置
    DATABASE_ECHO = True
    

Q5: 如何重置資料庫?

# 停止並刪除容器和資料
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 查詢

相關資源


最後更新: 2026-02-11 維護者: Porsche Chen