multi_demand_pool_di_repo.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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 delete_by_biz_dt(self, biz_dt: str) -> int:
  147. """删除指定业务日期的全部数据,便于按日重跑。"""
  148. stmt = delete(MultiDemandPoolDi).where(MultiDemandPoolDi.biz_dt == biz_dt)
  149. result = self.session.execute(stmt)
  150. return result.rowcount or 0
  151. def delete_by_keys(self, biz_dt: str, keys: list[RowKey]) -> int:
  152. """按 (strategy, demand_id) 批量删除指定业务日期数据。"""
  153. if not keys:
  154. return 0
  155. deleted = 0
  156. for i in range(0, len(keys), _BATCH_SIZE):
  157. batch = keys[i : i + _BATCH_SIZE]
  158. stmt = delete(MultiDemandPoolDi).where(
  159. MultiDemandPoolDi.biz_dt == biz_dt,
  160. tuple_(MultiDemandPoolDi.strategy, MultiDemandPoolDi.demand_id).in_(batch),
  161. )
  162. result = self.session.execute(stmt)
  163. deleted += result.rowcount or 0
  164. return deleted
  165. def list_demand_names_by_biz_dt(self, biz_dt: str) -> list[str]:
  166. """查询指定业务日期的全部 demand_name。"""
  167. stmt = select(MultiDemandPoolDi.demand_name).where(
  168. MultiDemandPoolDi.biz_dt == biz_dt
  169. )
  170. return [name for name in self.session.scalars(stmt).all() if name]
  171. def list_all_video_lists(self) -> list[str]:
  172. """查询全表非空 video_list(JSON 文本)。"""
  173. stmt = select(MultiDemandPoolDi.video_list).where(
  174. MultiDemandPoolDi.video_list.isnot(None),
  175. MultiDemandPoolDi.video_list != "",
  176. )
  177. return [text for text in self.session.scalars(stmt).all() if text]
  178. def list_id_name_video_lists(self) -> list[tuple[int, str, str | None]]:
  179. """查询全表 (id, demand_name, video_list)。"""
  180. stmt = select(
  181. MultiDemandPoolDi.id,
  182. MultiDemandPoolDi.demand_name,
  183. MultiDemandPoolDi.video_list,
  184. )
  185. return [
  186. (int(row_id), str(name), video_list)
  187. for row_id, name, video_list in self.session.execute(stmt).all()
  188. if name is not None
  189. ]
  190. def list_weights_by_name_like(
  191. self,
  192. biz_dt: str,
  193. keyword: str,
  194. strategies: list[str],
  195. ) -> list[tuple[str, float | None]]:
  196. """按日期 + demand_name LIKE + 指定策略,返回 (strategy, weight)。"""
  197. if not keyword or not strategies:
  198. return []
  199. stmt = select(MultiDemandPoolDi.strategy, MultiDemandPoolDi.weight).where(
  200. MultiDemandPoolDi.biz_dt == biz_dt,
  201. MultiDemandPoolDi.demand_name.like(f"%{keyword}%"),
  202. MultiDemandPoolDi.strategy.in_(strategies),
  203. )
  204. return [
  205. (str(strategy), None if weight is None or weight == 0 else float(weight))
  206. for strategy, weight in self.session.execute(stmt).all()
  207. ]
  208. def list_real_metrics_by_name_like(
  209. self,
  210. biz_dt: str,
  211. keyword: str,
  212. ) -> list[tuple[float | None, float | None]]:
  213. """
  214. 按日期 + demand_name LIKE 返回 rov_diff / vov_diff(real_rov_7d / real_vov_7d 字段)。
  215. 同一 demand_name 只取一条(MAX),避免多策略重复膨胀 count。
  216. """
  217. if not keyword:
  218. return []
  219. stmt = (
  220. select(
  221. func.max(MultiDemandPoolDi.real_rov_7d),
  222. func.max(MultiDemandPoolDi.real_vov_7d),
  223. )
  224. .where(
  225. MultiDemandPoolDi.biz_dt == biz_dt,
  226. MultiDemandPoolDi.demand_name.like(f"%{keyword}%"),
  227. )
  228. .group_by(MultiDemandPoolDi.demand_name)
  229. )
  230. return [
  231. (
  232. float(rov) if rov is not None else None,
  233. float(vov) if vov is not None else None,
  234. )
  235. for rov, vov in self.session.execute(stmt).all()
  236. ]
  237. def bulk_insert(self, rows: list[dict]) -> int:
  238. """批量插入。"""
  239. if not rows:
  240. return 0
  241. inserted = 0
  242. for i in range(0, len(rows), _BATCH_SIZE):
  243. batch = rows[i : i + _BATCH_SIZE]
  244. stmt = insert(MultiDemandPoolDi).values(batch)
  245. result = self.session.execute(stmt)
  246. inserted += result.rowcount
  247. return inserted
  248. def update_real_metrics_by_demand_name(
  249. self,
  250. biz_dt: str,
  251. metrics_by_name: dict[str, RealMetric],
  252. ) -> int:
  253. """按 demand_name(特征值)批量更新 rov_diff / vov_diff(real_rov_7d / real_vov_7d)。"""
  254. if not metrics_by_name:
  255. return 0
  256. updated = 0
  257. for demand_name, (real_rov_7d, real_vov_7d) in metrics_by_name.items():
  258. stmt = (
  259. update(MultiDemandPoolDi)
  260. .where(
  261. MultiDemandPoolDi.biz_dt == biz_dt,
  262. MultiDemandPoolDi.demand_name == demand_name,
  263. )
  264. .values(real_rov_7d=real_rov_7d, real_vov_7d=real_vov_7d)
  265. )
  266. result = self.session.execute(stmt)
  267. updated += result.rowcount or 0
  268. return updated
  269. def update_video_fields(self, biz_dt: str, rows: list[dict]) -> int:
  270. """按 (strategy, demand_id) 批量更新 video_list / video_count / weight。"""
  271. if not rows:
  272. return 0
  273. updated = 0
  274. for row in rows:
  275. stmt = (
  276. update(MultiDemandPoolDi)
  277. .where(
  278. MultiDemandPoolDi.biz_dt == biz_dt,
  279. MultiDemandPoolDi.strategy == row["strategy"],
  280. MultiDemandPoolDi.demand_id == row["demand_id"],
  281. )
  282. .values(
  283. video_list=row.get("video_list"),
  284. video_count=row.get("video_count"),
  285. weight=row.get("weight"),
  286. )
  287. )
  288. result = self.session.execute(stmt)
  289. updated += result.rowcount or 0
  290. return updated