diff --git a/app.py b/app.py index a76154b..246943f 100644 --- a/app.py +++ b/app.py @@ -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') diff --git a/database/schema_repair.py b/database/schema_repair.py new file mode 100644 index 0000000..db2cd4b --- /dev/null +++ b/database/schema_repair.py @@ -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}")