scene_spec.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. """腾讯广告"版位定投场景"标签查询 + 进程内缓存(P0 一账户 N 广告差异化基础设施)。
  2. 数据流(2026-06-09 用户确认):
  3. 主循环启动时 → get_wechat_position_tags(account_id)
  4. 缓存中:返回内存里的 dict
  5. 缓存过期/未填:调腾讯 /scene_spec_tags/get(type=WECHAT_POSITION) → 缓存 → 返回
  6. 用途:
  7. - 运行时校验 config.WECHAT_POSITION_TARGETED_PRESET 中的 ID 仍然合法(腾讯未弃用)
  8. - 给飞书审批表展示中文场景名(P0-C 扩展时)
  9. - 日志可读化(把 1024795 → "小程序激励式广告")
  10. API 参考:
  11. POST /scene_spec_tags/get
  12. 必填:account_id + type (enum,实测 WECHAT_POSITION 可用)
  13. 返回:data.list[{id, name, description, parent_id, targeting_name}]
  14. """
  15. import logging
  16. import time
  17. from typing import Optional
  18. from tools.ad_api import _get
  19. logger = logging.getLogger(__name__)
  20. # 缓存键 → "type 名"(如 'WECHAT_POSITION');TTL 1 小时(用户 2026-06-09 确认)
  21. _CACHE: dict = {}
  22. _TTL_SECONDS = 3600
  23. def _now() -> float:
  24. return time.time()
  25. def get_wechat_position_tags(
  26. account_id: int,
  27. force_refresh: bool = False,
  28. ) -> dict[int, str]:
  29. """查询 WECHAT_POSITION 全部场景 ID → 中文 description 映射(带 1h 缓存)。
  30. Args:
  31. account_id: 腾讯账户 ID(腾讯接口要求必填,但场景列表跟账户无关 — 任一账户的返回值通用)
  32. force_refresh: 强制刷新缓存
  33. Returns:
  34. {场景 ID(int): 中文名(str)} — 13 个映射(2026-06-09 实测)
  35. """
  36. cache_key = "WECHAT_POSITION"
  37. cached = _CACHE.get(cache_key)
  38. if (
  39. not force_refresh
  40. and cached is not None
  41. and (_now() - cached["ts"]) < _TTL_SECONDS
  42. ):
  43. return cached["data"]
  44. logger.info(
  45. "[scene_spec] cache miss/expired,调 /scene_spec_tags/get type=%s",
  46. cache_key,
  47. )
  48. resp = _get("/scene_spec_tags/get", {
  49. "account_id": account_id, "type": cache_key,
  50. })
  51. if resp.get("code") != 0:
  52. raise RuntimeError(
  53. f"scene_spec_tags/get 失败 code={resp.get('code')} "
  54. f"msg={resp.get('message_cn') or resp.get('message')}"
  55. )
  56. items = (resp.get("data") or {}).get("list") or []
  57. mapping = {int(it["id"]): str(it.get("description") or "") for it in items}
  58. _CACHE[cache_key] = {"data": mapping, "ts": _now()}
  59. logger.info(
  60. "[scene_spec] 缓存已更新 type=%s 共 %d 项",
  61. cache_key, len(mapping),
  62. )
  63. return mapping
  64. def validate_preset_ids(account_id: int, preset_ids: list[int]) -> dict:
  65. """校验给定 ID 列表是否都在腾讯当前合法场景集合内。
  66. 用于主循环启动时检查 config.WECHAT_POSITION_TARGETED_PRESET 仍有效。
  67. Returns:
  68. {"all_valid": bool, "invalid_ids": list[int], "mapping": {id: 中文}}
  69. """
  70. mapping = get_wechat_position_tags(account_id)
  71. invalid = [i for i in preset_ids if i not in mapping]
  72. return {
  73. "all_valid": not invalid,
  74. "invalid_ids": invalid,
  75. "mapping": {i: mapping.get(i, "<未知>") for i in preset_ids},
  76. }
  77. def describe_position_ids(account_id: int, ids: list[int]) -> str:
  78. """把场景 ID 列表转成 "公众号文章底部+中部+..." 的可读字符串(日志 / 审批表用)。"""
  79. if not ids:
  80. return "(无定投)"
  81. mapping = get_wechat_position_tags(account_id)
  82. return " + ".join(mapping.get(i, f"<ID {i}>") for i in ids)