multi_demand_pool_di_repo.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. from __future__ import annotations
  2. from sqlalchemy import delete, func, select, tuple_, update
  3. from sqlalchemy.dialects.mysql import insert
  4. from supply_infra.db.models.multi_demand_pool_di import MultiDemandPoolDi
  5. from supply_infra.db.repositories.base import BaseRepository
  6. _BATCH_SIZE = 1000
  7. RowKey = tuple[str, str]
  8. RealMetric = tuple[float | None, float | None]
  9. class MultiDemandPoolDiRepository(BaseRepository[MultiDemandPoolDi]):
  10. """策略需求天级表 repository。"""
  11. model = MultiDemandPoolDi
  12. def get_latest_biz_dt(self) -> str | None:
  13. """返回需求池中最新业务日;无数据时返回 None。"""
  14. stmt = select(func.max(MultiDemandPoolDi.biz_dt))
  15. return self.session.scalar(stmt)
  16. def list_distinct_demand_name_summaries(
  17. self,
  18. biz_dt: str,
  19. *,
  20. limit: int = 50,
  21. offset: int = 0,
  22. exclude_names: list[str] | None = None,
  23. ) -> list[dict]:
  24. """
  25. 按业务日分页列出去重需求词及聚合统计。
  26. 返回每项包含:demand_name、row_count(出现行数)、strategies(策略列表)、
  27. total_weight、total_video_count、max_real_rov_7d、max_real_vov_7d。
  28. 按 max_real_rov_7d 降序、total_weight 降序排列,优先暴露有后验数据/高权重的需求词。
  29. """
  30. stmt = (
  31. select(
  32. MultiDemandPoolDi.demand_name,
  33. func.count().label("row_count"),
  34. func.group_concat(MultiDemandPoolDi.strategy.distinct()).label("strategies"),
  35. func.sum(MultiDemandPoolDi.weight).label("total_weight"),
  36. func.sum(MultiDemandPoolDi.video_count).label("total_video_count"),
  37. func.max(MultiDemandPoolDi.real_rov_7d).label("max_real_rov_7d"),
  38. func.max(MultiDemandPoolDi.real_vov_7d).label("max_real_vov_7d"),
  39. )
  40. .where(MultiDemandPoolDi.biz_dt == biz_dt)
  41. )
  42. if exclude_names:
  43. stmt = stmt.where(MultiDemandPoolDi.demand_name.notin_(exclude_names))
  44. stmt = (
  45. stmt.group_by(MultiDemandPoolDi.demand_name)
  46. .order_by(
  47. func.max(MultiDemandPoolDi.real_rov_7d).desc(),
  48. func.sum(MultiDemandPoolDi.weight).desc(),
  49. )
  50. .limit(limit)
  51. .offset(offset)
  52. )
  53. rows = self.session.execute(stmt).all()
  54. return [
  55. {
  56. "demand_name": name,
  57. "row_count": int(row_count or 0),
  58. "strategies": (strategies or "").split(",") if strategies else [],
  59. "total_weight": float(total_weight) if total_weight is not None else None,
  60. "total_video_count": int(total_video_count) if total_video_count is not None else None,
  61. "max_real_rov_7d": float(max_rov) if max_rov is not None else None,
  62. "max_real_vov_7d": float(max_vov) if max_vov is not None else None,
  63. }
  64. for name, row_count, strategies, total_weight, total_video_count, max_rov, max_vov in rows
  65. ]
  66. def count_distinct_demand_names(self, biz_dt: str) -> int:
  67. """统计指定业务日去重需求词总数。"""
  68. stmt = select(func.count(func.distinct(MultiDemandPoolDi.demand_name))).where(
  69. MultiDemandPoolDi.biz_dt == biz_dt
  70. )
  71. return int(self.session.scalar(stmt) or 0)
  72. def list_by_biz_dt(self, biz_dt: str) -> list[MultiDemandPoolDi]:
  73. """返回业务日全部需求池行,供来源内排名等全局计算使用。"""
  74. stmt = (
  75. select(MultiDemandPoolDi)
  76. .where(MultiDemandPoolDi.biz_dt == biz_dt)
  77. .order_by(MultiDemandPoolDi.strategy, MultiDemandPoolDi.demand_name, MultiDemandPoolDi.id)
  78. )
  79. return list(self.session.scalars(stmt).all())
  80. def search_rows_by_name_fragment(self, biz_dt: str, keyword: str) -> list[dict]:
  81. """
  82. 按业务日 + 需求名双向包含关系搜索明细行。
  83. 匹配 demand_name LIKE %keyword% 或 keyword LIKE %demand_name%(互相包含),
  84. 用于把同语义、措辞不同的需求词合并到一起判断。返回按 demand_name 去重后的明细。
  85. """
  86. keyword = (keyword or "").strip()
  87. if not keyword:
  88. return []
  89. stmt = select(MultiDemandPoolDi).where(
  90. MultiDemandPoolDi.biz_dt == biz_dt,
  91. MultiDemandPoolDi.demand_name.like(f"%{keyword}%"),
  92. )
  93. rows = list(self.session.scalars(stmt).all())
  94. if len(keyword) >= 2:
  95. broad_stmt = select(MultiDemandPoolDi).where(
  96. MultiDemandPoolDi.biz_dt == biz_dt,
  97. )
  98. seen_ids = {int(r.id) for r in rows}
  99. for row in self.session.scalars(broad_stmt).all():
  100. if int(row.id) in seen_ids:
  101. continue
  102. name = row.demand_name or ""
  103. if name and name in keyword:
  104. rows.append(row)
  105. seen_ids.add(int(row.id))
  106. return [
  107. {
  108. "id": int(row.id),
  109. "demand_name": row.demand_name,
  110. "strategy": row.strategy,
  111. "weight": None if row.weight is None or row.weight == 0 else float(row.weight),
  112. "video_count": int(row.video_count) if row.video_count is not None else None,
  113. "real_rov_7d": float(row.real_rov_7d) if row.real_rov_7d is not None else None,
  114. "real_vov_7d": float(row.real_vov_7d) if row.real_vov_7d is not None else None,
  115. }
  116. for row in rows
  117. ]
  118. def get_by_ids(self, ids: list[int]) -> list[MultiDemandPoolDi]:
  119. """按 id 批量查询完整行。"""
  120. if not ids:
  121. return []
  122. rows: list[MultiDemandPoolDi] = []
  123. for start in range(0, len(ids), _BATCH_SIZE):
  124. batch = ids[start : start + _BATCH_SIZE]
  125. stmt = select(MultiDemandPoolDi).where(MultiDemandPoolDi.id.in_(batch))
  126. rows.extend(self.session.scalars(stmt).all())
  127. return rows
  128. def count_by_biz_dt(self, biz_dt: str) -> int:
  129. """统计指定业务日期去重行数(strategy + demand_id)。"""
  130. stmt = (
  131. select(func.count())
  132. .select_from(
  133. select(MultiDemandPoolDi.strategy, MultiDemandPoolDi.demand_id)
  134. .where(MultiDemandPoolDi.biz_dt == biz_dt)
  135. .distinct()
  136. .subquery()
  137. )
  138. )
  139. return int(self.session.scalar(stmt) or 0)
  140. def list_keys_by_biz_dt(self, biz_dt: str) -> set[RowKey]:
  141. """查询指定业务日期的 (strategy, demand_id) 键集合。"""
  142. stmt = select(MultiDemandPoolDi.strategy, MultiDemandPoolDi.demand_id).where(
  143. MultiDemandPoolDi.biz_dt == biz_dt
  144. )
  145. return {(str(s), str(d)) for s, d in self.session.execute(stmt).all()}
  146. def list_source_rows_by_biz_dt(self, biz_dt: str) -> list[dict]:
  147. """返回同步差分需要的源字段,不包含后续步骤回填的 ROV/VOV。"""
  148. stmt = select(
  149. MultiDemandPoolDi.id,
  150. MultiDemandPoolDi.strategy,
  151. MultiDemandPoolDi.demand_id,
  152. MultiDemandPoolDi.demand_name,
  153. MultiDemandPoolDi.weight,
  154. MultiDemandPoolDi.type,
  155. MultiDemandPoolDi.video_count,
  156. MultiDemandPoolDi.video_list,
  157. MultiDemandPoolDi.extend,
  158. MultiDemandPoolDi.biz_dt,
  159. ).where(MultiDemandPoolDi.biz_dt == biz_dt)
  160. return [
  161. {
  162. "id": int(row_id),
  163. "strategy": str(strategy),
  164. "demand_id": str(demand_id),
  165. "demand_name": str(demand_name),
  166. "weight": float(weight) if weight is not None else None,
  167. "type": str(demand_type) if demand_type is not None else None,
  168. "video_count": int(video_count) if video_count is not None else 0,
  169. "video_list": video_list,
  170. "extend": extend,
  171. "biz_dt": str(row_biz_dt),
  172. }
  173. for (
  174. row_id,
  175. strategy,
  176. demand_id,
  177. demand_name,
  178. weight,
  179. demand_type,
  180. video_count,
  181. video_list,
  182. extend,
  183. row_biz_dt,
  184. ) in self.session.execute(stmt).all()
  185. ]
  186. def delete_by_biz_dt(self, biz_dt: str) -> int:
  187. """删除指定业务日期的全部数据,便于按日重跑。"""
  188. stmt = delete(MultiDemandPoolDi).where(MultiDemandPoolDi.biz_dt == biz_dt)
  189. result = self.session.execute(stmt)
  190. return result.rowcount or 0
  191. def delete_by_keys(self, biz_dt: str, keys: list[RowKey]) -> int:
  192. """按 (strategy, demand_id) 批量删除指定业务日期数据。"""
  193. if not keys:
  194. return 0
  195. deleted = 0
  196. for i in range(0, len(keys), _BATCH_SIZE):
  197. batch = keys[i : i + _BATCH_SIZE]
  198. stmt = delete(MultiDemandPoolDi).where(
  199. MultiDemandPoolDi.biz_dt == biz_dt,
  200. tuple_(MultiDemandPoolDi.strategy, MultiDemandPoolDi.demand_id).in_(batch),
  201. )
  202. result = self.session.execute(stmt)
  203. deleted += result.rowcount or 0
  204. return deleted
  205. def list_demand_names_by_biz_dt(self, biz_dt: str) -> list[str]:
  206. """查询指定业务日期的全部 demand_name。"""
  207. stmt = select(MultiDemandPoolDi.demand_name).where(
  208. MultiDemandPoolDi.biz_dt == biz_dt
  209. )
  210. return [name for name in self.session.scalars(stmt).all() if name]
  211. def list_all_video_lists(self) -> list[str]:
  212. """查询全表非空 video_list(JSON 文本)。"""
  213. stmt = select(MultiDemandPoolDi.video_list).where(
  214. MultiDemandPoolDi.video_list.isnot(None),
  215. MultiDemandPoolDi.video_list != "",
  216. )
  217. return [text for text in self.session.scalars(stmt).all() if text]
  218. def list_id_name_video_lists(
  219. self,
  220. biz_dt: str,
  221. ) -> list[tuple[int, str, str | None]]:
  222. """查询指定业务日的 (id, demand_name, video_list)。"""
  223. stmt = select(
  224. MultiDemandPoolDi.id,
  225. MultiDemandPoolDi.demand_name,
  226. MultiDemandPoolDi.video_list,
  227. ).where(MultiDemandPoolDi.biz_dt == biz_dt)
  228. return [
  229. (int(row_id), str(name), video_list)
  230. for row_id, name, video_list in self.session.execute(stmt).all()
  231. if name is not None
  232. ]
  233. def list_weights_by_name_like(
  234. self,
  235. biz_dt: str,
  236. keyword: str,
  237. strategies: list[str],
  238. ) -> list[tuple[str, float | None]]:
  239. """按日期 + demand_name LIKE + 指定策略,返回 (strategy, weight)。"""
  240. if not keyword or not strategies:
  241. return []
  242. stmt = select(MultiDemandPoolDi.strategy, MultiDemandPoolDi.weight).where(
  243. MultiDemandPoolDi.biz_dt == biz_dt,
  244. MultiDemandPoolDi.demand_name.like(f"%{keyword}%"),
  245. MultiDemandPoolDi.strategy.in_(strategies),
  246. )
  247. return [
  248. (str(strategy), None if weight is None or weight == 0 else float(weight))
  249. for strategy, weight in self.session.execute(stmt).all()
  250. ]
  251. def list_real_metrics_by_name_like(
  252. self,
  253. biz_dt: str,
  254. keyword: str,
  255. ) -> list[tuple[float | None, float | None]]:
  256. """
  257. 按日期 + demand_name LIKE 返回 rov_diff / vov_diff(real_rov_7d / real_vov_7d 字段)。
  258. 同一 demand_name 只取一条(MAX),避免多策略重复膨胀 count。
  259. """
  260. if not keyword:
  261. return []
  262. stmt = (
  263. select(
  264. func.max(MultiDemandPoolDi.real_rov_7d),
  265. func.max(MultiDemandPoolDi.real_vov_7d),
  266. )
  267. .where(
  268. MultiDemandPoolDi.biz_dt == biz_dt,
  269. MultiDemandPoolDi.demand_name.like(f"%{keyword}%"),
  270. )
  271. .group_by(MultiDemandPoolDi.demand_name)
  272. )
  273. return [
  274. (
  275. float(rov) if rov is not None else None,
  276. float(vov) if vov is not None else None,
  277. )
  278. for rov, vov in self.session.execute(stmt).all()
  279. ]
  280. def bulk_insert(self, rows: list[dict]) -> int:
  281. """批量插入。"""
  282. if not rows:
  283. return 0
  284. inserted = 0
  285. for i in range(0, len(rows), _BATCH_SIZE):
  286. batch = rows[i : i + _BATCH_SIZE]
  287. stmt = insert(MultiDemandPoolDi).values(batch)
  288. result = self.session.execute(stmt)
  289. inserted += result.rowcount
  290. return inserted
  291. def update_real_metrics_by_demand_name(
  292. self,
  293. biz_dt: str,
  294. metrics_by_name: dict[str, RealMetric],
  295. ) -> int:
  296. """按 demand_name(特征值)批量更新 rov_diff / vov_diff(real_rov_7d / real_vov_7d)。"""
  297. if not metrics_by_name:
  298. return 0
  299. updated = 0
  300. for demand_name, (real_rov_7d, real_vov_7d) in metrics_by_name.items():
  301. stmt = (
  302. update(MultiDemandPoolDi)
  303. .where(
  304. MultiDemandPoolDi.biz_dt == biz_dt,
  305. MultiDemandPoolDi.demand_name == demand_name,
  306. )
  307. .values(real_rov_7d=real_rov_7d, real_vov_7d=real_vov_7d)
  308. )
  309. result = self.session.execute(stmt)
  310. updated += result.rowcount or 0
  311. return updated
  312. def update_source_fields(self, biz_dt: str, rows: list[dict]) -> int:
  313. """按 (strategy, demand_id) 更新所有会影响下游的上游源字段。"""
  314. if not rows:
  315. return 0
  316. updated = 0
  317. for row in rows:
  318. stmt = (
  319. update(MultiDemandPoolDi)
  320. .where(
  321. MultiDemandPoolDi.biz_dt == biz_dt,
  322. MultiDemandPoolDi.strategy == row["strategy"],
  323. MultiDemandPoolDi.demand_id == row["demand_id"],
  324. )
  325. .values(
  326. demand_name=row["demand_name"],
  327. video_list=row.get("video_list"),
  328. video_count=row.get("video_count"),
  329. weight=row.get("weight"),
  330. type=row.get("type"),
  331. extend=row.get("extend"),
  332. )
  333. )
  334. result = self.session.execute(stmt)
  335. updated += result.rowcount or 0
  336. return updated