repository.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. """当下供需 gap 脚本主驱 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.gap_script_demand.demand_id import build_gap_script_demand_id
  11. from app.gap_script_demand.exceptions import GapScriptDemandError
  12. from app.hot_content.types import MysqlConfig
  13. TABLE_NAME = "gap_script_demand_pool_di"
  14. class GapScriptDemandRepository:
  15. def __init__(self, config: MysqlConfig):
  16. if pymysql is None or DictCursor is None:
  17. raise GapScriptDemandError("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_gap_script_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. def has_partition_data(self, *, strategy: str, partition_dt: str) -> bool:
  51. strategy_value = strategy.strip()
  52. partition_value = partition_dt.strip()
  53. if not strategy_value or not partition_value:
  54. return False
  55. sql = f"""
  56. SELECT 1
  57. FROM {TABLE_NAME}
  58. WHERE strategy = %s
  59. AND partition_dt = %s
  60. LIMIT 1
  61. """
  62. with self.conn.cursor() as cursor:
  63. cursor.execute(sql, (strategy_value, partition_value))
  64. return cursor.fetchone() is not None
  65. def build_demand_rows(
  66. self,
  67. *,
  68. demand_names: list[str],
  69. strategy: str,
  70. partition_dt: str,
  71. ) -> list[dict[str, str]]:
  72. strategy_value = strategy.strip()
  73. partition_value = partition_dt.strip()
  74. if not strategy_value:
  75. raise GapScriptDemandError("gap script demand output strategy is required")
  76. if not partition_value:
  77. raise GapScriptDemandError("partition_dt is required")
  78. rows: list[dict[str, str]] = []
  79. seen_demand_ids: set[str] = set()
  80. for raw_name in demand_names:
  81. demand_name = str(raw_name).strip()
  82. if not demand_name:
  83. continue
  84. demand_id = build_gap_script_demand_id(
  85. strategy=strategy_value,
  86. demand_name=demand_name,
  87. partition_dt=partition_value,
  88. )
  89. if demand_id in seen_demand_ids:
  90. continue
  91. seen_demand_ids.add(demand_id)
  92. rows.append(
  93. {
  94. "strategy": strategy_value,
  95. "demand_id": demand_id,
  96. "demand_name": demand_name,
  97. "partition_dt": partition_value,
  98. }
  99. )
  100. return rows
  101. def insert_demand_rows(self, rows: list[dict[str, str]]) -> int:
  102. if not rows:
  103. return 0
  104. sql = f"""
  105. INSERT INTO {TABLE_NAME} (
  106. strategy,
  107. demand_id,
  108. demand_name,
  109. partition_dt
  110. )
  111. VALUES (%s, %s, %s, %s)
  112. """
  113. payload = [
  114. (
  115. row["strategy"],
  116. row["demand_id"],
  117. row["demand_name"],
  118. row["partition_dt"],
  119. )
  120. for row in rows
  121. ]
  122. with self.conn.cursor() as cursor:
  123. cursor.executemany(sql, payload)
  124. return len(rows)
  125. def persist_matched_demands(
  126. self,
  127. *,
  128. demand_names: list[str],
  129. strategy: str,
  130. partition_dt: str,
  131. ) -> dict[str, Any]:
  132. """当天已有数据则跳过;否则写入 MySQL 并返回待同步 ODPS 的行。"""
  133. strategy_value = strategy.strip()
  134. partition_value = partition_dt.strip()
  135. base_result = {
  136. "table": TABLE_NAME,
  137. "strategy": strategy_value,
  138. "partition_dt": partition_value,
  139. "input_count": len(demand_names),
  140. "skipped": False,
  141. "saved_count": 0,
  142. "rows": [],
  143. }
  144. if self.has_partition_data(strategy=strategy_value, partition_dt=partition_value):
  145. return {
  146. **base_result,
  147. "skipped": True,
  148. "skip_reason": "partition_data_exists",
  149. }
  150. rows = self.build_demand_rows(
  151. demand_names=demand_names,
  152. strategy=strategy_value,
  153. partition_dt=partition_value,
  154. )
  155. if not rows:
  156. return base_result
  157. saved_count = self.insert_demand_rows(rows)
  158. return {
  159. **base_result,
  160. "saved_count": saved_count,
  161. "rows": rows,
  162. }