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

78 lines
2.5 KiB
Python

"""
訂閱記錄 Model
管理租戶的訂閱狀態和歷史
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, String, Date, DateTime, Boolean, ForeignKey, Numeric
from sqlalchemy.orm import relationship
import enum
from app.db.base import Base
class SubscriptionStatus(str, enum.Enum):
"""訂閱狀態"""
ACTIVE = "active" # 進行中
CANCELLED = "cancelled" # 已取消
EXPIRED = "expired" # 已過期
class Subscription(Base):
"""訂閱記錄表"""
__tablename__ = "subscriptions"
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("organizes.id", ondelete="CASCADE"), nullable=False, index=True)
# 方案資訊
plan_id = Column(String(50), nullable=False, comment="方案 ID (starter/standard/enterprise)")
start_date = Column(Date, nullable=False, comment="開始日期")
end_date = Column(Date, nullable=False, comment="結束日期")
status = Column(String(20), default=SubscriptionStatus.ACTIVE, nullable=False, comment="狀態")
# 自動續約
auto_renew = Column(Boolean, default=True, nullable=False, comment="是否自動續約")
# 時間記錄
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
cancelled_at = Column(DateTime, nullable=True, comment="取消時間")
# 關聯
tenant = relationship("Tenant", back_populates="subscriptions")
def __repr__(self):
return f"<Subscription {self.plan_id} for Tenant#{self.tenant_id} ({self.start_date} ~ {self.end_date})>"
@property
def is_active(self) -> bool:
"""是否為活躍訂閱"""
today = date.today()
return (
self.status == SubscriptionStatus.ACTIVE and
self.start_date <= today <= self.end_date
)
@property
def days_remaining(self) -> int:
"""剩餘天數"""
if not self.is_active:
return 0
return (self.end_date - date.today()).days
def renew(self, months: int = 1) -> "Subscription":
"""續約 (創建新的訂閱記錄)"""
from dateutil.relativedelta import relativedelta
new_start = self.end_date + relativedelta(days=1)
new_end = new_start + relativedelta(months=months) - relativedelta(days=1)
return Subscription(
tenant_id=self.tenant_id,
plan_id=self.plan_id,
start_date=new_start,
end_date=new_end,
status=SubscriptionStatus.ACTIVE,
auto_renew=self.auto_renew
)