fix(db): 補全 metadata model import 與 realtime sales ORM
ADR-017 Phase 3f-0
This commit is contained in:
@@ -1,14 +1,3 @@
|
||||
# database/ai_models.py
|
||||
# ⚠️ 這四個 class 的原始定義已移至 autoheal_models.py(ADR-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",
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
41
database/realtime_sales_models.py
Normal file
41
database/realtime_sales_models.py
Normal 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)
|
||||
Reference in New Issue
Block a user