""" Installation System Models 初始化系統資料模型 """ from datetime import datetime from sqlalchemy import ( Column, Integer, String, Boolean, Text, TIMESTAMP, ForeignKey, ARRAY ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import relationship from app.db.base import Base class InstallationSession(Base): """安裝會話""" __tablename__ = "installation_sessions" id = Column(Integer, primary_key=True, index=True) tenant_id = Column(Integer, nullable=True) # 不設外鍵,初始化時 tenants 表可能不存在 session_name = Column(String(200)) environment = Column(String(20)) # development/testing/production # 狀態追蹤 started_at = Column(TIMESTAMP, default=datetime.now) completed_at = Column(TIMESTAMP) status = Column(String(20), default='in_progress') # in_progress/completed/failed/paused # 進度統計 total_checklist_items = Column(Integer) passed_checklist_items = Column(Integer, default=0) failed_checklist_items = Column(Integer, default=0) total_steps = Column(Integer) completed_steps = Column(Integer, default=0) failed_steps = Column(Integer, default=0) executed_by = Column(String(100)) # 存取控制 is_locked = Column(Boolean, default=False) locked_at = Column(TIMESTAMP) locked_by = Column(String(100)) lock_reason = Column(String(200)) is_unlocked = Column(Boolean, default=False) unlocked_at = Column(TIMESTAMP) unlocked_by = Column(String(100)) unlock_reason = Column(String(200)) unlock_expires_at = Column(TIMESTAMP) last_viewed_at = Column(TIMESTAMP) last_viewed_by = Column(String(100)) view_count = Column(Integer, default=0) created_at = Column(TIMESTAMP, default=datetime.now) # Relationships # 不定義 tenant relationship,因為沒有 FK constraint tenant_info = relationship("InstallationTenantInfo", back_populates="session", uselist=False) department_setups = relationship("InstallationDepartmentSetup", back_populates="session") temporary_passwords = relationship("TemporaryPassword", back_populates="session") access_logs = relationship("InstallationAccessLog", back_populates="session") checklist_results = relationship("InstallationChecklistResult", back_populates="session") installation_logs = relationship("InstallationLog", back_populates="session") class InstallationChecklistItem(Base): """檢查項目定義(系統級)""" __tablename__ = "installation_checklist_items" id = Column(Integer, primary_key=True, index=True) category = Column(String(50), nullable=False) # hardware/network/software/container/security item_code = Column(String(100), unique=True, nullable=False) item_name = Column(String(200), nullable=False) check_type = Column(String(50), nullable=False) # command/api/config/manual check_command = Column(Text) # 自動檢查命令 expected_value = Column(Text) min_requirement = Column(Text) recommended_value = Column(Text) is_required = Column(Boolean, default=True) sequence_order = Column(Integer, nullable=False) description = Column(Text) created_at = Column(TIMESTAMP, default=datetime.now) # Relationships results = relationship("InstallationChecklistResult", back_populates="checklist_item") class InstallationChecklistResult(Base): """檢查結果(租戶級)""" __tablename__ = "installation_checklist_results" id = Column(Integer, primary_key=True, index=True) tenant_id = Column(Integer, nullable=True) # 不設外鍵,初始化時 tenants 表可能不存在 session_id = Column(Integer, ForeignKey("installation_sessions.id", ondelete="CASCADE")) checklist_item_id = Column(Integer, ForeignKey("installation_checklist_items.id", ondelete="CASCADE"), nullable=False) status = Column(String(20), nullable=False) # pass/fail/warning/pending/skip actual_value = Column(Text) checked_at = Column(TIMESTAMP) checked_by = Column(String(100)) auto_checked = Column(Boolean, default=False) remarks = Column(Text) created_at = Column(TIMESTAMP, default=datetime.now) updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now) # Relationships # 不定義 tenant relationship,因為沒有 FK constraint session = relationship("InstallationSession", back_populates="checklist_results") checklist_item = relationship("InstallationChecklistItem", back_populates="results") class InstallationStep(Base): """安裝步驟定義(系統級)""" __tablename__ = "installation_steps" id = Column(Integer, primary_key=True, index=True) step_code = Column(String(50), unique=True, nullable=False) step_name = Column(String(200), nullable=False) phase = Column(String(20), nullable=False) # phase1/phase2/... sequence_order = Column(Integer, nullable=False) description = Column(Text) execution_type = Column(String(50)) # auto/manual/script execution_script = Column(Text) depends_on_steps = Column(ARRAY(String)) # 依賴的步驟代碼 is_required = Column(Boolean, default=True) created_at = Column(TIMESTAMP, default=datetime.now) # Relationships logs = relationship("InstallationLog", back_populates="step") class InstallationLog(Base): """安裝執行記錄(租戶級)""" __tablename__ = "installation_logs" id = Column(Integer, primary_key=True, index=True) tenant_id = Column(Integer, nullable=True) # 不設外鍵,初始化時 tenants 表可能不存在 step_id = Column(Integer, ForeignKey("installation_steps.id", ondelete="CASCADE"), nullable=False) session_id = Column(Integer, ForeignKey("installation_sessions.id", ondelete="CASCADE")) status = Column(String(20), nullable=False) # pending/running/success/failed/skipped started_at = Column(TIMESTAMP) completed_at = Column(TIMESTAMP) executed_by = Column(String(100)) execution_method = Column(String(50)) # manual/auto/api/script result_data = Column(JSONB) error_message = Column(Text) retry_count = Column(Integer, default=0) remarks = Column(Text) created_at = Column(TIMESTAMP, default=datetime.now) updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now) # Relationships # 不定義 tenant relationship,因為沒有 FK constraint step = relationship("InstallationStep", back_populates="logs") session = relationship("InstallationSession", back_populates="installation_logs") class InstallationTenantInfo(Base): """租戶初始化資訊""" __tablename__ = "installation_tenant_info" id = Column(Integer, primary_key=True, index=True) tenant_id = Column(Integer, nullable=True, unique=True) # 不設外鍵,初始化時 tenants 表可能不存在 session_id = Column(Integer, ForeignKey("installation_sessions.id", ondelete="SET NULL")) # 公司基本資訊 company_name = Column(String(200)) company_name_en = Column(String(200)) tenant_code = Column(String(50)) # 租戶代碼 = Keycloak Realm tenant_prefix = Column(String(10)) # 員工編號前綴 tax_id = Column(String(50)) industry = Column(String(100)) company_size = Column(String(20)) # small/medium/large # 聯絡資訊 tel = Column(String(20)) # 公司電話(對應 tenants.tel) phone = Column(String(50)) fax = Column(String(50)) email = Column(String(200)) website = Column(String(200)) add = Column(Text) # 公司地址(對應 tenants.add) address = Column(Text) address_en = Column(Text) # 郵件網域設定 domain_set = Column(Integer, default=2) # 1=組織網域, 2=部門網域 domain = Column(String(100)) # 組織網域(domain_set=1 時使用) # 負責人資訊 representative_name = Column(String(100)) representative_title = Column(String(100)) representative_email = Column(String(200)) representative_phone = Column(String(50)) # 系統管理員資訊 admin_employee_id = Column(String(50)) admin_username = Column(String(100)) admin_legal_name = Column(String(100)) admin_english_name = Column(String(100)) admin_email = Column(String(200)) admin_phone = Column(String(50)) # 初始設定 default_language = Column(String(10), default='zh-TW') timezone = Column(String(50), default='Asia/Taipei') date_format = Column(String(20), default='YYYY-MM-DD') currency = Column(String(10), default='TWD') # 狀態追蹤 is_completed = Column(Boolean, default=False) completed_at = Column(TIMESTAMP) completed_by = Column(String(100)) created_at = Column(TIMESTAMP, default=datetime.now) updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now) # Relationships # 不定義 tenant relationship,因為沒有 FK constraint session = relationship("InstallationSession", back_populates="tenant_info") class InstallationDepartmentSetup(Base): """部門架構設定""" __tablename__ = "installation_department_setup" id = Column(Integer, primary_key=True, index=True) tenant_id = Column(Integer, nullable=True) # 不設外鍵,初始化時 tenants 表可能不存在 session_id = Column(Integer, ForeignKey("installation_sessions.id", ondelete="CASCADE")) department_code = Column(String(50), nullable=False) department_name = Column(String(200), nullable=False) department_name_en = Column(String(200)) email_domain = Column(String(100)) parent_code = Column(String(50)) depth = Column(Integer, default=0) manager_name = Column(String(100)) is_created = Column(Boolean, default=False) created_at = Column(TIMESTAMP, default=datetime.now) # Relationships # 不定義 tenant relationship,因為沒有 FK constraint session = relationship("InstallationSession", back_populates="department_setups") class TemporaryPassword(Base): """臨時密碼""" __tablename__ = "temporary_passwords" id = Column(Integer, primary_key=True, index=True) tenant_id = Column(Integer, nullable=True) # 不設外鍵,初始化時 tenants 表可能不存在 employee_id = Column(Integer, nullable=True) # 不設外鍵,初始化時 employees 表可能不存在 username = Column(String(100), nullable=False) session_id = Column(Integer, ForeignKey("installation_sessions.id", ondelete="SET NULL")) # 密碼資訊 password_hash = Column(String(255), nullable=False) plain_password = Column(String(100)) # 明文密碼(僅初始化階段) password_method = Column(String(20)) # auto/manual is_temporary = Column(Boolean, default=True) must_change_on_login = Column(Boolean, default=True) # 有效期限 created_at = Column(TIMESTAMP, default=datetime.now) expires_at = Column(TIMESTAMP) # 使用狀態 is_used = Column(Boolean, default=False) used_at = Column(TIMESTAMP) first_login_at = Column(TIMESTAMP) password_changed_at = Column(TIMESTAMP) # 查看控制 is_viewable = Column(Boolean, default=True) viewable_until = Column(TIMESTAMP) view_count = Column(Integer, default=0) last_viewed_at = Column(TIMESTAMP) first_viewed_at = Column(TIMESTAMP) # 明文密碼清除記錄 plain_password_cleared_at = Column(TIMESTAMP) cleared_reason = Column(String(100)) # Relationships # 不定義 tenant 和 employee relationship,因為沒有 FK constraint session = relationship("InstallationSession", back_populates="temporary_passwords") class InstallationAccessLog(Base): """存取審計日誌""" __tablename__ = "installation_access_logs" id = Column(Integer, primary_key=True, index=True) session_id = Column(Integer, ForeignKey("installation_sessions.id", ondelete="CASCADE"), nullable=False) action = Column(String(50), nullable=False) # lock/unlock/view/download_pdf action_by = Column(String(100)) action_method = Column(String(50)) # database/api/system ip_address = Column(String(50)) user_agent = Column(Text) access_granted = Column(Boolean) deny_reason = Column(String(200)) sensitive_data_accessed = Column(ARRAY(String)) created_at = Column(TIMESTAMP, default=datetime.now) # Relationships session = relationship("InstallationSession", back_populates="access_logs") class InstallationEnvironmentConfig(Base): """環境配置記錄""" __tablename__ = "installation_environment_config" id = Column(Integer, primary_key=True, index=True) session_id = Column(Integer, ForeignKey("installation_sessions.id", ondelete="SET NULL")) config_key = Column(String(100), unique=True, nullable=False, index=True) config_value = Column(Text) config_category = Column(String(50), nullable=False, index=True) # redis/database/keycloak/mailserver/nextcloud/traefik is_sensitive = Column(Boolean, default=False) # 是否為敏感資訊(密碼等) is_configured = Column(Boolean, default=False) configured_at = Column(TIMESTAMP) configured_by = Column(String(100)) description = Column(Text) created_at = Column(TIMESTAMP, default=datetime.now) updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now) # Relationships session = relationship("InstallationSession") class InstallationSystemStatus(Base): """系統狀態記錄(三階段:Initialization/Operational/Transition)""" __tablename__ = "installation_system_status" id = Column(Integer, primary_key=True, index=True) current_phase = Column(String(20), nullable=False, index=True) # initialization/operational/transition previous_phase = Column(String(20)) phase_changed_at = Column(TIMESTAMP) phase_changed_by = Column(String(100)) phase_change_reason = Column(Text) # Initialization 階段資訊 initialized_at = Column(TIMESTAMP) initialized_by = Column(String(100)) initialization_completed = Column(Boolean, default=False) # Operational 階段資訊 last_health_check_at = Column(TIMESTAMP) health_check_status = Column(String(20)) # healthy/degraded/unhealthy operational_since = Column(TIMESTAMP) # Transition 階段資訊 transition_started_at = Column(TIMESTAMP) transition_approved_by = Column(String(100)) env_db_consistent = Column(Boolean) consistency_checked_at = Column(TIMESTAMP) inconsistencies = Column(Text) # JSON 格式 # 系統鎖定 is_locked = Column(Boolean, default=False) locked_at = Column(TIMESTAMP) locked_by = Column(String(100)) lock_reason = Column(String(200)) created_at = Column(TIMESTAMP, default=datetime.now) updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now)