Files
hr-portal/backend/app/schemas/department.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

126 lines
3.8 KiB
Python

"""
部門 Schemas (統一樹狀結構)
"""
from datetime import datetime
from typing import Optional, List, Any
from pydantic import Field, ConfigDict
from app.schemas.base import BaseSchema
class DepartmentBase(BaseSchema):
"""部門基礎 Schema"""
name: str = Field(..., min_length=2, max_length=100, description="部門名稱")
name_en: Optional[str] = Field(None, max_length=100, description="英文名稱")
code: str = Field(..., min_length=1, max_length=20, description="部門代碼")
description: Optional[str] = Field(None, description="說明")
class DepartmentCreate(DepartmentBase):
"""創建部門 Schema
- parent_id=NULL: 建立第一層部門,可設定 email_domain
- parent_id=有值: 建立子部門,不可設定 email_domain (繼承)
"""
parent_id: Optional[int] = Field(None, description="上層部門 ID (NULL=第一層)")
email_domain: Optional[str] = Field(None, max_length=100,
description="郵件網域 (只有第一層可設定,例如 ease.taipei)")
email_address: Optional[str] = Field(None, max_length=255, description="部門信箱")
email_quota_mb: Optional[int] = Field(5120, description="部門信箱配額 (MB)")
model_config = ConfigDict(
json_schema_extra={
"example": {
"parent_id": None,
"name": "業務發展部",
"name_en": "Business Development",
"code": "BD",
"email_domain": "ease.taipei",
"description": "業務發展相關部門"
}
}
)
class DepartmentUpdate(BaseSchema):
"""更新部門 Schema
注意: code 和 parent_id 建立後不可修改
email_domain 只有第一層 (depth=0) 可更新
"""
name: Optional[str] = Field(None, min_length=2, max_length=100)
name_en: Optional[str] = Field(None, max_length=100)
description: Optional[str] = None
email_domain: Optional[str] = Field(None, max_length=100,
description="只有第一層部門可更新")
email_address: Optional[str] = Field(None, max_length=255)
email_quota_mb: Optional[int] = None
is_active: Optional[bool] = None
class DepartmentResponse(BaseSchema):
"""部門響應 Schema"""
id: int
tenant_id: int
parent_id: Optional[int] = None
code: str
name: str
name_en: Optional[str] = None
depth: int
email_domain: Optional[str] = None
effective_email_domain: Optional[str] = None
email_address: Optional[str] = None
email_quota_mb: int
description: Optional[str] = None
is_active: bool
is_top_level: bool = False
created_at: datetime
parent_name: Optional[str] = None
member_count: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
class DepartmentListItem(BaseSchema):
"""部門列表項 Schema"""
id: int
tenant_id: int
parent_id: Optional[int] = None
code: str
name: str
depth: int
email_domain: Optional[str] = None
effective_email_domain: Optional[str] = None
email_address: Optional[str] = None
is_active: bool
member_count: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
class DepartmentTreeNode(BaseSchema):
"""部門樹狀節點 Schema (遞迴)"""
id: int
code: str
name: str
name_en: Optional[str] = None
depth: int
parent_id: Optional[int] = None
email_domain: Optional[str] = None
effective_email_domain: Optional[str] = None
email_address: Optional[str] = None
email_quota_mb: int
description: Optional[str] = None
is_active: bool
is_top_level: bool
member_count: int = 0
children: List[Any] = []
model_config = ConfigDict(from_attributes=True)