fix(db): 補全 metadata model import 與 realtime sales ORM

ADR-017 Phase 3f-0
This commit is contained in:
OoO
2026-04-29 21:00:46 +08:00
parent 8be332728e
commit f4149d4c05
12 changed files with 314 additions and 27 deletions

View File

@@ -1,14 +1,3 @@
# database/ai_models.py
# ⚠️ 這四個 class 的原始定義已移至 autoheal_models.pyADR-013 統一管理)。
# 此檔僅作向後相容 re-export shim不再重複定義 SQLAlchemy Table
# 以避免 "Table already defined for this MetaData instance" 衝突。
from .autoheal_models import ( # noqa: F401
AgentContext,
ActionPlan,
ActionOutcome,
AgentStrategyWeights,
)
# AI history and template models
from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, Float
from database.models import Base
@@ -163,6 +152,5 @@ class AIInsight(Base):
__all__ = [
"AgentContext", "ActionPlan", "ActionOutcome", "AgentStrategyWeights",
"AIGenerationHistory", "AIPromptTemplate", "AIUsageTracking", "AIInsight",
]

View File

@@ -1,5 +1,6 @@
import os
import re
import threading
from sqlalchemy import create_engine, desc, select, text, literal
from sqlalchemy.orm import sessionmaker
from datetime import datetime
@@ -7,12 +8,22 @@ from .models import Base, Category, Product, PriceRecord, MonthlySummaryAnalysis
from .user_models import User, LoginHistory # noqa: F401 - 必須在 trend_models 之前導入,解決 ForeignKey 依賴問題
from .edm_models import PromoProduct # V-Fix: 確保 EDM 模型被註冊,以便自動建表
from .trend_models import TrendRecord, TrendKeyword, TrendAnalysis, WebSearchCache, TelegramUser # noqa: F401 - 趨勢資料表
from .ai_models import AgentContext, ActionPlan, ActionOutcome, AgentStrategyWeights # noqa: F401 - AI agent 模型
from .autoheal_models import Incident, Playbook, HealLog # noqa: F401 - ADR-013 AIOps 自動修復
from .import_models import ImportJob # noqa: F401 - 確保 import_jobs 表被 Base.metadata 管理
from .permission_models import Permission, UserPermission # noqa: F401 - 確保權限表被 Base.metadata 管理
from .ai_models import AIGenerationHistory, AIPromptTemplate, AIUsageTracking, AIInsight # noqa: F401 - AI history/template
from .autoheal_models import ( # noqa: F401 - ADR-013 AIOps 自動修復表
AgentContext,
ActionPlan,
ActionOutcome,
AgentStrategyWeights,
Incident,
Playbook,
HealLog,
)
from .import_models import ImportJob, ImportConfig # noqa: F401 - 確保 import_jobs/import_config 被 Base.metadata 管理
from .notification_models import NotificationTemplate # noqa: F401 - 確保 notification_templates 表被 Base.metadata 管理
from .ppt_reports import PPTReport # noqa: F401 - 確保 ppt_reports 表被 Base.metadata 管理
from .vendor_models import VendorStockout # noqa: F401 - 確保 vendor_stockout 表被 Base.metadata 管理
from .vendor_models import VendorStockout, VendorList, VendorEmail, EmailSendLog # noqa: F401 - 確保 vendor 表被 Base.metadata 管理
from .realtime_sales_models import RealtimeSalesMonthly # noqa: F401 - 確保 realtime_sales_monthly 被 Base.metadata 管理
# 🚩 導入優化後的日誌管理模組
from utils.logger_manager import SystemLogger
@@ -20,6 +31,33 @@ from utils.logger_manager import SystemLogger
# 初始化資料庫模組專用 Logger
sys_log = SystemLogger("Database").get_logger()
_metadata_init_lock = threading.Lock()
_metadata_initialized = False
_POSTGRES_METADATA_LOCK_ID = 170017
def ensure_metadata_initialized(engine, use_postgres_lock=False):
"""冪等初始化 SQLAlchemy metadata避免一般流程重複碰 DDL。"""
global _metadata_initialized
if _metadata_initialized:
return
with _metadata_init_lock:
if _metadata_initialized:
return
if use_postgres_lock:
with engine.begin() as conn:
conn.execute(text("SELECT pg_advisory_lock(:lock_id)"), {"lock_id": _POSTGRES_METADATA_LOCK_ID})
try:
Base.metadata.create_all(conn)
finally:
conn.execute(text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": _POSTGRES_METADATA_LOCK_ID})
else:
Base.metadata.create_all(engine)
_metadata_initialized = True
def sanitize_timestamp(timestamp_str):
"""
驗證並清理時間戳字串,防止 SQL Injection
@@ -63,6 +101,7 @@ class DatabaseManager:
'options': '-c statement_timeout=60000' # SQL 超時 60 秒
}
)
ensure_metadata_initialized(self.engine, use_postgres_lock=True)
self.Session = sessionmaker(bind=self.engine)
sys_log.info(f"[Database] ✅ 使用 PostgreSQL 資料庫 (連線池已優化)")
# ADR-013: 確保 AIOps 自動修復表存在並植入種子 PlayBook
@@ -422,4 +461,4 @@ def get_session():
finally:
session.close()
"""
return get_db_manager().get_session()
return get_db_manager().get_session()

View File

@@ -0,0 +1,41 @@
from sqlalchemy import Column, Date, DateTime, Integer, Numeric, String, Text
from database.models import Base
class RealtimeSalesMonthly(Base):
"""
即時業績月報 ORM。
這張表先由 PostgreSQL init.sql 建出,之後又被程式碼與匯入流程持續擴充。
這裡先把目前程式碼直接依賴的核心欄位納入 metadata讓 create_all、
metrics 與啟動自檢有一致的表定義。
"""
__tablename__ = "realtime_sales_monthly"
__table_args__ = {"extend_existing": True}
id = Column(Integer, primary_key=True)
日期 = Column(Date, index=True)
訂單編號 = Column(String(50), index=True)
商品ID = Column(String(100), index=True)
商品編號 = Column(String(100), index=True)
商品名稱 = Column(Text)
數量 = Column(Integer)
總業績 = Column(Numeric(15, 2))
總成本 = Column(Numeric(15, 2))
毛利 = Column(Numeric(15, 2))
退貨數量 = Column(Integer)
商品單位售價 = Column(Numeric(15, 2))
廠商名稱 = Column(String(255), index=True)
分類名稱 = Column(String(255), index=True)
商品館 = Column(String(255), index=True)
品牌名稱 = Column(String(255), index=True)
時間 = Column(String(50))
付款方式 = Column(String(100))
折扣活動名稱 = Column(String(255))
折價券折扣金額 = Column(Numeric(15, 2))
折扣金額 = Column(Numeric(15, 2))
滿額再折扣金額 = Column(Numeric(15, 2))
分期手續費 = Column(Numeric(15, 2))
created_at = Column(DateTime)