diff --git a/CONSTITUTION.md b/CONSTITUTION.md index 9b96d73..d229519 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -2,7 +2,7 @@ > 本文件定義專案開發的核心準則與不可違反的規範 > **建立日期**: 2026-01-12 -> **當前版本**: V10.46 (Fix product pick sales date casting) +> **當前版本**: V10.47 (Auto-detect sales columns for product picks) > **最後更新**: 2026-05-01 --- diff --git a/app.py b/app.py index c1ece93..65cfa50 100644 --- a/app.py +++ b/app.py @@ -95,8 +95,8 @@ except Exception as e: sys_log.error(f"無法檢測磁碟空間: {e}") # 🚩 系統版本定義 (備份與顯示用) -# 🚩 2026-05-01 V10.46: Fix product pick sales date casting -SYSTEM_VERSION = "V10.46" +# 🚩 2026-05-01 V10.47: Auto-detect sales columns for product picks +SYSTEM_VERSION = "V10.47" # ========================================== # 🔒 SQL Injection 防護函數 diff --git a/services/ai_product_pick_agent.py b/services/ai_product_pick_agent.py index 8391a4f..ac3af48 100644 --- a/services/ai_product_pick_agent.py +++ b/services/ai_product_pick_agent.py @@ -66,27 +66,68 @@ def _has_daily_sales_snapshot(conn) -> bool: return False +def _daily_sales_columns(conn) -> Dict[str, str]: + """依正式匯入表實際欄位挑選可用欄名。""" + from sqlalchemy import text + + rows = conn.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'daily_sales_snapshot' + """)).fetchall() + columns = {row[0] for row in rows} + + def first_available(candidates): + return next((col for col in candidates if col in columns), None) + + return { + "sku": first_available(["商品ID", "Product ID", "ID", "i_code", "Item Code"]), + "date": first_available(["snapshot_date", "日期", "訂單日期", "交易日期", "Date"]), + "revenue": first_available(["總業績", "銷售金額", "業績", "金額", "Amount", "Sales", "Total"]), + "qty": first_available(["數量", "銷售數量", "銷量", "Qty", "Quantity"]), + } + + +def _quote_identifier(identifier: str) -> str: + return '"' + identifier.replace('"', '""') + '"' + + def _fetch_candidates(conn, limit: int) -> List[Dict[str, Any]]: from sqlalchemy import text sales_join = "" sales_select = "0 AS sales_7d, 0 AS sales_prev_7d, 0 AS qty_7d" + sales_cols = {} if _has_daily_sales_snapshot(conn): + sales_cols = _daily_sales_columns(conn) + if not all([sales_cols.get("sku"), sales_cols.get("date"), sales_cols.get("revenue"), sales_cols.get("qty")]): + sales_cols = {} + + if sales_cols: + sku_col = _quote_identifier(sales_cols["sku"]) + date_col = _quote_identifier(sales_cols["date"]) + revenue_col = _quote_identifier(sales_cols["revenue"]) + qty_col = _quote_identifier(sales_cols["qty"]) sales_join = """ LEFT JOIN ( SELECT - "商品ID" AS sku, - SUM(CASE WHEN snapshot_date::date >= CURRENT_DATE - 7 - THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_7d, - SUM(CASE WHEN snapshot_date::date >= CURRENT_DATE - 14 - AND snapshot_date::date < CURRENT_DATE - 7 - THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_prev_7d, - SUM(CASE WHEN snapshot_date::date >= CURRENT_DATE - 7 - THEN COALESCE("數量"::numeric, 0) ELSE 0 END) AS qty_7d + {sku_col} AS sku, + SUM(CASE WHEN {date_col}::date >= CURRENT_DATE - 7 + THEN COALESCE({revenue_col}::numeric, 0) ELSE 0 END) AS sales_7d, + SUM(CASE WHEN {date_col}::date >= CURRENT_DATE - 14 + AND {date_col}::date < CURRENT_DATE - 7 + THEN COALESCE({revenue_col}::numeric, 0) ELSE 0 END) AS sales_prev_7d, + SUM(CASE WHEN {date_col}::date >= CURRENT_DATE - 7 + THEN COALESCE({qty_col}::numeric, 0) ELSE 0 END) AS qty_7d FROM daily_sales_snapshot - GROUP BY "商品ID" + GROUP BY {sku_col} ) sales ON sales.sku = lm.sku - """ + """.format( + sku_col=sku_col, + date_col=date_col, + revenue_col=revenue_col, + qty_col=qty_col, + ) sales_select = """ COALESCE(sales.sales_7d, 0) AS sales_7d, COALESCE(sales.sales_prev_7d, 0) AS sales_prev_7d, diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 69dca02..b5d144f 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -148,7 +148,9 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action(): assert "'product_pick'" in agent_source assert "PChomeProductPickAgent" in agent_source assert "PChome 價格優勢" in agent_source - assert "snapshot_date::date" in agent_source + assert "_daily_sales_columns" in agent_source + assert '"總業績"' in agent_source + assert "{date_col}::date" in agent_source assert "conn.rollback()" in agent_source assert "@ai_bp.route('/api/ai/product-picks/generate', methods=['POST'])" in route_source