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

69 lines
2.5 KiB
Python

"""
網路硬碟 Model
一個員工對應一個 NAS 帳號
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.base import Base
class NetworkDrive(Base):
"""網路硬碟表"""
__tablename__ = "tenant_network_drives"
__table_args__ = (
UniqueConstraint("employee_id", name="uq_network_drive_employee"),
)
id = Column(Integer, primary_key=True, index=True)
employee_id = Column(Integer, ForeignKey("tenant_employees.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
# 一個員工只有一個 NAS 帳號
drive_name = Column(String(100), unique=True, nullable=False, comment="NAS 帳號名稱 (與 username_base 相同)")
quota_gb = Column(Integer, nullable=False, comment="配額 (GB),取所有身份中的最高職級")
# 訪問路徑
webdav_url = Column(String(255), comment="WebDAV 路徑")
smb_url = Column(String(255), comment="SMB 路徑")
# 通用欄位
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="更新時間")
# 關聯
employee = relationship("Employee", back_populates="network_drive")
def __repr__(self):
return f"<NetworkDrive {self.drive_name} - {self.quota_gb}GB>"
@property
def webdav_path(self) -> str:
"""WebDAV 完整路徑"""
return self.webdav_url or f"https://nas.lab.taipei/webdav/{self.drive_name}"
@property
def smb_path(self) -> str:
"""SMB 完整路徑"""
return self.smb_url or f"\\\\10.1.0.30\\{self.drive_name}"
def update_quota_from_job_level(self, job_level: str) -> None:
"""根據職級更新配額"""
from app.core.config import settings
quota_mapping = {
"Junior": settings.NAS_QUOTA_JUNIOR,
"Mid": settings.NAS_QUOTA_MID,
"Senior": settings.NAS_QUOTA_SENIOR,
"Manager": settings.NAS_QUOTA_MANAGER,
}
new_quota = quota_mapping.get(job_level, settings.NAS_QUOTA_JUNIOR)
# 只在配額增加時更新 (不降低配額)
if new_quota > self.quota_gb:
self.quota_gb = new_quota