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:
Pine
2026-08-27 14:35:32 +08:00
parent 7ec9b8fd8a
commit 71b1f03bca
6 changed files with 147 additions and 6 deletions
+20 -2
View File
@@ -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,
+14 -1
View File
@@ -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