refactor(p1-01e): repair_database_schema 抽到 database/schema_repair.py
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s

- 80 行 schema 修復邏輯抽出,搭配 _ensure_column helper 去除 7 個 promo_products 欄位重複碼塊
- app.py 改為 from database.schema_repair import repair_database_schema 維持原呼叫
- 行為 100% 對齊(含 SQLite WAL 啟用、products.created_at 補資料)

行數變化: app.py 7,151 → 7,070 (-81)
This commit is contained in:
ooo
2026-04-28 15:51:44 +08:00
parent dea94d2e0f
commit f114c209ce
2 changed files with 77 additions and 83 deletions

85
app.py
View File

@@ -159,89 +159,8 @@ from utils.security import ( # noqa: E402
validate_upload_file,
)
# 🚩 資料庫結構自動修復 (V9.53 新增)
def repair_database_schema():
db = DatabaseManager()
engine = db.engine
from sqlalchemy import inspect, text
from config import DATABASE_TYPE
try:
# 🚩 V9.96: 啟用 SQLite WAL 模式以解決 database is locked 問題 (僅 SQLite)
if DATABASE_TYPE == 'sqlite':
with engine.connect() as conn:
# 啟用 WAL 模式 (Write-Ahead Logging)
conn.execute(text("PRAGMA journal_mode=WAL"))
conn.commit()
sys_log.info("[Database] [WAL] ✅ SQLite WAL 模式已啟用 | 提升並發寫入效能")
else:
sys_log.info(f"[Database] ✅ 使用 {DATABASE_TYPE.upper()} 資料庫")
inspector = inspect(engine)
# V9.70: 檢查 products 表
if 'products' in inspector.get_table_names():
product_columns = [c['name'] for c in inspector.get_columns('products')]
if 'image_url' not in product_columns:
sys_log.warning("[Database] [Schema] ⚠️ 偵測到 products 表缺少 image_url 欄位 | 正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE products ADD COLUMN image_url TEXT"))
conn.commit()
sys_log.info("[Database] [Schema] ✅ products.image_url 欄位修復完成")
if 'created_at' not in product_columns:
sys_log.warning("[Database] [Schema] ⚠️ 偵測到 products 表缺少 created_at 欄位 | 正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE products ADD COLUMN created_at DATETIME"))
conn.execute(text("UPDATE products SET created_at = updated_at WHERE created_at IS NULL"))
conn.commit()
sys_log.info("[Database] [Schema] ✅ products.created_at 欄位修復完成")
if 'promo_products' in inspector.get_table_names():
columns = [c['name'] for c in inspector.get_columns('promo_products')]
if 'url' not in columns:
sys_log.warning("⚠️ 偵測到 promo_products 表缺少 url 欄位,正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE promo_products ADD COLUMN url TEXT"))
conn.commit()
sys_log.info("✅ url 欄位修復完成")
if 'image_url' not in columns:
sys_log.warning("⚠️ 偵測到 promo_products 表缺少 image_url 欄位,正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE promo_products ADD COLUMN image_url TEXT"))
conn.commit()
sys_log.info("✅ image_url 欄位修復完成")
if 'previous_price' not in columns:
sys_log.warning("⚠️ 偵測到 promo_products 表缺少 previous_price 欄位,正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE promo_products ADD COLUMN previous_price INTEGER"))
conn.commit()
sys_log.info("✅ previous_price 欄位修復完成")
if 'session_time_text' not in columns:
sys_log.warning("⚠️ 偵測到 promo_products 表缺少 session_time_text 欄位,正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE promo_products ADD COLUMN session_time_text TEXT"))
conn.commit()
sys_log.info("✅ session_time_text 欄位修復完成")
if 'remain_qty' not in columns:
sys_log.warning("⚠️ 偵測到 promo_products 表缺少 remain_qty 欄位,正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE promo_products ADD COLUMN remain_qty INTEGER"))
conn.commit()
sys_log.info("✅ remain_qty 欄位修復完成")
if 'discount_text' not in columns:
sys_log.warning("⚠️ 偵測到 promo_products 表缺少 discount_text 欄位,正在自動修復...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE promo_products ADD COLUMN discount_text TEXT"))
conn.commit()
sys_log.info("✅ discount_text 欄位修復完成")
if 'page_type' not in columns:
sys_log.warning("⚠️ 偵測到 promo_products 表缺少 page_type 欄位,正在自動修復...")
with engine.connect() as conn:
# 將既有資料預設為 'edm'
conn.execute(text("ALTER TABLE promo_products ADD COLUMN page_type TEXT DEFAULT 'edm'"))
conn.commit()
sys_log.info("✅ page_type 欄位修復完成")
except Exception as e:
sys_log.error(f"[Database] [Schema] ❌ 資料庫修復失敗 | Error: {e}")
# 🚩 資料庫結構自動修復 (V9.53 新增) — 實作搬至 database/schema_repair.py
from database.schema_repair import repair_database_schema # noqa: E402, F401
# 從環境變數讀取 NGROK_AUTH_TOKEN如果未設定則使用原值但會發出警告
NGROK_AUTH_TOKEN = os.getenv('NGROK_AUTH_TOKEN', '36e27NM5V7sUJ8QxJIAAWCp7sUv_3brtcrBarYvcP3SbvFKhF')

