account_material_strategy.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. """Account-level creative material source strategy.
  2. The default remains historical recall. AI generation and external recall are
  3. enabled only for accounts explicitly configured for those module B branches.
  4. """
  5. from __future__ import annotations
  6. import logging
  7. from dataclasses import dataclass
  8. logger = logging.getLogger(__name__)
  9. MATERIAL_SOURCE_HISTORY = "history"
  10. MATERIAL_SOURCE_AI_GENERATED = "ai_generated"
  11. MATERIAL_SOURCE_EXTERNAL_RECALL = "external_recall"
  12. _AI_SOURCE_VALUES = {
  13. "ai",
  14. "ai_generated",
  15. "ai生成",
  16. "ai生成素材",
  17. "生成素材",
  18. "新生成素材",
  19. }
  20. _HISTORY_SOURCE_VALUES = {
  21. "",
  22. "history",
  23. "历史",
  24. "历史素材",
  25. "历史投放素材",
  26. "历史已投素材",
  27. }
  28. _EXTERNAL_SOURCE_VALUES = {
  29. "external",
  30. "external_recall",
  31. "外部",
  32. "外部素材",
  33. "外部素材召回",
  34. "外部合作素材",
  35. }
  36. _TRUE_VALUES = {"1", "true", "yes", "y", "是", "允许", "回退"}
  37. @dataclass(frozen=True)
  38. class AccountMaterialStrategy:
  39. account_id: int
  40. material_source: str = MATERIAL_SOURCE_HISTORY
  41. ai_fallback_to_history: bool = False
  42. @property
  43. def use_ai_generated(self) -> bool:
  44. return self.material_source == MATERIAL_SOURCE_AI_GENERATED
  45. @property
  46. def use_external_recall(self) -> bool:
  47. return self.material_source == MATERIAL_SOURCE_EXTERNAL_RECALL
  48. def normalize_material_source(raw: object) -> str:
  49. text = str(raw or "").strip().lower()
  50. if text in _AI_SOURCE_VALUES:
  51. return MATERIAL_SOURCE_AI_GENERATED
  52. if text in _HISTORY_SOURCE_VALUES:
  53. return MATERIAL_SOURCE_HISTORY
  54. if text in _EXTERNAL_SOURCE_VALUES:
  55. return MATERIAL_SOURCE_EXTERNAL_RECALL
  56. raise ValueError(f"未知素材来源:{raw}")
  57. def parse_bool_flag(raw: object) -> bool:
  58. return str(raw or "").strip().lower() in _TRUE_VALUES
  59. def ensure_account_material_strategy_columns() -> None:
  60. """Add strategy columns when deploying to an older DB schema."""
  61. from db.connection import get_connection
  62. conn = get_connection()
  63. try:
  64. with conn.cursor() as cur:
  65. cur.execute(
  66. """
  67. SELECT COLUMN_NAME
  68. FROM information_schema.COLUMNS
  69. WHERE TABLE_SCHEMA = DATABASE()
  70. AND TABLE_NAME = 'ad_creation_account_config'
  71. AND COLUMN_NAME IN ('material_source', 'ai_fallback_to_history')
  72. """
  73. )
  74. existing = {row["COLUMN_NAME"] for row in (cur.fetchall() or [])}
  75. if "material_source" not in existing:
  76. cur.execute(
  77. """
  78. ALTER TABLE ad_creation_account_config
  79. ADD COLUMN material_source VARCHAR(50) NOT NULL DEFAULT 'history'
  80. COMMENT '创意素材来源:history/ai_generated/external_recall'
  81. AFTER feedback_key
  82. """
  83. )
  84. if "ai_fallback_to_history" not in existing:
  85. cur.execute(
  86. """
  87. ALTER TABLE ad_creation_account_config
  88. ADD COLUMN ai_fallback_to_history BOOLEAN NOT NULL DEFAULT FALSE
  89. COMMENT 'AI生成失败时是否回退历史素材'
  90. AFTER material_source
  91. """
  92. )
  93. conn.commit()
  94. finally:
  95. conn.close()
  96. def load_account_material_strategy(account_id: int) -> AccountMaterialStrategy:
  97. """Load configured material strategy. Missing config defaults to history."""
  98. from db.connection import get_connection
  99. try:
  100. ensure_account_material_strategy_columns()
  101. except Exception as e:
  102. logger.warning(
  103. "[material_strategy] ensure columns failed account=%d, fallback history: %s",
  104. account_id, e,
  105. )
  106. return AccountMaterialStrategy(account_id=account_id)
  107. conn = get_connection()
  108. try:
  109. with conn.cursor() as cur:
  110. cur.execute(
  111. """
  112. SELECT material_source, ai_fallback_to_history
  113. FROM ad_creation_account_config
  114. WHERE account_id=%s
  115. LIMIT 1
  116. """,
  117. (account_id,),
  118. )
  119. row = cur.fetchone() or {}
  120. finally:
  121. conn.close()
  122. try:
  123. source = normalize_material_source(row.get("material_source"))
  124. except ValueError:
  125. logger.warning(
  126. "[material_strategy] account=%d material_source=%r 非法,按 history 处理",
  127. account_id, row.get("material_source"),
  128. )
  129. source = MATERIAL_SOURCE_HISTORY
  130. return AccountMaterialStrategy(
  131. account_id=account_id,
  132. material_source=source,
  133. ai_fallback_to_history=bool(row.get("ai_fallback_to_history")),
  134. )