a6e809600e
CI/CD Pipeline / Frontend Lint (push) Failing after 97h55m54s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 97h55m54s
Deploy / Deploy Staging (push) Failing after 97h55m45s
Deploy / Staging E2E Tests (push) Failing after 97h52m0s
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
280 lines
9.8 KiB
Python
280 lines
9.8 KiB
Python
"""模板管理 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)
|