Files
hr-portal/backend/app/models/employee_identity.py.deprecated
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

67 lines
2.9 KiB
Plaintext

"""
員工身份 Model
一個員工可以在多個事業部任職,每個事業部對應一個身份
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, String, Boolean, Date, DateTime, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.base import Base
class EmployeeIdentity(Base):
"""員工身份表"""
__tablename__ = "employee_identities"
__table_args__ = (
UniqueConstraint("tenant_id", "employee_id", "business_unit_id", name="uq_tenant_emp_bu"),
)
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("organizes.id", ondelete="CASCADE"), nullable=False, index=True, comment="租戶 ID")
employee_id = Column(Integer, ForeignKey("employees.id", ondelete="CASCADE"), nullable=False, index=True)
# SSO 帳號 (= 郵件地址)
username = Column(String(100), unique=True, nullable=False, index=True, comment="SSO 帳號 (porsche.chen@lab.taipei)")
keycloak_id = Column(String(100), unique=True, nullable=False, index=True, comment="Keycloak UUID")
# 組織與職務
business_unit_id = Column(Integer, ForeignKey("business_units.id"), nullable=False, index=True)
department_id = Column(Integer, ForeignKey("departments.id"), nullable=True, index=True)
job_title = Column(String(100), nullable=False, comment="職稱")
job_level = Column(String(20), nullable=False, comment="職級 (Junior/Mid/Senior/Manager)")
is_primary = Column(Boolean, default=False, nullable=False, comment="是否為主要身份")
# 郵件配額
email_quota_mb = Column(Integer, nullable=False, comment="郵件配額 (MB)")
# 時間記錄
started_at = Column(Date, nullable=False, comment="開始日期")
ended_at = Column(Date, nullable=True, comment="結束日期")
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# 關聯
tenant = relationship("Tenant")
employee = relationship("Employee", back_populates="identities")
business_unit = relationship("BusinessUnit", back_populates="employee_identities")
department = relationship("Department") # back_populates 已移除 (employee_identities 廢棄)
def __repr__(self):
return f"<EmployeeIdentity {self.username}>"
@property
def email(self) -> str:
"""郵件地址 (= SSO 帳號)"""
return self.username
@property
def is_cross_department(self) -> bool:
"""是否跨部門任職 (檢查同一員工是否有其他身份)"""
return len(self.employee.identities) > 1
def generate_username(self, username_base: str, email_domain: str) -> str:
"""生成 SSO 帳號"""
return f"{username_base}@{email_domain}"