template_store.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. from typing import Dict, List, Optional
  5. from fastapi import HTTPException
  6. from ..schemas import TemplateCreate, TemplateRecord, TemplateUpdate
  7. class TemplateStore:
  8. """Simple JSON-file based store for template definitions."""
  9. def __init__(self, data_path: Path):
  10. self.data_path = data_path
  11. self.data_path.parent.mkdir(parents=True, exist_ok=True)
  12. if not self.data_path.exists():
  13. self.data_path.write_text(json.dumps([], ensure_ascii=False, indent=2))
  14. self._cache: Dict[str, TemplateRecord] = {}
  15. self._load()
  16. def _load(self) -> None:
  17. raw = json.loads(self.data_path.read_text(encoding='utf-8'))
  18. self._cache = {item["id"]: TemplateRecord(**item) for item in raw}
  19. def _persist(self) -> None:
  20. # Use JSON mode so datetime fields are converted to ISO strings.
  21. serializable = [record.model_dump(mode="json") for record in self._cache.values()]
  22. self.data_path.write_text(
  23. json.dumps(serializable, ensure_ascii=False, indent=2),
  24. encoding='utf-8'
  25. )
  26. def list_templates(self, machine_model: str = None, device_type: str = None, template_type: str = None) -> List[TemplateRecord]:
  27. templates = list(self._cache.values())
  28. if machine_model:
  29. templates = [t for t in templates if t.machine_model == machine_model]
  30. if device_type:
  31. templates = [t for t in templates if t.device_type == device_type]
  32. if template_type:
  33. templates = [t for t in templates if t.template_type == template_type]
  34. return templates
  35. def get_template(self, template_id: str) -> TemplateRecord:
  36. try:
  37. record = self._cache[template_id]
  38. # 如果是复用模板,返回源模板的内容(但保留当前模板的元数据)
  39. if record.reusable_from:
  40. source = self._cache.get(record.reusable_from)
  41. if source:
  42. # 返回源模板,但保留当前模板的元数据
  43. merged = source.model_copy()
  44. merged.id = record.id
  45. merged.name = record.name
  46. merged.category = record.category
  47. merged.device_type = record.device_type
  48. merged.machine_model = record.machine_model
  49. merged.template_type = record.template_type
  50. merged.type_id = record.type_id
  51. merged.zone_ids = record.zone_ids
  52. merged.card_type = record.card_type
  53. merged.reusable_from = record.reusable_from
  54. merged.reuse_targets = record.reuse_targets
  55. merged.description = record.description
  56. merged.created_at = record.created_at
  57. merged.updated_at = record.updated_at
  58. return merged
  59. return record
  60. except KeyError as exc:
  61. raise HTTPException(status_code=404, detail="模板不存在") from exc
  62. def create_template(self, payload: TemplateCreate) -> TemplateRecord:
  63. record = TemplateRecord(**payload.model_dump())
  64. self._cache[record.id] = record
  65. self._persist()
  66. return record
  67. def update_template(self, template_id: str, payload: TemplateUpdate) -> TemplateRecord:
  68. record = self.get_template(template_id)
  69. update_data = payload.model_dump(exclude_none=True)
  70. updated = record.model_copy(update=update_data)
  71. from datetime import datetime
  72. updated.updated_at = datetime.utcnow()
  73. self._cache[template_id] = updated
  74. self._persist()
  75. return updated
  76. def delete_template(self, template_id: str) -> None:
  77. if template_id not in self._cache:
  78. raise HTTPException(status_code=404, detail="模板不存在")
  79. record = self._cache[template_id]
  80. # 如果该模板被其他模板复用,禁止删除
  81. if record.reuse_targets:
  82. raise HTTPException(status_code=400, detail=f"该模板被 {len(record.reuse_targets)} 个模板复用,无法删除")
  83. # 如果这是复用模板,需要从源模板的 reuse_targets 中移除
  84. if record.reusable_from and record.reusable_from in self._cache:
  85. source = self._cache[record.reusable_from]
  86. if template_id in source.reuse_targets:
  87. source.reuse_targets.remove(template_id)
  88. del self._cache[template_id]
  89. self._persist()
  90. def generate_redis_keys(self, record: TemplateRecord) -> dict:
  91. """根据模板信息自动生成 Redis 一级 key 列表"""
  92. if not record.type_id or not record.zone_ids:
  93. return {"second": record.redis_key_second or "", "first": []}
  94. # 二级 key
  95. card = record.card_type or "single_card"
  96. second_key = f"broadcast-template-jinja:{card}:{record.device_type}:{record.template_type}"
  97. # 模板类型到 card_type 和 recommend_type 的映射
  98. template_type_map = {
  99. "common": ("common", None),
  100. "time_popular": ("time_made_popularity", "1"),
  101. "time_custom": ("time_made_popularity", "2"),
  102. "time_season": ("time_season_storage", None), # 同时有 time_season_in
  103. "time_care": ("time_care", None),
  104. "time_environmental": ("time_environmental", None),
  105. "time_weather": ("time_weather", None),
  106. "time_health": ("time_health", None),
  107. }
  108. card_type_key, recommend_type = template_type_map.get(record.template_type, (record.template_type, None))
  109. first_keys = []
  110. for zone_id in record.zone_ids:
  111. if record.template_type == "common":
  112. key = f"broadcast-template-jinja:{record.type_id}:{zone_id}:common"
  113. first_keys.append(key)
  114. else:
  115. # 非 common 模板,需要为每个 zone_id 和 recommend_type 生成 key
  116. if recommend_type:
  117. key = f"broadcast-template-jinja:{record.type_id}:{zone_id}:{card_type_key}:{recommend_type}"
  118. first_keys.append(key)
  119. else:
  120. # 对于 season,同时生成 storage 和 in
  121. if record.template_type == "time_season":
  122. first_keys.append(f"broadcast-template-jinja:{record.type_id}:{zone_id}:time_season_storage:1")
  123. first_keys.append(f"broadcast-template-jinja:{record.type_id}:{zone_id}:time_season_in:1")
  124. else:
  125. key = f"broadcast-template-jinja:{record.type_id}:{zone_id}:{card_type_key}:1"
  126. first_keys.append(key)
  127. return {"second": second_key, "first": first_keys}
  128. def list_categories(self, machine_model: str = None) -> List[str]:
  129. templates = self._cache.values()
  130. if machine_model:
  131. templates = [t for t in templates if t.machine_model == machine_model]
  132. categories = {record.category for record in templates}
  133. return sorted(categories)
  134. def list_machine_models(self) -> List[str]:
  135. models = {record.machine_model for record in self._cache.values()}
  136. return sorted(models)
  137. def list_device_types(self, machine_model: str = None) -> List[str]:
  138. templates = self._cache.values()
  139. if machine_model:
  140. templates = [t for t in templates if t.machine_model == machine_model]
  141. device_types = {record.device_type for record in templates}
  142. return sorted(device_types)
  143. def list_template_types(self, machine_model: str = None, device_type: str = None) -> List[str]:
  144. templates = self._cache.values()
  145. if machine_model:
  146. templates = [t for t in templates if t.machine_model == machine_model]
  147. if device_type:
  148. templates = [t for t in templates if t.device_type == device_type]
  149. template_types = {record.template_type for record in templates}
  150. return sorted(template_types)
  151. def load_store(base_dir: Path) -> TemplateStore:
  152. data_path = base_dir / "data" / "templates.json"
  153. return TemplateStore(data_path)