Files
hr-portal/backend/app/models/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

75 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
部門 Model
統一樹狀部門結構:
- depth=0, parent_id=NULL: 第一層部門 (原事業部),可設定 email_domain
- depth=1+: 子部門,繼承上層 email_domain
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey, UniqueConstraint, Index
from sqlalchemy.orm import relationship
from app.db.base import Base
class Department(Base):
"""部門表 (統一樹狀結構)"""
__tablename__ = "tenant_departments"
__table_args__ = (
UniqueConstraint("tenant_id", "seq_no", name="uq_tenant_dept_seq"),
UniqueConstraint("tenant_id", "parent_id", "code", name="uq_tenant_parent_dept_code"),
Index("idx_dept_tenant_id", "tenant_id"),
Index("idx_departments_parent", "parent_id"),
Index("idx_departments_depth", "depth"),
)
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True,
comment="租戶 ID")
seq_no = Column(Integer, nullable=False, comment="租戶內序號 (觸發器自動生成)")
parent_id = Column(Integer, ForeignKey("tenant_departments.id", ondelete="CASCADE"), nullable=True,
comment="上層部門 ID (NULL=第一層,即原事業部)")
code = Column(String(20), nullable=False, comment="部門代碼 (同層內唯一)")
name = Column(String(100), nullable=False, comment="部門名稱")
name_en = Column(String(100), nullable=True, comment="英文名稱")
email_domain = Column(String(100), nullable=True,
comment="郵件網域 (只有 depth=0 可設定,例如 ease.taipei)")
email_address = Column(String(255), nullable=True, comment="部門信箱 (例如: wind@ease.taipei)")
email_quota_mb = Column(Integer, default=5120, nullable=False, comment="部門信箱配額 (MB)")
depth = Column(Integer, default=0, nullable=False, comment="層次深度 (0=第一層1=第二層,以此類推)")
description = Column(Text, comment="說明")
# 通用欄位
is_active = Column(Boolean, default=True, nullable=False, comment="是否啟用")
edit_by = Column(String(100), nullable=True, comment="最後編輯者")
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, comment="建立時間")
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False, comment="更新時間")
# 關聯
tenant = relationship("Tenant", back_populates="departments")
parent = relationship("Department", back_populates="children", remote_side="Department.id")
children = relationship("Department", back_populates="parent", cascade="all, delete-orphan")
members = relationship(
"DepartmentMember",
back_populates="department",
cascade="all, delete-orphan",
lazy="dynamic"
)
def __repr__(self):
return f"<Department depth={self.depth} code={self.code} name={self.name}>"
@property
def effective_email_domain(self) -> str | None:
"""有效郵件網域 (第一層自身設定,子層追溯上層)"""
if self.depth == 0:
return self.email_domain
if self.parent:
return self.parent.effective_email_domain
return None
@property
def is_top_level(self) -> bool:
"""是否為第一層部門 (原事業部)"""
return self.depth == 0 and self.parent_id is None