|
|
@@ -3,16 +3,18 @@
|
|
|
|
|
|
流程:
|
|
|
1. 比对当天 ODPS / MySQL 行数,相同则跳过写入
|
|
|
-2. 有差异时拉取 ODPS,按 (strategy, demand_id) 只插入缺失、删除多余
|
|
|
-3. 拉取近 7 日真实 ROV/VOV,按特征值匹配回填 real_rov_7d / real_vov_7d
|
|
|
-4. 查询当天全部 demand_name,按空格分词写入 set
|
|
|
-5. 过滤 demand_belong_category 中已存在的词
|
|
|
-6. 剩余词按 100 词一批调用 demand_belong_category_agent
|
|
|
-7. 遍历 demand_belong_category 全部词,按策略与真实 ROV/VOV 写入 demand_popularity_stats(avg/count)
|
|
|
-8. 基于三表计算整棵类目树节点加权平均分,写入 category_tree_weight
|
|
|
+2. 有差异时拉取 ODPS,按 (strategy, demand_id) 只插入缺失、删除多余,并回填已有行 video_list
|
|
|
+3. video_list 每条最多保留前 100 个 video_id,video_count 与之保持一致
|
|
|
+4. 拉取近 7 日真实 ROV/VOV,按特征值匹配回填 real_rov_7d / real_vov_7d
|
|
|
+5. 查询当天全部 demand_name,按空格分词写入 set
|
|
|
+6. 过滤 demand_belong_category 中已存在的词
|
|
|
+7. 剩余词按 100 词一批调用 demand_belong_category_agent
|
|
|
+8. 遍历 demand_belong_category 全部词,按策略与真实 ROV/VOV 写入 demand_popularity_stats(avg/count)
|
|
|
+9. 基于三表计算整棵类目树节点加权平均分,写入 category_tree_weight
|
|
|
"""
|
|
|
from __future__ import annotations
|
|
|
|
|
|
+import json
|
|
|
import logging
|
|
|
from datetime import datetime, timedelta
|
|
|
from decimal import Decimal
|
|
|
@@ -39,6 +41,7 @@ _STATS_UPSERT_BATCH = 200
|
|
|
_REAL_METRIC_LIMIT = 1000
|
|
|
_REAL_METRIC_LOOKBACK_DAYS = 7
|
|
|
_GLOBAL_FEATURE_VALUE = "全局SUM"
|
|
|
+_VIDEO_LIST_LIMIT = 100
|
|
|
|
|
|
# 策略名 → 统计字段前缀;去年同期阳历/阴历合并为 plat_ly_pop
|
|
|
_STRATEGY_METRIC: dict[str, str] = {
|
|
|
@@ -68,6 +71,39 @@ def _row_key(row: dict[str, Any]) -> RowKey:
|
|
|
return (row["strategy"], row["demand_id"])
|
|
|
|
|
|
|
|
|
+def _normalize_video_list(raw: Any) -> tuple[str | None, int]:
|
|
|
+ """取前 N 个 video_id,返回 (JSON 文本, count),二者保持一致。"""
|
|
|
+ if raw is None:
|
|
|
+ return None, 0
|
|
|
+
|
|
|
+ items: list[Any]
|
|
|
+ if isinstance(raw, str):
|
|
|
+ text = raw.strip()
|
|
|
+ if not text:
|
|
|
+ return None, 0
|
|
|
+ try:
|
|
|
+ parsed = json.loads(text)
|
|
|
+ items = list(parsed) if isinstance(parsed, list) else [text]
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ items = [part.strip() for part in text.split(",") if part.strip()]
|
|
|
+ elif isinstance(raw, (list, tuple)):
|
|
|
+ items = list(raw)
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ items = list(raw)
|
|
|
+ except TypeError:
|
|
|
+ return None, 0
|
|
|
+
|
|
|
+ truncated = [
|
|
|
+ str(v).strip()
|
|
|
+ for v in items[:_VIDEO_LIST_LIMIT]
|
|
|
+ if v is not None and str(v).strip()
|
|
|
+ ]
|
|
|
+ if not truncated:
|
|
|
+ return None, 0
|
|
|
+ return json.dumps(truncated, ensure_ascii=False), len(truncated)
|
|
|
+
|
|
|
+
|
|
|
def _to_mysql_rows(raw_rows: list[dict[str, Any]], biz_dt: str) -> list[dict[str, Any]]:
|
|
|
"""转换为 MySQL 行,按 (strategy, demand_id) 去重(保留最后一条)。"""
|
|
|
by_key: dict[RowKey, dict[str, Any]] = {}
|
|
|
@@ -79,14 +115,15 @@ def _to_mysql_rows(raw_rows: list[dict[str, Any]], biz_dt: str) -> list[dict[str
|
|
|
logger.warning("Skip row with missing required fields: %s", row)
|
|
|
continue
|
|
|
|
|
|
+ video_list, video_count = _normalize_video_list(row.get("video_list"))
|
|
|
mapped = {
|
|
|
"strategy": str(strategy),
|
|
|
"demand_id": str(demand_id),
|
|
|
"demand_name": str(demand_name),
|
|
|
"weight": row.get("weight"),
|
|
|
"type": str(row["type"]) if row.get("type") is not None else None,
|
|
|
- "video_count": row.get("video_count"),
|
|
|
- "video_list": None,
|
|
|
+ "video_count": video_count,
|
|
|
+ "video_list": video_list,
|
|
|
"extend": str(row["extend"]) if row.get("extend") is not None else None,
|
|
|
"biz_dt": biz_dt,
|
|
|
}
|
|
|
@@ -143,7 +180,7 @@ def _classify_words(biz_dt: str) -> dict:
|
|
|
|
|
|
|
|
|
def _sync_diff(partition_date: str) -> dict[str, Any]:
|
|
|
- """行数不同时拉取 ODPS,只同步 (strategy, demand_id) 差异。"""
|
|
|
+ """行数不同时拉取 ODPS,只同步 (strategy, demand_id) 差异,并回填已有行的 video 字段。"""
|
|
|
odps = get_odps_client()
|
|
|
raw_rows = odps.fetch_multi_demand_pool(partition_date)
|
|
|
mysql_rows = _to_mysql_rows(raw_rows, partition_date)
|
|
|
@@ -156,17 +193,21 @@ def _sync_diff(partition_date: str) -> dict[str, Any]:
|
|
|
|
|
|
to_insert_keys = odps_keys - mysql_keys
|
|
|
to_delete_keys = mysql_keys - odps_keys
|
|
|
+ to_update_keys = odps_keys & mysql_keys
|
|
|
|
|
|
insert_rows = [odps_by_key[k] for k in to_insert_keys]
|
|
|
+ update_rows = [odps_by_key[k] for k in to_update_keys]
|
|
|
deleted = repo.delete_by_keys(partition_date, list(to_delete_keys))
|
|
|
inserted = repo.bulk_insert(insert_rows)
|
|
|
+ updated = repo.update_video_fields(partition_date, update_rows)
|
|
|
|
|
|
logger.info(
|
|
|
- "Diff sync: odps=%d mysql_before=%d insert=%d delete=%d",
|
|
|
+ "Diff sync: odps=%d mysql_before=%d insert=%d delete=%d video_update=%d",
|
|
|
len(odps_keys),
|
|
|
len(mysql_keys),
|
|
|
inserted,
|
|
|
deleted,
|
|
|
+ updated,
|
|
|
)
|
|
|
return {
|
|
|
"fetched": len(raw_rows),
|
|
|
@@ -174,10 +215,35 @@ def _sync_diff(partition_date: str) -> dict[str, Any]:
|
|
|
"mysql_before": len(mysql_keys),
|
|
|
"inserted": inserted,
|
|
|
"deleted": deleted,
|
|
|
+ "video_updated": updated,
|
|
|
"skipped_invalid": len(raw_rows) - len(mysql_rows),
|
|
|
}
|
|
|
|
|
|
|
|
|
+def backfill_video_list(partition_date: str) -> dict[str, Any]:
|
|
|
+ """从 ODPS 回填指定分区的 video_list / video_count(每条最多前 100 个 video_id)。"""
|
|
|
+ logger.info("Backfill video_list for partition: %s", partition_date)
|
|
|
+ odps = get_odps_client()
|
|
|
+ raw_rows = odps.fetch_multi_demand_pool(partition_date)
|
|
|
+ mysql_rows = _to_mysql_rows(raw_rows, partition_date)
|
|
|
+
|
|
|
+ with get_session() as session:
|
|
|
+ updated = MultiDemandPoolDiRepository(session).update_video_fields(
|
|
|
+ partition_date,
|
|
|
+ mysql_rows,
|
|
|
+ )
|
|
|
+
|
|
|
+ result = {
|
|
|
+ "partition_date": partition_date,
|
|
|
+ "fetched": len(raw_rows),
|
|
|
+ "unique_rows": len(mysql_rows),
|
|
|
+ "updated": updated,
|
|
|
+ "with_video": sum(1 for r in mysql_rows if r.get("video_list")),
|
|
|
+ }
|
|
|
+ logger.info("Backfill video_list completed: %s", result)
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
def _to_float(value: Any) -> float | None:
|
|
|
if value is None:
|
|
|
return None
|