_mapper.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. """需求搜索队列 MySQL 读写 + 视频搜索词 PG 查询 —— 仅负责 DB I/O,不含业务逻辑"""
  2. from typing import Dict, List
  3. from src.infra.database.mysql.manager import MysqlManager
  4. from src.infra.database.postgresql.manager import PgManager
  5. from ._const import DemandConst
  6. class DemandQueueMapper(DemandConst):
  7. """demand_search_queue 表的数据访问层
  8. 职责: SQL 拼接 + 参数绑定 + 返回原生 dict。
  9. 不负责: 业务校验、格式转换(由上层 service 负责)。
  10. """
  11. def __init__(self, pool: MysqlManager):
  12. self.pool = pool
  13. # ═══════════════════════════════════════════════════════════════
  14. # 写入
  15. # ═══════════════════════════════════════════════════════════════
  16. async def insert_batch(self, items: List[Dict]) -> int:
  17. """批量写入队列,返回受影响行数
  18. items 每条须包含:
  19. account, search_key, dt, video_id, channel_name,
  20. key_type, experiment_id, drive_type
  21. """
  22. if not items:
  23. return 0
  24. query = f"""
  25. INSERT IGNORE INTO {self.QUEUE_TABLE}
  26. (account, search_key, dt, video_id,
  27. channel_name, category, key_type, experiment_id,
  28. status, drive_type)
  29. VALUES
  30. (%s, %s, %s, %s,
  31. %s, %s, %s, %s,
  32. %s, %s)
  33. """
  34. params = [
  35. (
  36. it["account"],
  37. it["search_key"],
  38. it["dt"],
  39. it["video_id"],
  40. it["channel_name"],
  41. it.get("category", ""),
  42. it["key_type"],
  43. it["experiment_id"],
  44. self.QueueStatus.INIT,
  45. it["drive_type"],
  46. )
  47. for it in items
  48. ]
  49. return await self.pool.save(query=query, params=params, batch=True)
  50. # ═══════════════════════════════════════════════════════════════
  51. # 读取(供下游 consumer 使用)
  52. # ═══════════════════════════════════════════════════════════════
  53. async def fetch_pending(
  54. self,
  55. dt: str,
  56. *,
  57. limit: int = 500,
  58. offset: int = 0,
  59. ) -> List[Dict]:
  60. """查询待搜索的队列项(按 dt)"""
  61. query = f"""
  62. SELECT *
  63. FROM {self.QUEUE_TABLE}
  64. WHERE dt = %s AND status = %s
  65. ORDER BY id ASC
  66. LIMIT %s OFFSET %s
  67. """
  68. return await self.pool.fetch(
  69. query=query,
  70. params=(dt, self.QueueStatus.INIT, limit, offset),
  71. )
  72. async def count_by_status(self, dt: str, status: int) -> int:
  73. """统计指定状态的队列项数量"""
  74. query = f"""
  75. SELECT COUNT(*) AS cnt
  76. FROM {self.QUEUE_TABLE}
  77. WHERE dt = %s AND status = %s
  78. """
  79. row = await self.pool.fetch_one(query=query, params=(dt, status))
  80. return row["cnt"] if row else 0
  81. # ═══════════════════════════════════════════════════════════════
  82. # 状态更新
  83. # ═══════════════════════════════════════════════════════════════
  84. async def mark_processing(self, item_id: int) -> int:
  85. query = f"""
  86. UPDATE {self.QUEUE_TABLE}
  87. SET status = %s
  88. WHERE id = %s AND status = %s
  89. """
  90. return await self.pool.save(
  91. query=query,
  92. params=(self.QueueStatus.PROCESSING, item_id, self.QueueStatus.INIT),
  93. )
  94. async def mark_processing_batch(self, ids: List[int]) -> int:
  95. """批量标记为处理中"""
  96. if not ids:
  97. return 0
  98. placeholders = ",".join(["%s"] * len(ids))
  99. query = f"""
  100. UPDATE {self.QUEUE_TABLE}
  101. SET status = %s
  102. WHERE id IN ({placeholders})
  103. """
  104. return await self.pool.save(
  105. query=query,
  106. params=(self.QueueStatus.PROCESSING, *ids),
  107. )
  108. async def mark_done(self, item_id: int) -> int:
  109. query = f"""
  110. UPDATE {self.QUEUE_TABLE}
  111. SET status = %s
  112. WHERE id = %s
  113. """
  114. return await self.pool.save(
  115. query=query, params=(self.QueueStatus.SUCCESS, item_id),
  116. )
  117. async def mark_failed(self, item_id: int, reason: str = "") -> int:
  118. query = f"""
  119. UPDATE {self.QUEUE_TABLE}
  120. SET status = %s, fail_reason = %s
  121. WHERE id = %s
  122. """
  123. return await self.pool.save(
  124. query=query,
  125. params=(self.QueueStatus.FAIL, reason[:512], item_id),
  126. )
  127. class VideoDeconstructMapper:
  128. """video_vectors 表的数据访问层(PostgreSQL)
  129. 职责: 按 video_id 查询搜索词,每行 (config_code, text) 为一个搜索词。
  130. config_code 直接对应 DemandConst.KeyType。
  131. """
  132. COLUMNS = ("video_id", "config_code", "text")
  133. def __init__(self, pg: PgManager):
  134. self.pg = pg
  135. async def find_by_video_ids(self, video_ids: list[int]) -> dict[int, list[Dict]]:
  136. """批量查询 video_vectors,一次网络往返
  137. Returns:
  138. {video_id: [{"config_code": "VIDEO_KEYPOINT", "text": "xx"}, ...], ...}
  139. 不存在的 video_id 不会出现在返回 key 中。
  140. """
  141. if not video_ids:
  142. return {}
  143. cols = ", ".join(self.COLUMNS)
  144. rows = await self.pg.fetch(
  145. f"SELECT {cols} FROM video_vectors WHERE video_id = ANY($1)",
  146. params=(video_ids,),
  147. )
  148. result: dict[int, list[Dict]] = {}
  149. for row in rows:
  150. vid = int(row["video_id"])
  151. config_code = (row.get("config_code") or "").strip()
  152. text_val = (row.get("text") or "").strip()
  153. if config_code and text_val:
  154. if vid not in result:
  155. result[vid] = []
  156. result[vid].append({"config_code": config_code, "text": text_val})
  157. return result
  158. __all__ = ["DemandQueueMapper", "VideoDeconstructMapper"]