feat: implement template service with builtin and custom templates

- BUILTIN_TEMPLATES: 4 presets (正方形/纵向/横向长方形/左胸小号)
- load_custom_templates(): reads templates.json, skips corrupt entries
  individually so one bad record never blocks the rest
- get_all_templates(): builtin first, then custom
- add_template(): upsert by name, forces type="custom"
- rename_template() / delete_template(): raise ValueError if not found
- _save_custom_templates(): auto-creates config/ dir, logs on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:01:40 +08:00
co-authored by Claude Sonnet 4.6
parent 2d9121e3e0
commit 519bb9a6d2
2 changed files with 201 additions and 12 deletions
+191 -2
View File
@@ -1,2 +1,191 @@
def load_templates(): import json
return [] import logging
from typing import List, Optional
from core.models import Template
logger = logging.getLogger(__name__)
_TEMPLATES_FILENAME = "templates.json"
# ---------------------------------------------------------------------------
# 内置模板(代码定义,不可删除/修改)
# ---------------------------------------------------------------------------
BUILTIN_TEMPLATES: List[Template] = [
Template(
name="正方形模板",
x_ratio=0.25, y_ratio=0.25,
width_ratio=0.50, height_ratio=0.50,
rotation=0.0, type="builtin",
),
Template(
name="纵向长方形模板",
x_ratio=0.30, y_ratio=0.15,
width_ratio=0.40, height_ratio=0.55,
rotation=0.0, type="builtin",
),
Template(
name="横向长方形模板",
x_ratio=0.15, y_ratio=0.30,
width_ratio=0.70, height_ratio=0.40,
rotation=0.0, type="builtin",
),
Template(
name="左胸小号模板",
x_ratio=0.35, y_ratio=0.28,
width_ratio=0.12, height_ratio=0.12,
rotation=0.0, type="builtin",
),
]
# ---------------------------------------------------------------------------
# 内部辅助
# ---------------------------------------------------------------------------
def _templates_file():
from services.file_service import get_config_path
return get_config_path(_TEMPLATES_FILENAME)
def _validate_entry(data) -> Optional[Template]:
"""Parse one raw dict into a Template; return None and log on any error."""
try:
name = str(data["name"]).strip()
if not name:
raise ValueError("name is empty")
w = float(data["width_ratio"])
h = float(data["height_ratio"])
if w <= 0 or h <= 0:
raise ValueError("width_ratio and height_ratio must be > 0")
return Template(
name=name,
x_ratio=float(data.get("x_ratio", 0.0)),
y_ratio=float(data.get("y_ratio", 0.0)),
width_ratio=w,
height_ratio=h,
rotation=float(data.get("rotation", 0.0)),
type="custom",
)
except (KeyError, ValueError, TypeError) as exc:
logger.warning("Skipping invalid template entry (%s): %s", exc, data)
return None
def _save_custom_templates(templates: List[Template]):
"""Write custom templates to JSON. Logs error on failure, does not raise."""
path = _templates_file()
try:
path.parent.mkdir(exist_ok=True)
payload = {
"templates": [
{
"name": t.name,
"x_ratio": t.x_ratio,
"y_ratio": t.y_ratio,
"width_ratio": t.width_ratio,
"height_ratio": t.height_ratio,
"rotation": t.rotation,
}
for t in templates
]
}
with open(str(path), "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
logger.info("Saved %d custom template(s) to %s", len(templates), path)
except OSError as exc:
logger.error("Failed to save templates: %s", exc)
# ---------------------------------------------------------------------------
# 公开 API
# ---------------------------------------------------------------------------
def get_builtin_templates() -> List[Template]:
"""Return a copy of the built-in template list."""
return list(BUILTIN_TEMPLATES)
def load_custom_templates() -> List[Template]:
"""Load user-defined templates from JSON.
Returns [] on missing file. Skips corrupt individual entries so a
single bad record never blocks the rest.
"""
path = _templates_file()
if not path.exists():
return []
try:
with open(str(path), encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError) as exc:
logger.warning("Cannot read templates.json (%s), ignoring.", exc)
return []
if not isinstance(data, dict) or "templates" not in data:
logger.warning("templates.json has unexpected structure, ignoring.")
return []
results = []
for entry in data["templates"]:
t = _validate_entry(entry)
if t is not None:
results.append(t)
logger.info("Loaded %d custom template(s) from %s", len(results), path)
return results
def get_all_templates() -> List[Template]:
"""Return builtin templates followed by custom templates."""
return get_builtin_templates() + load_custom_templates()
def add_template(template: Template):
"""Add or overwrite a custom template, then persist.
If a custom template with the same name already exists it is replaced.
Built-in templates cannot be added via this function.
"""
template.type = "custom"
customs = load_custom_templates()
customs = [t for t in customs if t.name != template.name]
customs.append(template)
_save_custom_templates(customs)
logger.info("Template added: %s", template.name)
def rename_template(old_name: str, new_name: str):
"""Rename a custom template and persist.
Raises ValueError if *old_name* is not found or *new_name* is empty.
"""
new_name = new_name.strip()
if not new_name:
raise ValueError("New template name cannot be empty")
customs = load_custom_templates()
found = False
for t in customs:
if t.name == old_name:
t.name = new_name
found = True
break
if not found:
raise ValueError("Custom template not found: {}".format(old_name))
_save_custom_templates(customs)
logger.info("Template renamed: %s -> %s", old_name, new_name)
def delete_template(name: str):
"""Delete a custom template by name and persist.
Raises ValueError if *name* is not found among custom templates.
"""
customs = load_custom_templates()
filtered = [t for t in customs if t.name != name]
if len(filtered) == len(customs):
raise ValueError("Custom template not found: {}".format(name))
_save_custom_templates(filtered)
logger.info("Template deleted: %s", name)
+10 -10
View File
@@ -185,19 +185,19 @@
任务: 任务:
- [ ] 先读取 `src/services/template_service.py` 现有内容 - [x] 先读取 `src/services/template_service.py` 现有内容
- [ ] 完善 `src/services/template_service.py` - [x] 完善 `src/services/template_service.py`
- [ ] 定义内置模板 - [x] 定义内置模板
- [ ] 支持读取自定义模板 JSON - [x] 支持读取自定义模板 JSON
- [ ] 支持保存自定义模板 JSON - [x] 支持保存自定义模板 JSON
- [ ] 支持新增、重命名、删除自定义模板 - [x] 支持新增、重命名、删除自定义模板
- [ ] 校验模板字段 - [x] 校验模板字段
验收: 验收:
- [ ] 内置模板和自定义模板可区分 - [x] 内置模板和自定义模板可区分
- [ ] 单个模板损坏不影响全部模板加载 - [x] 单个模板损坏不影响全部模板加载
- [ ] UI 不直接操作模板 JSON 文件 - [x] UI 不直接操作模板 JSON 文件
## 5. 图片合成核心 ## 5. 图片合成核心