repository.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """节日需求 MySQL 仓储。"""
  2. from __future__ import annotations
  3. from typing import Any
  4. try:
  5. import pymysql
  6. from pymysql.cursors import DictCursor
  7. except ImportError: # pragma: no cover
  8. pymysql = None
  9. DictCursor = None
  10. from app.festival_demand.demand_id import build_festival_demand_id
  11. from app.festival_demand.exceptions import FestivalDemandError
  12. from app.hot_content.types import MysqlConfig
  13. TABLE_NAME = "festival_demand_pool_di"
  14. class FestivalDemandRepository:
  15. def __init__(self, config: MysqlConfig):
  16. if pymysql is None or DictCursor is None:
  17. raise FestivalDemandError("missing dependency: pip install pymysql")
  18. self.conn = pymysql.connect(
  19. host=config.host,
  20. port=config.port,
  21. user=config.user,
  22. password=config.password,
  23. database=config.database,
  24. charset=config.charset,
  25. autocommit=True,
  26. cursorclass=DictCursor,
  27. )
  28. self._ensure_table()
  29. def close(self) -> None:
  30. self.conn.close()
  31. def _ensure_table(self) -> None:
  32. sql = f"""
  33. CREATE TABLE IF NOT EXISTS {TABLE_NAME} (
  34. id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  35. strategy VARCHAR(128) NOT NULL COMMENT '策略',
  36. demand_id CHAR(32) NOT NULL COMMENT '需求id',
  37. demand_name VARCHAR(1024) NOT NULL COMMENT '需求',
  38. partition_dt VARCHAR(8) NOT NULL DEFAULT '' COMMENT '业务日期 yyyymmdd',
  39. created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  40. updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
  41. ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  42. PRIMARY KEY (id),
  43. UNIQUE KEY uk_festival_demand_pool (strategy, demand_id),
  44. KEY idx_strategy_partition (strategy, partition_dt),
  45. KEY idx_demand_name (demand_name(191))
  46. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  47. """
  48. with self.conn.cursor() as cursor:
  49. cursor.execute(sql)
  50. self._ensure_partition_dt_column(cursor)
  51. @staticmethod
  52. def _ensure_partition_dt_column(cursor: Any) -> None:
  53. cursor.execute(
  54. f"""
  55. SELECT COUNT(*) AS cnt
  56. FROM information_schema.COLUMNS
  57. WHERE TABLE_SCHEMA = DATABASE()
  58. AND TABLE_NAME = '{TABLE_NAME}'
  59. AND COLUMN_NAME = 'partition_dt'
  60. """
  61. )
  62. row = cursor.fetchone() or {}
  63. if int(row.get("cnt") or 0) > 0:
  64. return
  65. cursor.execute(
  66. f"""
  67. ALTER TABLE {TABLE_NAME}
  68. ADD COLUMN partition_dt VARCHAR(8) NOT NULL DEFAULT '' COMMENT '业务日期 yyyymmdd'
  69. AFTER demand_name,
  70. ADD KEY idx_strategy_partition (strategy, partition_dt)
  71. """
  72. )
  73. def has_partition_data(self, *, strategy: str, partition_dt: str) -> bool:
  74. strategy_value = strategy.strip()
  75. partition_value = partition_dt.strip()
  76. if not strategy_value or not partition_value:
  77. return False
  78. sql = f"""
  79. SELECT 1
  80. FROM {TABLE_NAME}
  81. WHERE strategy = %s
  82. AND partition_dt = %s
  83. LIMIT 1
  84. """
  85. with self.conn.cursor() as cursor:
  86. cursor.execute(sql, (strategy_value, partition_value))
  87. return cursor.fetchone() is not None
  88. def build_demand_rows(
  89. self,
  90. *,
  91. demand_names: list[str],
  92. strategy: str,
  93. partition_dt: str,
  94. ) -> list[dict[str, str]]:
  95. strategy_value = strategy.strip()
  96. partition_value = partition_dt.strip()
  97. if not strategy_value:
  98. raise FestivalDemandError("festival demand output strategy is required")
  99. if not partition_value:
  100. raise FestivalDemandError("partition_dt is required")
  101. rows: list[dict[str, str]] = []
  102. seen_demand_ids: set[str] = set()
  103. for raw_name in demand_names:
  104. demand_name = str(raw_name).strip()
  105. if not demand_name:
  106. continue
  107. demand_id = build_festival_demand_id(
  108. strategy=strategy_value,
  109. demand_name=demand_name,
  110. partition_dt=partition_value,
  111. )
  112. if demand_id in seen_demand_ids:
  113. continue
  114. seen_demand_ids.add(demand_id)
  115. rows.append(
  116. {
  117. "strategy": strategy_value,
  118. "demand_id": demand_id,
  119. "demand_name": demand_name,
  120. "partition_dt": partition_value,
  121. }
  122. )
  123. return rows
  124. def insert_demand_rows(self, rows: list[dict[str, str]]) -> int:
  125. if not rows:
  126. return 0
  127. sql = f"""
  128. INSERT INTO {TABLE_NAME} (
  129. strategy,
  130. demand_id,
  131. demand_name,
  132. partition_dt
  133. )
  134. VALUES (%s, %s, %s, %s)
  135. """
  136. payload = [
  137. (
  138. row["strategy"],
  139. row["demand_id"],
  140. row["demand_name"],
  141. row["partition_dt"],
  142. )
  143. for row in rows
  144. ]
  145. with self.conn.cursor() as cursor:
  146. cursor.executemany(sql, payload)
  147. return len(rows)
  148. def persist_generated_demands(
  149. self,
  150. *,
  151. demand_names: list[str],
  152. strategy: str,
  153. partition_dt: str,
  154. ) -> dict[str, Any]:
  155. """当天已有数据则跳过;否则写入 MySQL 并返回待同步 ODPS 的行。"""
  156. strategy_value = strategy.strip()
  157. partition_value = partition_dt.strip()
  158. base_result = {
  159. "table": TABLE_NAME,
  160. "strategy": strategy_value,
  161. "partition_dt": partition_value,
  162. "input_count": len(demand_names),
  163. "skipped": False,
  164. "saved_count": 0,
  165. "rows": [],
  166. }
  167. if self.has_partition_data(strategy=strategy_value, partition_dt=partition_value):
  168. return {
  169. **base_result,
  170. "skipped": True,
  171. "skip_reason": "partition_data_exists",
  172. }
  173. rows = self.build_demand_rows(
  174. demand_names=demand_names,
  175. strategy=strategy_value,
  176. partition_dt=partition_value,
  177. )
  178. if not rows:
  179. return base_result
  180. saved_count = self.insert_demand_rows(rows)
  181. return {
  182. **base_result,
  183. "saved_count": saved_count,
  184. "rows": rows,
  185. }