feat(content): 发布体系 大/小图 + 公开 + 园区端发布
- 迁移0018:content_items 加 card_mode/is_public/source/tenant_id
- ContentRepository:_to_dict/create/update 含新字段;list 加 public_only/tenant_id 过滤
- /opc/content 仅 published+公开;/opc/content/{id} 非公开→404
- 园区端 POST /park/api/content:发布人强制=园区名、source=carrier、tenant=本园、is_public 默认false(仅大屏)、card_mode默认small
- 园区端 GET /park/api/content:本园区内容(含未公开)供大屏
- schemas 补 card_mode/is_public
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
"""资讯发布体系:card_mode/is_public/source/tenant_id
|
||||
|
||||
Revision ID: 0018_content_publish
|
||||
Revises: 0017_content_detail
|
||||
Create Date: 2026-08-27
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0018_content_publish"
|
||||
down_revision = "0017_content_detail"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("content_items", sa.Column("card_mode", sa.String(), nullable=False, server_default="big"))
|
||||
op.add_column("content_items", sa.Column("is_public", sa.Boolean(), nullable=False, server_default="1"))
|
||||
op.add_column("content_items", sa.Column("source", sa.String(), nullable=False, server_default="operator"))
|
||||
op.add_column("content_items", sa.Column("tenant_id", sa.String(), nullable=False, server_default=""))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("content_items", "tenant_id")
|
||||
op.drop_column("content_items", "source")
|
||||
op.drop_column("content_items", "is_public")
|
||||
op.drop_column("content_items", "card_mode")
|
||||
@@ -92,8 +92,8 @@ async def opc_content(
|
||||
type: str = "news",
|
||||
db: Database = Depends(get_db),
|
||||
):
|
||||
"""C 端资讯:type ∈ policy(政策)/news(资讯)/skill(技能)/dynamic(动态),仅 published。公开浏览。"""
|
||||
return {"items": await db.content.list(ctype=type, status="published")}
|
||||
"""C 端资讯:type ∈ policy/news/skill/dynamic,仅 published + 公开(is_public)。公开浏览。"""
|
||||
return {"items": await db.content.list(ctype=type, status="published", public_only=True)}
|
||||
|
||||
|
||||
@router.get("/content/{content_id}", summary="资讯详情")
|
||||
@@ -102,7 +102,7 @@ async def opc_content_detail(
|
||||
db: Database = Depends(get_db),
|
||||
):
|
||||
item = await db.content.get(content_id)
|
||||
if item is None or item.get("status") != "published":
|
||||
if item is None or item.get("status") != "published" or not item.get("is_public"):
|
||||
raise HTTPException(status_code=404, detail="内容不存在")
|
||||
# 阅读数自增(每次详情浏览 +1)
|
||||
if item.get("status") == "published":
|
||||
|
||||
@@ -93,6 +93,8 @@ class ContentCreateRequest(BaseModel):
|
||||
link: str = "" # 外链
|
||||
publisher_name: str = "" # 发布人昵称(空则用操作者)
|
||||
publisher_avatar: str = "" # 发布人头像
|
||||
card_mode: str = "big" # big 大图 | small 小图(运营端设)
|
||||
is_public: bool = True # 是否公开
|
||||
status: str = "draft"
|
||||
|
||||
|
||||
@@ -113,6 +115,10 @@ class ContentUpdateRequest(BaseModel):
|
||||
link: str | None = None
|
||||
publisher_name: str | None = None
|
||||
publisher_avatar: str | None = None
|
||||
card_mode: str | None = None
|
||||
is_public: bool | None = None
|
||||
source: str | None = None
|
||||
tenant_id: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -307,6 +307,10 @@ class ContentItem(Base):
|
||||
publisher_name: Mapped[str] = mapped_column(String, default="") # 发布人昵称快照
|
||||
publisher_avatar: Mapped[str] = mapped_column(String, default="") # 发布人头像快照
|
||||
read_count: Mapped[int] = mapped_column(Integer, default=0) # 阅读数
|
||||
card_mode: Mapped[str] = mapped_column(String, default="big") # big 大图 | small 小图(运营端设)
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, default=True) # 是否公开(公开=C端,否则仅园区大屏)
|
||||
source: Mapped[str] = mapped_column(String, default="operator") # operator 运营端 | carrier 园区端
|
||||
tenant_id: Mapped[str] = mapped_column(String, default="") # 园区端发布所属园区
|
||||
status: Mapped[str] = mapped_column(String, default="draft") # draft/published/offline
|
||||
created_at: Mapped[str] = mapped_column(String, default="")
|
||||
updated_at: Mapped[str] = mapped_column(String, default="")
|
||||
|
||||
@@ -1270,7 +1270,7 @@ class ContentRepository:
|
||||
# 富字段白名单(images 为 list,落库转 json)
|
||||
_FIELDS = {"type", "title", "summary", "body", "status", "publisher_id",
|
||||
"cover", "images", "video", "video_cover", "link",
|
||||
"publisher_name", "publisher_avatar"}
|
||||
"publisher_name", "publisher_avatar", "card_mode", "is_public", "source", "tenant_id"}
|
||||
|
||||
def _to_dict(self, c: ContentItem) -> dict:
|
||||
try:
|
||||
@@ -1282,16 +1282,22 @@ class ContentRepository:
|
||||
"body": c.body, "publisher_id": c.publisher_id, "status": c.status,
|
||||
"cover": c.cover, "images": images, "video": c.video, "video_cover": c.video_cover,
|
||||
"link": c.link, "publisher_name": c.publisher_name, "publisher_avatar": c.publisher_avatar,
|
||||
"read_count": c.read_count,
|
||||
"read_count": c.read_count, "card_mode": c.card_mode, "is_public": bool(c.is_public),
|
||||
"source": c.source, "tenant_id": c.tenant_id,
|
||||
"created_at": c.created_at, "updated_at": c.updated_at,
|
||||
}
|
||||
|
||||
async def list(self, ctype: str | None = None, status: str | None = None) -> list[dict]:
|
||||
async def list(self, ctype: str | None = None, status: str | None = None,
|
||||
public_only: bool = False, tenant_id: str | None = None) -> list[dict]:
|
||||
q = select(ContentItem).order_by(ContentItem.created_at.desc())
|
||||
if ctype:
|
||||
q = q.where(ContentItem.type == ctype)
|
||||
if status:
|
||||
q = q.where(ContentItem.status == status)
|
||||
if public_only:
|
||||
q = q.where(ContentItem.is_public.is_(True))
|
||||
if tenant_id:
|
||||
q = q.where(ContentItem.tenant_id == tenant_id)
|
||||
return [self._to_dict(c) for c in await self.session.scalars(q)]
|
||||
|
||||
async def create(self, fields: dict) -> dict:
|
||||
@@ -1306,6 +1312,10 @@ class ContentRepository:
|
||||
link=fields.get("link", ""),
|
||||
publisher_name=fields.get("publisher_name", ""),
|
||||
publisher_avatar=fields.get("publisher_avatar", ""),
|
||||
card_mode=fields.get("card_mode", "big"),
|
||||
is_public=bool(fields.get("is_public", True)),
|
||||
source=fields.get("source", "operator"),
|
||||
tenant_id=fields.get("tenant_id", ""),
|
||||
status=fields.get("status", "draft"), created_at=now, updated_at=now)
|
||||
self.session.add(c)
|
||||
await self.session.commit()
|
||||
|
||||
@@ -1079,3 +1079,48 @@ async def carrier_company_compute(cid: str, body: CompanyComputeBody, user: dict
|
||||
logging.getLogger(__name__).warning("企业配额发放失败 %s: %s", m.get("username"), exc)
|
||||
return {"ok": True, "company": updated, "quota_add": body.quota_add}
|
||||
|
||||
|
||||
|
||||
# ==================== 园区端:发布资讯 + 大屏内容 ====================
|
||||
|
||||
class ParkContentBody(BaseModel):
|
||||
type: str = "news"
|
||||
title: str
|
||||
summary: str = ""
|
||||
body: str = ""
|
||||
cover: str = ""
|
||||
video: str = ""
|
||||
video_cover: str = ""
|
||||
link: str = ""
|
||||
card_mode: str = "small" # 园区默认小图;运营端可改大/小图
|
||||
|
||||
|
||||
@router.post("/api/content", summary="园区端发布资讯(发布人=园区名,默认不公开)")
|
||||
async def carrier_publish_content(
|
||||
body: ParkContentBody,
|
||||
request: Request,
|
||||
db=Depends(_carrier_db),
|
||||
user: dict = Depends(_carrier_user),
|
||||
):
|
||||
t = await _my_park(user)
|
||||
item = await db.content.create({
|
||||
"type": body.type, "title": body.title, "summary": body.summary, "body": body.body,
|
||||
"cover": body.cover, "video": body.video, "video_cover": body.video_cover, "link": body.link,
|
||||
"publisher_name": t["name"], "publisher_avatar": "", # 发布人强制=园区名,不可改
|
||||
"source": "carrier", "tenant_id": t["id"],
|
||||
"card_mode": body.card_mode or "small",
|
||||
"is_public": False, # 默认不公开:仅本园区大屏;由运营端切公开
|
||||
"status": "published",
|
||||
})
|
||||
await write_audit(db, action="content.publish", resource="content", resource_id=item["id"],
|
||||
detail=f"carrier {item['title']}", user=user, request=request)
|
||||
return {"ok": True, "content": item}
|
||||
|
||||
|
||||
@router.get("/api/content", summary="园区资讯列表(本园区,含未公开,供大屏)")
|
||||
async def carrier_content_list(
|
||||
db=Depends(_carrier_db),
|
||||
user: dict = Depends(_carrier_user),
|
||||
):
|
||||
t = await _my_park(user)
|
||||
return {"items": await db.content.list(tenant_id=t["id"])}
|
||||
|
||||
Reference in New Issue
Block a user