feat(content): 资讯富编辑(封面/图集/视频/外链/发布人) + 媒体上传
- 迁移0016:content_items 加 cover/images_json/video/video_cover/link/publisher_name/publisher_avatar - ContentRepository:_to_dict/create/update 支持富字段;images(list)↔images_json;update 白名单含 publisher_id - schemas:ContentCreate/UpdateRequest 加富字段 - rbac_operator:POST /content 注入发布人(actor 昵称/头像快照);新增 POST /admin/upload(媒体上传) - app/services/media_upload.py:统一上传(serverdata/uploads + abs_url),与 /api/upload 同契约 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, Response, UploadFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..dependencies import get_db
|
||||
@@ -209,12 +209,30 @@ async def create_content(
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:content.manage")),
|
||||
):
|
||||
item = await db.content.create(req.model_dump(exclude_none=True))
|
||||
payload = req.model_dump(exclude_none=True)
|
||||
# 发布人:未显式指定时用当前操作者(昵称/头像快照)
|
||||
payload.setdefault("publisher_id", actor.get("id", ""))
|
||||
if not payload.get("publisher_name"):
|
||||
u = await db.users.get_by_id(actor.get("id", ""))
|
||||
payload["publisher_name"] = (u or {}).get("nickname") or actor.get("username") or "平台"
|
||||
payload["publisher_avatar"] = (u or {}).get("avatar", "") or ""
|
||||
item = await db.content.create(payload)
|
||||
await write_audit(db, action="content.create", resource="content", resource_id=item["id"],
|
||||
detail=item["title"], user=actor, request=request)
|
||||
return item
|
||||
|
||||
|
||||
@router.post("/upload", summary="上传媒体(图片/pdf/doc/docx)")
|
||||
async def admin_upload(
|
||||
file: UploadFile = File(...),
|
||||
actor: dict = Depends(require_permission("action:content.manage")),
|
||||
):
|
||||
"""运营端上传资讯封面 / 图集 / 视频封面等:存 uploads 目录,返回 {ok, url}。"""
|
||||
from ...services import media_upload
|
||||
url = await media_upload.save_media(file)
|
||||
return {"ok": True, "url": url}
|
||||
|
||||
|
||||
@router.post("/content/{content_id}/status", summary="发布/下架内容")
|
||||
async def set_content_status(
|
||||
content_id: str,
|
||||
|
||||
@@ -86,10 +86,16 @@ class ContentCreateRequest(BaseModel):
|
||||
title: str
|
||||
summary: str = ""
|
||||
body: str = ""
|
||||
cover: str = "" # 封面图
|
||||
images: list[str] = [] # 图集
|
||||
video: str = "" # 视频 URL
|
||||
video_cover: str = "" # 视频封面
|
||||
link: str = "" # 外链
|
||||
publisher_name: str = "" # 发布人昵称(空则用操作者)
|
||||
publisher_avatar: str = "" # 发布人头像
|
||||
status: str = "draft"
|
||||
|
||||
|
||||
|
||||
class ContentStatusRequest(BaseModel):
|
||||
status: str # published/offline/draft
|
||||
|
||||
@@ -100,6 +106,13 @@ class ContentUpdateRequest(BaseModel):
|
||||
title: str | None = None
|
||||
summary: str | None = None
|
||||
body: str | None = None
|
||||
cover: str | None = None
|
||||
images: list[str] | None = None
|
||||
video: str | None = None
|
||||
video_cover: str | None = None
|
||||
link: str | None = None
|
||||
publisher_name: str | None = None
|
||||
publisher_avatar: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user