service.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. """节日需求每日任务入口。"""
  2. from __future__ import annotations
  3. from datetime import date, datetime
  4. from typing import Any
  5. from app.festival_demand.config import load_festival_demand_config
  6. from app.festival_demand.demand_generation import generate_demands_from_matches
  7. from app.festival_demand.detection import detect_active_festivals
  8. from app.festival_demand.matching import (
  9. collect_matched_demand_names,
  10. match_demand_names_to_festivals,
  11. )
  12. from app.festival_demand.odps_demand_pool import fetch_demand_names
  13. from app.festival_demand.odps_writer import build_odps_row, sync_festival_demands_to_odps
  14. from app.festival_demand.repository import FestivalDemandRepository
  15. from app.festival_demand.types import FestivalDemandConfig
  16. from app.hot_content.config import load_flow_config
  17. from app.hot_content.timezone import SHANGHAI_TZ
  18. def _today_shanghai() -> date:
  19. return datetime.now(SHANGHAI_TZ).date()
  20. def print_festival_demand_summary(summary: dict[str, Any]) -> None:
  21. """将节日需求流程结果打印到控制台。"""
  22. print("=" * 60)
  23. print("节日需求任务结果")
  24. print("=" * 60)
  25. print(f"查询日期: {summary.get('query_date')}")
  26. print(f"ODPS 分区: {summary.get('partition_dt')}")
  27. print(f"需求池 strategy: {summary.get('demand_pool_strategy')}")
  28. if summary.get("skip_reason") in {
  29. "no_active_festivals",
  30. "skip_odps",
  31. "no_demand_names",
  32. "partition_data_exists",
  33. }:
  34. print(f"跳过原因: {summary.get('skip_reason')}")
  35. print("=" * 60)
  36. return
  37. festival_names = summary.get("festival_names") or []
  38. print(f"\n活跃节日 ({len(festival_names)}):")
  39. for name in festival_names:
  40. print(f" - {name}")
  41. print(f"\nODPS 需求词总数: {summary.get('demand_name_total', 0)}")
  42. print(f"匹配特征词数: {summary.get('matched_count', 0)}")
  43. matched_names = summary.get("matched_demand_names") or []
  44. if matched_names:
  45. print("\n匹配到的特征词:")
  46. for name in matched_names:
  47. print(f" * {name}")
  48. demands_by_type = summary.get("demands_by_type") or {}
  49. type_labels = {
  50. "year_festival": "方式1: 年份 + 节日",
  51. "festival": "方式2: 节日",
  52. "year_festival_feature": "方式3: 年份 + 节日 + 特征点",
  53. }
  54. generated_names = summary.get("generated_demand_names") or []
  55. print(f"\n生成需求总数: {summary.get('generated_demand_count', len(generated_names))}")
  56. for generation_type, label in type_labels.items():
  57. names = demands_by_type.get(generation_type) or []
  58. if not names:
  59. continue
  60. print(f"\n{label} ({len(names)}):")
  61. for name in names:
  62. print(f" + {name}")
  63. if not generated_names:
  64. print("\n(暂无生成需求)")
  65. mysql_save = summary.get("mysql_save") or {}
  66. if mysql_save.get("skipped"):
  67. print(
  68. f"\nMySQL/ODPS 跳过: reason={mysql_save.get('skip_reason')} "
  69. f"partition_dt={mysql_save.get('partition_dt')}"
  70. )
  71. elif mysql_save:
  72. print(
  73. f"\nMySQL 写入: table={mysql_save.get('table')} "
  74. f"saved={mysql_save.get('saved_count', 0)} "
  75. f"strategy={mysql_save.get('strategy')}"
  76. )
  77. odps_sync = summary.get("odps_sync") or {}
  78. if odps_sync:
  79. print(
  80. f"ODPS 写入: table={odps_sync.get('target_table')} "
  81. f"partition_dt={odps_sync.get('partition_dt')} "
  82. f"written={odps_sync.get('written_count', 0)}"
  83. )
  84. print("=" * 60)
  85. def _persist_generated_demands(
  86. *,
  87. demand_names: list[str],
  88. strategy: str,
  89. partition_dt: str,
  90. target_table: str,
  91. ) -> tuple[dict[str, Any], dict[str, Any]]:
  92. flow_config = load_flow_config()
  93. repository = FestivalDemandRepository(flow_config.mysql)
  94. try:
  95. mysql_save = repository.persist_generated_demands(
  96. demand_names=demand_names,
  97. strategy=strategy,
  98. partition_dt=partition_dt,
  99. )
  100. if mysql_save.get("skipped") or not mysql_save.get("rows"):
  101. return mysql_save, {}
  102. odps_rows = [
  103. build_odps_row(
  104. strategy=row["strategy"],
  105. demand_id=row["demand_id"],
  106. demand_name=row["demand_name"],
  107. )
  108. for row in mysql_save["rows"]
  109. ]
  110. odps_sync = sync_festival_demands_to_odps(
  111. rows=odps_rows,
  112. partition_dt=partition_dt,
  113. target_table=target_table,
  114. )
  115. return mysql_save, odps_sync
  116. finally:
  117. repository.close()
  118. def run_festival_demand_daily_job(
  119. config: FestivalDemandConfig,
  120. *,
  121. query_date: date | None = None,
  122. skip_odps: bool = False,
  123. ) -> dict[str, Any]:
  124. """执行节日需求流程:检测活跃节日 → 拉取 ODPS 需求词 → LLM 批量匹配。"""
  125. target_date = query_date or _today_shanghai()
  126. partition_dt = target_date.strftime("%Y%m%d")
  127. detection = detect_active_festivals(target_date, config)
  128. festivals = detection.get("festivals") or []
  129. summary: dict[str, Any] = {
  130. "query_date": target_date.isoformat(),
  131. "partition_dt": partition_dt,
  132. "demand_pool_strategy": config.demand_pool_strategy,
  133. "output_strategy": config.output_strategy,
  134. "demand_pool_source_table": config.demand_pool_source_table,
  135. "festival_count": len(festivals),
  136. "festival_names": detection.get("festival_names") or [],
  137. "festivals": festivals,
  138. "demand_name_total": 0,
  139. "match_batch_size": config.match_batch_size,
  140. "matched_count": 0,
  141. "matched_demand_names": [],
  142. "matches": [],
  143. "generated_demand_count": 0,
  144. "generated_demand_names": [],
  145. "generated_demands": [],
  146. "demands_by_type": {
  147. "year_festival": [],
  148. "festival": [],
  149. "year_festival_feature": [],
  150. },
  151. "mysql_save": {},
  152. "odps_sync": {},
  153. }
  154. if not festivals:
  155. summary["skip_reason"] = "no_active_festivals"
  156. return summary
  157. if skip_odps:
  158. summary["skip_reason"] = "skip_odps"
  159. return summary
  160. flow_config = load_flow_config()
  161. repository = FestivalDemandRepository(flow_config.mysql)
  162. try:
  163. if repository.has_partition_data(
  164. strategy=config.output_strategy,
  165. partition_dt=partition_dt,
  166. ):
  167. summary["skip_reason"] = "partition_data_exists"
  168. summary["mysql_save"] = {
  169. "table": "festival_demand_pool_di",
  170. "strategy": config.output_strategy,
  171. "partition_dt": partition_dt,
  172. "skipped": True,
  173. "skip_reason": "partition_data_exists",
  174. }
  175. return summary
  176. finally:
  177. repository.close()
  178. demand_names = fetch_demand_names(
  179. partition_dt=partition_dt,
  180. strategy=config.demand_pool_strategy,
  181. source_table=config.demand_pool_source_table,
  182. )
  183. summary["demand_name_total"] = len(demand_names)
  184. if not demand_names:
  185. summary["skip_reason"] = "no_demand_names"
  186. return summary
  187. print(
  188. f"festival demand: start matching "
  189. f"festivals={len(festivals)} demand_names={len(demand_names)} "
  190. f"batch_size={config.match_batch_size}",
  191. flush=True,
  192. )
  193. matches = match_demand_names_to_festivals(
  194. demand_names=demand_names,
  195. festivals=festivals,
  196. config=config,
  197. )
  198. matched_demand_names = collect_matched_demand_names(matches)
  199. generated = generate_demands_from_matches(matches, query_date=target_date)
  200. summary["matched_count"] = len(matched_demand_names)
  201. summary["matched_demand_names"] = matched_demand_names
  202. summary["matches"] = matches
  203. summary["generated_demand_count"] = generated["generated_demand_count"]
  204. summary["generated_demand_names"] = generated["generated_demand_names"]
  205. summary["generated_demands"] = generated["generated_demands"]
  206. summary["demands_by_type"] = generated["demands_by_type"]
  207. if generated["generated_demand_names"]:
  208. print("festival demand: persisting generated demands", flush=True)
  209. mysql_save, odps_sync = _persist_generated_demands(
  210. demand_names=generated["generated_demand_names"],
  211. strategy=config.output_strategy,
  212. partition_dt=partition_dt,
  213. target_table=config.demand_pool_source_table,
  214. )
  215. summary["mysql_save"] = mysql_save
  216. summary["odps_sync"] = odps_sync
  217. if mysql_save.get("skipped"):
  218. summary["skip_reason"] = mysql_save.get("skip_reason")
  219. return summary
  220. def test_festivals_for_date(
  221. query_date: date,
  222. *,
  223. skip_odps: bool = False,
  224. ) -> dict[str, Any]:
  225. """测试指定日期的节日需求流程。"""
  226. config = load_festival_demand_config()
  227. return run_festival_demand_daily_job(
  228. config,
  229. query_date=query_date,
  230. skip_odps=skip_odps,
  231. )