Files
hr-portal/backend/app/api/v1/system_functions.py
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

304 lines
9.0 KiB
Python

"""
System Functions API
系統功能明細 CRUD API
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from app.db.session import get_db
from app.models.system_function import SystemFunction
from app.schemas.system_function import (
SystemFunctionCreate,
SystemFunctionUpdate,
SystemFunctionResponse,
SystemFunctionListResponse
)
from app.api.deps import get_pagination_params
from app.schemas.base import PaginationParams
router = APIRouter()
@router.get("", response_model=SystemFunctionListResponse)
def get_system_functions(
function_type: Optional[int] = Query(None, description="功能類型 (1:node, 2:function)"),
upper_function_id: Optional[int] = Query(None, description="上層功能代碼"),
is_mana: Optional[bool] = Query(None, description="系統管理"),
is_active: Optional[bool] = Query(None, description="啟用(預設顯示全部)"),
search: Optional[str] = Query(None, description="搜尋 (code or name)"),
pagination: PaginationParams = Depends(get_pagination_params),
db: Session = Depends(get_db),
):
"""
取得系統功能列表
- 支援分頁
- 支援篩選 (function_type, upper_function_id, is_mana, is_active)
- 支援搜尋 (code or name)
"""
query = db.query(SystemFunction)
# 篩選條件
filters = []
if function_type is not None:
filters.append(SystemFunction.function_type == function_type)
if upper_function_id is not None:
filters.append(SystemFunction.upper_function_id == upper_function_id)
if is_mana is not None:
filters.append(SystemFunction.is_mana == is_mana)
if is_active is not None:
filters.append(SystemFunction.is_active == is_active)
# 搜尋
if search:
filters.append(
or_(
SystemFunction.code.ilike(f"%{search}%"),
SystemFunction.name.ilike(f"%{search}%")
)
)
if filters:
query = query.filter(and_(*filters))
# 排序 (依照 order 排序)
query = query.order_by(SystemFunction.order.asc())
# 計算總數
total = query.count()
# 分頁
offset = (pagination.page - 1) * pagination.page_size
items = query.offset(offset).limit(pagination.page_size).all()
return SystemFunctionListResponse(
total=total,
items=items,
page=pagination.page,
page_size=pagination.page_size
)
@router.get("/{function_id}", response_model=SystemFunctionResponse)
def get_system_function(
function_id: int,
db: Session = Depends(get_db),
):
"""
取得單一系統功能
"""
function = db.query(SystemFunction).filter(SystemFunction.id == function_id).first()
if not function:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"System function not found: {function_id}"
)
return function
@router.post("", response_model=SystemFunctionResponse, status_code=status.HTTP_201_CREATED)
def create_system_function(
function_in: SystemFunctionCreate,
db: Session = Depends(get_db),
):
"""
建立系統功能
驗證規則:
- function_type=1 (node) 時, module_code 不能輸入
- function_type=2 (function) 時, module_code 和 module_functions 為必填
- upper_function_id 必須是 function_type=1 且 is_active=1 的功能, 或 0 (初始層)
"""
# 驗證 upper_function_id
if function_in.upper_function_id > 0:
parent = db.query(SystemFunction).filter(
SystemFunction.id == function_in.upper_function_id,
SystemFunction.function_type == 1,
SystemFunction.is_active == True
).first()
if not parent:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid upper_function_id: {function_in.upper_function_id} "
"(must be function_type=1 and is_active=1)"
)
# 檢查 code 是否重複
existing = db.query(SystemFunction).filter(SystemFunction.code == function_in.code).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"System function code already exists: {function_in.code}"
)
# 建立資料
db_function = SystemFunction(**function_in.model_dump())
db.add(db_function)
db.commit()
db.refresh(db_function)
return db_function
@router.put("/{function_id}", response_model=SystemFunctionResponse)
def update_system_function(
function_id: int,
function_in: SystemFunctionUpdate,
db: Session = Depends(get_db),
):
"""
更新系統功能 (完整更新)
"""
# 查詢現有資料
db_function = db.query(SystemFunction).filter(SystemFunction.id == function_id).first()
if not db_function:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"System function not found: {function_id}"
)
# 更新資料
update_data = function_in.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_function, field, value)
db.commit()
db.refresh(db_function)
return db_function
@router.patch("/{function_id}", response_model=SystemFunctionResponse)
def patch_system_function(
function_id: int,
function_in: SystemFunctionUpdate,
db: Session = Depends(get_db),
):
"""
更新系統功能 (部分更新)
"""
return update_system_function(function_id, function_in, db)
@router.delete("/{function_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_system_function(
function_id: int,
db: Session = Depends(get_db),
):
"""
刪除系統功能 (實際上是軟刪除, 設定 is_active=False)
"""
db_function = db.query(SystemFunction).filter(SystemFunction.id == function_id).first()
if not db_function:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"System function not found: {function_id}"
)
# 軟刪除
db_function.is_active = False
db.commit()
return None
@router.delete("/{function_id}/hard", status_code=status.HTTP_204_NO_CONTENT)
def hard_delete_system_function(
function_id: int,
db: Session = Depends(get_db),
):
"""
永久刪除系統功能 (硬刪除)
⚠️ 警告: 此操作無法復原
"""
db_function = db.query(SystemFunction).filter(SystemFunction.id == function_id).first()
if not db_function:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"System function not found: {function_id}"
)
# 硬刪除
db.delete(db_function)
db.commit()
return None
@router.get("/menu/tree", response_model=List[dict])
def get_menu_tree(
is_sysmana: bool = Query(False, description="是否為系統管理公司"),
db: Session = Depends(get_db),
):
"""
取得功能列表樹狀結構 (用於前端選單顯示)
根據 is_sysmana 過濾功能:
- is_sysmana=true: 返回所有功能 (包含 is_mana=true 的系統管理功能)
- is_sysmana=false: 只返回 is_mana=false 的一般功能
返回格式:
[
{
"id": 10,
"code": "system_managements",
"name": "系統管理後台",
"function_type": 1,
"order": 100,
"function_icon": "",
"module_code": null,
"module_functions": [],
"children": [
{
"id": 11,
"code": "system_settings",
"name": "系統資料設定",
"function_type": 2,
...
}
]
}
]
"""
# 查詢條件
query = db.query(SystemFunction).filter(SystemFunction.is_active == True)
# 如果不是系統管理公司,過濾掉 is_mana=true 的功能
if not is_sysmana:
query = query.filter(SystemFunction.is_mana == False)
# 排序
functions = query.order_by(SystemFunction.order.asc()).all()
# 建立樹狀結構
def build_tree(parent_id: int = 0) -> List[dict]:
tree = []
for func in functions:
if func.upper_function_id == parent_id:
node = {
"id": func.id,
"code": func.code,
"name": func.name,
"function_type": func.function_type,
"order": func.order,
"function_icon": func.function_icon or "",
"module_code": func.module_code,
"module_functions": func.module_functions or [],
"description": func.description or "",
"children": build_tree(func.id) if func.function_type == 1 else []
}
tree.append(node)
return tree
return build_tree(0)