fix(ai): 自動偵測挑品銷售欄位
All checks were successful
CD Pipeline / deploy (push) Successful in 1m50s

This commit is contained in:
OoO
2026-05-01 10:18:07 +08:00
parent 70de91f1f6
commit 9f9e0727e7
4 changed files with 57 additions and 14 deletions

View File

@@ -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
---

4
app.py
View File

@@ -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 防護函數

View File

@@ -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,

View File

@@ -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