This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.315 修正競品簡報/報表指定日期取價:`fetch_competitor_comparison_results()` 在有 start/end date 時改讀 `competitor_price_history` 的期間快照,MOMO 價格也取期間結束前最新價;沒有指定日期才使用目前有效 `competitor_prices`,避免把今天的 PChome 快取價塞回歷史 daily/growth/PPT 判讀。
|
||||
- V10.314 擴大 PChome 候選池與搜尋韌性:PChome 搜尋 API 改為依 limit 掃多頁並對 429/5xx/timeout 做有限重試;feeder 預設每個商品最多 5 組搜尋詞、每詞 20 候選、2 頁,且搜尋清理不再刪掉括號/方括號內的品牌與規格,讓正確候選更有機會進 matcher,而不是長期停在「待對比」。
|
||||
- V10.312 強化 PChome 商品身份比對防錯配:matcher 開始解析 mg/mcg 劑量、件組套組與多規格集合,60ml+150ml vs 60ml+20ml、10mg vs 20mg、10片 vs 10盒、精華 vs 化妝水都會進硬否決或單位價覆核,不再靠單一規格重疊放行;覆核診斷同步新增「劑量差異」標籤,降低核心比價錯配污染 daily/growth/PPT/AI 分析。
|
||||
- V10.311 統一競品價差語意:`fetch_competitor_comparison_results()`、competitor PPT 與 OpenClaw competitor prompt 全部改用 `MOMO - PChome`,正值代表 MOMO 較貴 / PChome 低價壓力,負值代表 MOMO 價格優勢;避免 daily/growth 顯示價格壓力但 PPT/AI 反向解讀。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.314"
|
||||
SYSTEM_VERSION = "V10.315"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -607,3 +607,4 @@ POSTGRES_HOST=momo-db
|
||||
| 2026-04-17 | NIM Tool Calling E2E | 真實 NVIDIA_API_KEY 驗證:dispatched=3, errors=[] |
|
||||
| 2026-05-20 | PChome 商品身份比對仍可能因單一規格重疊誤放行 | V10.312 起 matcher 解析 mg/mcg 劑量、件組套組、多規格集合與同數字不同單位;劑量/容量/重量/件數/品類衝突會硬否決或導向單位價覆核,避免錯配污染 Dashboard、daily/growth、PPT 與 AI 競價分析 |
|
||||
| 2026-05-20 | 正確 PChome 候選常因只掃第一頁或搜尋詞丟失品牌/規格而未進入 matcher | V10.314 起搜尋 API 依 limit 掃多頁、對暫時性錯誤有限重試;feeder 預設 5 組搜尋詞、20 候選、2 頁,並保留括號/方括號內品牌與規格,提升覆核隊列與正式比價的候選品質 |
|
||||
| 2026-05-20 | 指定日期競品簡報可能混用目前 `competitor_prices` 快取價 | V10.315 起 `fetch_competitor_comparison_results()` 有 start/end date 時改用 `competitor_price_history` 期間快照,MOMO 價格取報表結束日前最新價;即時報表才使用目前有效 `competitor_prices` |
|
||||
|
||||
@@ -3343,7 +3343,7 @@ def _generate_ppt_cmd(sub_type: str, sub_arg: str, _chat_id: int, target: str,
|
||||
if found_c else 0)
|
||||
|
||||
data_summary = (
|
||||
f"【可信資料源=competitor_prices 高信心配對,MOMO vs PChome】\n"
|
||||
f"【可信資料源=指定期間 competitor_price_history;即時報表才用 competitor_prices,MOMO vs PChome】\n"
|
||||
f"分析週期:{period_label}\n"
|
||||
f"掃描商品:{len(results)} 件 | 高信心比對:{len(found_c)} 件 | 需單位價比較:{len(unit_comparable_c)} 件 | 待補身份/價格:{len(not_found_c)} 件\n"
|
||||
f"PChome 低價壓力(PChome 比 MOMO 便宜):{len(pchome_low_price_c)} 件 | MOMO 價格優勢(MOMO 比 PChome 便宜):{len(momo_low_price_c)} 件\n"
|
||||
|
||||
@@ -934,15 +934,24 @@ def fetch_competitor_comparison_results(
|
||||
if not (
|
||||
inspector.has_table("products")
|
||||
and inspector.has_table("price_records")
|
||||
and inspector.has_table("competitor_prices")
|
||||
):
|
||||
return []
|
||||
|
||||
start_date_param = str(start_date).replace("/", "-")[:10] if start_date else ""
|
||||
end_date_param = str(end_date).replace("/", "-")[:10] if end_date else ""
|
||||
requested_historical_prices = bool(start_date_param or end_date_param)
|
||||
use_history_prices = bool(requested_historical_prices and inspector.has_table("competitor_price_history"))
|
||||
if requested_historical_prices and not use_history_prices:
|
||||
return []
|
||||
if not (use_history_prices or inspector.has_table("competitor_prices")):
|
||||
return []
|
||||
|
||||
has_daily_sales = inspector.has_table("daily_sales")
|
||||
has_match_attempts = inspector.has_table("competitor_match_attempts")
|
||||
sales_cte = ""
|
||||
sales_join = ""
|
||||
sales_select = "0 AS momo_revenue,"
|
||||
momo_price_cutoff = ""
|
||||
attempt_cte = """
|
||||
latest_attempt AS (
|
||||
SELECT
|
||||
@@ -964,15 +973,17 @@ def fetch_competitor_comparison_results(
|
||||
"ABS((lm.momo_price - vc.pchome_price) / vc.pchome_price * 100) DESC NULLS LAST"
|
||||
)
|
||||
params: dict[str, Any] = {"limit": limit}
|
||||
if end_date_param:
|
||||
params["end_date"] = end_date_param
|
||||
momo_price_cutoff = "AND pr.timestamp < DATE(:end_date) + INTERVAL '1 day'"
|
||||
|
||||
if has_daily_sales:
|
||||
where = []
|
||||
if start_date:
|
||||
if start_date_param:
|
||||
where.append("DATE(s.date) >= DATE(:start_date)")
|
||||
params["start_date"] = str(start_date).replace("/", "-")[:10]
|
||||
if end_date:
|
||||
params["start_date"] = start_date_param
|
||||
if end_date_param:
|
||||
where.append("DATE(s.date) <= DATE(:end_date)")
|
||||
params["end_date"] = str(end_date).replace("/", "-")[:10]
|
||||
sales_where = "WHERE " + " AND ".join(where) if where else ""
|
||||
sales_cte = f""",
|
||||
sales_rank AS (
|
||||
@@ -1011,6 +1022,57 @@ def fetch_competitor_comparison_results(
|
||||
)
|
||||
"""
|
||||
|
||||
if use_history_prices:
|
||||
history_filters = [
|
||||
"cph.source = 'pchome'",
|
||||
"cph.price IS NOT NULL",
|
||||
"cph.price > 0",
|
||||
f"COALESCE(cph.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}",
|
||||
"COALESCE(cph.tags, '[]'::jsonb) ? 'identity_v2'",
|
||||
]
|
||||
if start_date_param:
|
||||
params["start_date"] = start_date_param
|
||||
history_filters.append("cph.crawled_at >= DATE(:start_date)")
|
||||
if end_date_param:
|
||||
history_filters.append("cph.crawled_at < DATE(:end_date) + INTERVAL '1 day'")
|
||||
history_where = "\n AND ".join(history_filters)
|
||||
valid_competitor_cte = f"""
|
||||
valid_competitor AS (
|
||||
SELECT DISTINCT ON (cph.sku)
|
||||
cph.sku,
|
||||
cph.price AS pchome_price,
|
||||
cph.competitor_product_id,
|
||||
cph.competitor_product_name,
|
||||
cph.match_score,
|
||||
cph.crawled_at,
|
||||
'competitor_price_history' AS competitor_source
|
||||
FROM competitor_price_history cph
|
||||
WHERE {history_where}
|
||||
ORDER BY cph.sku, cph.crawled_at DESC NULLS LAST
|
||||
)
|
||||
"""
|
||||
else:
|
||||
valid_competitor_cte = f"""
|
||||
valid_competitor AS (
|
||||
SELECT DISTINCT ON (cp.sku)
|
||||
cp.sku,
|
||||
cp.price AS pchome_price,
|
||||
cp.competitor_product_id,
|
||||
cp.competitor_product_name,
|
||||
cp.match_score,
|
||||
cp.crawled_at,
|
||||
'competitor_prices' AS competitor_source
|
||||
FROM competitor_prices cp
|
||||
WHERE cp.source = 'pchome'
|
||||
AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP)
|
||||
AND cp.price IS NOT NULL
|
||||
AND cp.price > 0
|
||||
AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}
|
||||
AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
|
||||
ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
|
||||
)
|
||||
"""
|
||||
|
||||
sql = text(f"""
|
||||
WITH latest_momo AS (
|
||||
SELECT
|
||||
@@ -1022,23 +1084,9 @@ def fetch_competitor_comparison_results(
|
||||
FROM products p
|
||||
JOIN price_records pr ON pr.product_id = p.id
|
||||
WHERE p.status = 'ACTIVE'
|
||||
{momo_price_cutoff}
|
||||
),
|
||||
valid_competitor AS (
|
||||
SELECT DISTINCT ON (cp.sku)
|
||||
cp.sku,
|
||||
cp.price AS pchome_price,
|
||||
cp.competitor_product_id,
|
||||
cp.competitor_product_name,
|
||||
cp.match_score
|
||||
FROM competitor_prices cp
|
||||
WHERE cp.source = 'pchome'
|
||||
AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP)
|
||||
AND cp.price IS NOT NULL
|
||||
AND cp.price > 0
|
||||
AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}
|
||||
AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
|
||||
ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
|
||||
),
|
||||
{valid_competitor_cte},
|
||||
{attempt_cte}
|
||||
{sales_cte}
|
||||
SELECT
|
||||
@@ -1049,6 +1097,8 @@ def fetch_competitor_comparison_results(
|
||||
vc.competitor_product_id,
|
||||
vc.competitor_product_name,
|
||||
vc.match_score,
|
||||
vc.crawled_at AS competitor_crawled_at,
|
||||
vc.competitor_source,
|
||||
la.attempt_status,
|
||||
la.candidate_count,
|
||||
la.best_competitor_product_id,
|
||||
@@ -1099,6 +1149,8 @@ def fetch_competitor_comparison_results(
|
||||
"price_diff_pct": _num(row.get("price_diff_pct")),
|
||||
"match_score": _num(row.get("match_score")),
|
||||
"momo_revenue": _num(row.get("momo_revenue")),
|
||||
"competitor_source": row.get("competitor_source") or "",
|
||||
"pc_crawled_at": _date_label(row.get("competitor_crawled_at")),
|
||||
"match_status": match_status,
|
||||
"match_status_label": _attempt_status_label(match_status),
|
||||
"action_label": _attempt_action_label(match_status),
|
||||
|
||||
@@ -53,6 +53,22 @@ def test_competitor_ppt_results_keep_pending_diagnostics_in_export():
|
||||
assert "((lm.momo_price - vc.pchome_price) / vc.pchome_price * 100) AS price_diff_pct" in source
|
||||
|
||||
|
||||
def test_competitor_ppt_results_use_history_for_dated_reports():
|
||||
source = (ROOT / "services" / "competitor_intel_repository.py").read_text(encoding="utf-8")
|
||||
route_source = (ROOT / "routes" / "openclaw_bot_routes.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "requested_historical_prices" in source
|
||||
assert "use_history_prices" in source
|
||||
assert "FROM competitor_price_history cph" in source
|
||||
assert "cph.crawled_at >= DATE(:start_date)" in source
|
||||
assert "cph.crawled_at < DATE(:end_date) + INTERVAL '1 day'" in source
|
||||
assert "pr.timestamp < DATE(:end_date) + INTERVAL '1 day'" in source
|
||||
assert "'competitor_price_history' AS competitor_source" in source
|
||||
assert "\"competitor_source\"" in source
|
||||
assert "\"pc_crawled_at\"" in source
|
||||
assert "指定期間 competitor_price_history" in route_source
|
||||
|
||||
|
||||
def test_competitor_ppt_and_ai_use_momo_minus_pchome_gap_direction():
|
||||
ppt_source = (ROOT / "services" / "ppt_generator.py").read_text(encoding="utf-8")
|
||||
route_source = (ROOT / "routes" / "openclaw_bot_routes.py").read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user