account_material_strategy.py 4.3 KB

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