"""模板管理 API — Phase 8 模板编排引擎. RESTful CRUD for EditTemplate: - GET /api/v1/edit-templates 列表(分页 + 类型筛选) - GET /api/v1/edit-templates/{id} 详情 - POST /api/v1/edit-templates 创建(管理员) - PUT /api/v1/edit-templates/{id} 更新 - DELETE /api/v1/edit-templates/{id} 删除(软删除 → inactive) 业务逻辑委托给 EditTemplateService 服务层。 """ from __future__ import annotations import logging from datetime import datetime from typing import Any, List, Optional from app.auth import AuthenticatedUser, get_current_user from app.dependencies import get_db_session from app.services import EditTemplateService from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi.responses import Response from pydantic import BaseModel, Field from sqlalchemy.orm import Session from packages.domain.config_schemas import normalize_template_config from packages.domain.edit_template import EditTemplate, EditTemplateStatus logger = logging.getLogger(__name__) router = APIRouter() # ── Pydantic Schemas ───────────────────────────────────────────────────────── class EditTemplateCreateRequest(BaseModel): """创建模板请求体""" name: str = Field(..., min_length=1, max_length=200, description="模板名称") description: str = Field(default="", max_length=2000, description="模板描述") template_type: str = Field(default="default", max_length=50, description="模板类型") config: dict[str, Any] = Field(default_factory=dict, description="模板配置 (JSON)") preview_url: str = Field(default="", max_length=500, description="预览地址") sort_weight: int = Field(default=0, ge=0, le=9999, description="排序权重") class EditTemplateUpdateRequest(BaseModel): """更新模板请求体""" name: Optional[str] = Field(default=None, min_length=1, max_length=200, description="模板名称") description: Optional[str] = Field(default=None, max_length=2000, description="模板描述") template_type: Optional[str] = Field(default=None, max_length=50, description="模板类型") config: Optional[dict[str, Any]] = Field(default=None, description="模板配置 (JSON)") preview_url: Optional[str] = Field(default=None, max_length=500, description="预览地址") sort_weight: Optional[int] = Field(default=None, ge=0, le=9999, description="排序权重") status: Optional[str] = Field(default=None, description="状态: active / inactive") class EditTemplateResponse(BaseModel): """模板响应体""" id: str name: str description: str template_type: str config: dict[str, Any] preview_url: str sort_weight: int status: str created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class EditTemplateListResponse(BaseModel): """模板列表响应体""" items: List[EditTemplateResponse] total: int page: int page_size: int # ── Helpers ─────────────────────────────────────────────────────────────────── def _require_admin(current_user: AuthenticatedUser) -> None: """校验当前用户是否为管理员,非管理员返回 403""" if not getattr(current_user.user, "is_admin", False): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可执行此操作", ) def _to_response(t: EditTemplate) -> EditTemplateResponse: return EditTemplateResponse( id=t.id, name=t.name, description=t.description, template_type=t.template_type, config=t.config, preview_url=t.preview_url, sort_weight=t.sort_weight, status=t.status.value if hasattr(t.status, "value") else t.status, created_at=t.created_at, updated_at=t.updated_at, ) # ── Routes ──────────────────────────────────────────────────────────────────── @router.get("", response_model=EditTemplateListResponse) def list_templates( page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量"), template_type: Optional[str] = Query(default=None, description="按类型筛选"), status_filter: Optional[str] = Query( default=None, alias="status", description="按状态筛选: active / inactive", ), db: Session = Depends(get_db_session), current_user: AuthenticatedUser = Depends(get_current_user), ) -> EditTemplateListResponse: """获取模板列表(支持分页、按类型/状态筛选)""" svc = EditTemplateService(db) # 解析状态筛选 status_enum: Optional[EditTemplateStatus] = None if status_filter: try: status_enum = EditTemplateStatus(status_filter) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"无效的状态值: {status_filter},可选值: active, inactive", ) skip = (page - 1) * page_size templates = svc.list_templates( template_type=template_type, status=status_enum, skip=skip, limit=page_size, ) total = svc.count_templates( template_type=template_type, status=status_enum, ) return EditTemplateListResponse( items=[_to_response(t) for t in templates], total=total, page=page, page_size=page_size, ) @router.get("/{template_id}", response_model=EditTemplateResponse) def get_template( template_id: str, db: Session = Depends(get_db_session), current_user: AuthenticatedUser = Depends(get_current_user), ) -> EditTemplateResponse: """获取单个模板详情""" svc = EditTemplateService(db) try: template = svc.get_template_or_raise(template_id) except ValueError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc), ) return _to_response(template) @router.post("", response_model=EditTemplateResponse, status_code=status.HTTP_201_CREATED) def create_template( body: EditTemplateCreateRequest, db: Session = Depends(get_db_session), current_user: AuthenticatedUser = Depends(get_current_user), ) -> EditTemplateResponse: """创建模板(管理员)""" _require_admin(current_user) svc = EditTemplateService(db) # 标准化 config,填充 cover/title/subtitle/bgm 默认值 normalized_config = normalize_template_config(body.config) try: created = svc.create_template( name=body.name, description=body.description, template_type=body.template_type, config=normalized_config, preview_url=body.preview_url, sort_weight=body.sort_weight, ) except ValueError as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc), ) logger.info("创建模板: id=%s name=%s by user=%s", created.id, created.name, current_user.user.id) return _to_response(created) @router.put("/{template_id}", response_model=EditTemplateResponse) def update_template( template_id: str, body: EditTemplateUpdateRequest, db: Session = Depends(get_db_session), current_user: AuthenticatedUser = Depends(get_current_user), ) -> EditTemplateResponse: """更新模板""" _require_admin(current_user) svc = EditTemplateService(db) # 解析状态 status_enum: Optional[EditTemplateStatus] = None if body.status is not None: try: status_enum = EditTemplateStatus(body.status) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"无效的状态值: {body.status},可选值: active, inactive", ) # 标准化 config(如果提供了) config_to_update = normalize_template_config(body.config) if body.config is not None else None try: result = svc.update_template( template_id, name=body.name, description=body.description, template_type=body.template_type, config=config_to_update, preview_url=body.preview_url, sort_weight=body.sort_weight, status=status_enum, ) except ValueError as exc: err_msg = str(exc) if "不存在" in err_msg: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=err_msg, ) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=err_msg, ) logger.info("更新模板: id=%s by user=%s", template_id, current_user.user.id) return _to_response(result) @router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None) def delete_template( template_id: str, db: Session = Depends(get_db_session), current_user: AuthenticatedUser = Depends(get_current_user), ) -> Response: """删除模板(软删除 → 设为 inactive)""" _require_admin(current_user) svc = EditTemplateService(db) try: svc.deactivate_template(template_id) except ValueError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc), ) logger.info("删除模板(软删除): id=%s by user=%s", template_id, current_user.user.id) return Response(status_code=204)