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

50 lines
2.0 KiB
Plaintext

"""
事業部 Model
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.base import Base
class BusinessUnit(Base):
"""事業部表"""
__tablename__ = "business_units"
__table_args__ = (
UniqueConstraint("tenant_id", "code", name="uq_tenant_bu_code"),
)
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("organizes.id", ondelete="CASCADE"), nullable=False, index=True, comment="租戶 ID")
name = Column(String(100), nullable=False, comment="事業部名稱")
name_en = Column(String(100), comment="英文名稱")
code = Column(String(20), nullable=False, index=True, comment="事業部代碼 (BD, TD, OM, 租戶內唯一)")
email_domain = Column(String(100), unique=True, nullable=False, comment="郵件網域 (ease.taipei, lab.taipei, porscheworld.tw)")
description = Column(Text, comment="說明")
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Phase 2.2 新增欄位
primary_domain = Column(String(100), comment="主要網域 (與 email_domain 相同)")
email_address = Column(String(255), comment="事業部信箱 (例如: business@ease.taipei)")
email_quota_mb = Column(Integer, default=10240, nullable=False, comment="事業部信箱配額 (MB)")
# 關聯
tenant = relationship("Tenant", back_populates="business_units")
# departments relationship 已移除 (business_unit_id FK 已從 departments 表刪除於 migration 0005)
employee_identities = relationship(
"EmployeeIdentity",
back_populates="business_unit",
lazy="dynamic"
)
def __repr__(self):
return f"<BusinessUnit {self.code} - {self.name}>"
@property
def sso_domain(self) -> str:
"""SSO 帳號網域"""
return self.email_domain