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>
This commit is contained in:
2026-02-23 20:12:43 +08:00
commit 360533393f
386 changed files with 70353 additions and 0 deletions

59
backend/app/batch/base.py Normal file
View File

@@ -0,0 +1,59 @@
"""
批次作業基礎工具
提供 log_batch_execution 等共用函式
"""
import logging
from datetime import datetime
from typing import Optional
logger = logging.getLogger(__name__)
def log_batch_execution(
batch_name: str,
status: str,
message: Optional[str] = None,
started_at: Optional[datetime] = None,
finished_at: Optional[datetime] = None,
) -> None:
"""
記錄批次執行日誌到資料庫
Args:
batch_name: 批次名稱
status: 執行狀態 (success/failed/warning)
message: 執行訊息
started_at: 開始時間 (若未提供則使用 finished_at)
finished_at: 完成時間 (若未提供則使用現在)
"""
from app.db.session import get_db
from app.models.batch_log import BatchLog
now = datetime.utcnow()
finished = finished_at or now
started = started_at or finished
duration = None
if started and finished:
duration = int((finished - started).total_seconds())
try:
db = next(get_db())
log_entry = BatchLog(
batch_name=batch_name,
status=status,
message=message,
started_at=started,
finished_at=finished,
duration_seconds=duration,
)
db.add(log_entry)
db.commit()
logger.info(f"[{batch_name}] 批次執行記錄已寫入: {status}")
except Exception as e:
logger.error(f"[{batch_name}] 寫入批次日誌失敗: {e}")
finally:
try:
db.close()
except Exception:
pass