This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 本文件定義專案開發的核心準則與不可違反的規範
|
> 本文件定義專案開發的核心準則與不可違反的規範
|
||||||
> **建立日期**: 2026-01-12
|
> **建立日期**: 2026-01-12
|
||||||
> **當前版本**: V10.46 (Fix product pick sales date casting)
|
> **當前版本**: V10.47 (Auto-detect sales columns for product picks)
|
||||||
> **最後更新**: 2026-05-01
|
> **最後更新**: 2026-05-01
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
4
app.py
4
app.py
@@ -95,8 +95,8 @@ except Exception as e:
|
|||||||
sys_log.error(f"無法檢測磁碟空間: {e}")
|
sys_log.error(f"無法檢測磁碟空間: {e}")
|
||||||
|
|
||||||
# 🚩 系統版本定義 (備份與顯示用)
|
# 🚩 系統版本定義 (備份與顯示用)
|
||||||
# 🚩 2026-05-01 V10.46: Fix product pick sales date casting
|
# 🚩 2026-05-01 V10.47: Auto-detect sales columns for product picks
|
||||||
SYSTEM_VERSION = "V10.46"
|
SYSTEM_VERSION = "V10.47"
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 🔒 SQL Injection 防護函數
|
# 🔒 SQL Injection 防護函數
|
||||||
|
|||||||
@@ -66,27 +66,68 @@ def _has_daily_sales_snapshot(conn) -> bool:
|
|||||||
return False
|
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]]:
|
def _fetch_candidates(conn, limit: int) -> List[Dict[str, Any]]:
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
sales_join = ""
|
sales_join = ""
|
||||||
sales_select = "0 AS sales_7d, 0 AS sales_prev_7d, 0 AS qty_7d"
|
sales_select = "0 AS sales_7d, 0 AS sales_prev_7d, 0 AS qty_7d"
|
||||||
|
sales_cols = {}
|
||||||
if _has_daily_sales_snapshot(conn):
|
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 = """
|
sales_join = """
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
"商品ID" AS sku,
|
{sku_col} AS sku,
|
||||||
SUM(CASE WHEN snapshot_date::date >= CURRENT_DATE - 7
|
SUM(CASE WHEN {date_col}::date >= CURRENT_DATE - 7
|
||||||
THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_7d,
|
THEN COALESCE({revenue_col}::numeric, 0) ELSE 0 END) AS sales_7d,
|
||||||
SUM(CASE WHEN snapshot_date::date >= CURRENT_DATE - 14
|
SUM(CASE WHEN {date_col}::date >= CURRENT_DATE - 14
|
||||||
AND snapshot_date::date < CURRENT_DATE - 7
|
AND {date_col}::date < CURRENT_DATE - 7
|
||||||
THEN COALESCE("銷售金額"::numeric, 0) ELSE 0 END) AS sales_prev_7d,
|
THEN COALESCE({revenue_col}::numeric, 0) ELSE 0 END) AS sales_prev_7d,
|
||||||
SUM(CASE WHEN snapshot_date::date >= CURRENT_DATE - 7
|
SUM(CASE WHEN {date_col}::date >= CURRENT_DATE - 7
|
||||||
THEN COALESCE("數量"::numeric, 0) ELSE 0 END) AS qty_7d
|
THEN COALESCE({qty_col}::numeric, 0) ELSE 0 END) AS qty_7d
|
||||||
FROM daily_sales_snapshot
|
FROM daily_sales_snapshot
|
||||||
GROUP BY "商品ID"
|
GROUP BY {sku_col}
|
||||||
) sales ON sales.sku = lm.sku
|
) 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 = """
|
sales_select = """
|
||||||
COALESCE(sales.sales_7d, 0) AS sales_7d,
|
COALESCE(sales.sales_7d, 0) AS sales_7d,
|
||||||
COALESCE(sales.sales_prev_7d, 0) AS sales_prev_7d,
|
COALESCE(sales.sales_prev_7d, 0) AS sales_prev_7d,
|
||||||
|
|||||||
@@ -148,7 +148,9 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
|
|||||||
assert "'product_pick'" in agent_source
|
assert "'product_pick'" in agent_source
|
||||||
assert "PChomeProductPickAgent" in agent_source
|
assert "PChomeProductPickAgent" in agent_source
|
||||||
assert "PChome 價格優勢" 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 "conn.rollback()" in agent_source
|
||||||
|
|
||||||
assert "@ai_bp.route('/api/ai/product-picks/generate', methods=['POST'])" in route_source
|
assert "@ai_bp.route('/api/ai/product-picks/generate', methods=['POST'])" in route_source
|
||||||
|
|||||||
Reference in New Issue
Block a user