75
database/schema_repair.py Normal file
View File

@@ -0,0 +1,75 @@
"""啟動時資料庫 schema 自動修復。
從 app.py 抽出的 repair_database_schema()。
歷史背景:早期 V9.53 起為了讓部署不必執行 migration在啟動時動態檢查欄位並
ALTER TABLE 補上。長期應改走 Alembic / Migration script但這個 fallback 還活著。
呼叫端app.py 啟動末段呼叫一次。
"""
from utils.logger_manager import SystemLogger
_log = SystemLogger("SchemaRepair").get_logger()
def _ensure_column(engine, text_fn, table, column, ddl, post_sql=None):
"""補上單一欄位 helper若不存在則 ALTER TABLE。"""
from sqlalchemy import inspect
inspector = inspect(engine)
if table not in inspector.get_table_names():
return False
columns = [c['name'] for c in inspector.get_columns(table)]
if column in columns:
return False
_log.warning(f"[Database] [Schema] ⚠️ 偵測到 {table} 表缺少 {column} 欄位 | 正在自動修復...")
with engine.connect() as conn:
conn.execute(text_fn(ddl))
if post_sql:
conn.execute(text_fn(post_sql))
conn.commit()
_log.info(f"[Database] [Schema] ✅ {table}.{column} 欄位修復完成")
return True
def repair_database_schema():
"""啟動時資料庫結構自動修復V9.53)。"""
from database.manager import DatabaseManager
from sqlalchemy import inspect, text
from config import DATABASE_TYPE
db = DatabaseManager()
engine = db.engine
try:
# V9.96: SQLite WAL 模式(解決 database is locked
if DATABASE_TYPE == 'sqlite':
with engine.connect() as conn:
conn.execute(text("PRAGMA journal_mode=WAL"))
conn.commit()
_log.info("[Database] [WAL] ✅ SQLite WAL 模式已啟用 | 提升並發寫入效能")
else:
_log.info(f"[Database] ✅ 使用 {DATABASE_TYPE.upper()} 資料庫")
inspector = inspect(engine)
# V9.70: products 表
_ensure_column(engine, text, 'products', 'image_url',
"ALTER TABLE products ADD COLUMN image_url TEXT")
_ensure_column(engine, text, 'products', 'created_at',
"ALTER TABLE products ADD COLUMN created_at DATETIME",
post_sql="UPDATE products SET created_at = updated_at WHERE created_at IS NULL")
# promo_products 表
promo_columns = [
('url', "ALTER TABLE promo_products ADD COLUMN url TEXT"),
('image_url', "ALTER TABLE promo_products ADD COLUMN image_url TEXT"),
('previous_price', "ALTER TABLE promo_products ADD COLUMN previous_price INTEGER"),
('session_time_text', "ALTER TABLE promo_products ADD COLUMN session_time_text TEXT"),
('remain_qty', "ALTER TABLE promo_products ADD COLUMN remain_qty INTEGER"),
('discount_text', "ALTER TABLE promo_products ADD COLUMN discount_text TEXT"),
('page_type', "ALTER TABLE promo_products ADD COLUMN page_type TEXT DEFAULT 'edm'"),
]
for col_name, ddl in promo_columns:
_ensure_column(engine, text, 'promo_products', col_name, ddl)
except Exception as e:
_log.error(f"[Database] [Schema] ❌ 資料庫修復失敗 | Error: {e}")