feat(content): 资讯详情 阅读数 + 留言
- 迁移0017:content_items.read_count + content_comments 表
- ContentRepository:_to_dict 加 read_count;incr_read_count/content_comments/add_content_comment
- rbac_opc:GET /opc/content/{id} 自增阅读数;GET/POST /opc/content/{id}/comments(发布需 opc_member,带昵称/头像)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
"""content read_count + content_comments
|
||||
|
||||
Revision ID: 0017_content_detail
|
||||
Revises: 0016_content_media
|
||||
Create Date: 2026-08-27
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0017_content_detail"
|
||||
down_revision = "0016_content_media"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("content_items", sa.Column("read_count", sa.Integer(), nullable=False, server_default="0"))
|
||||
op.create_table(
|
||||
"content_comments",
|
||||
sa.Column("id", sa.String(), primary_key=True),
|
||||
sa.Column("content_id", sa.String(), default="", index=True),
|
||||
sa.Column("user_id", sa.String(), default=""),
|
||||
sa.Column("username", sa.String(), default=""),
|
||||
sa.Column("nickname", sa.String(), default=""),
|
||||
sa.Column("avatar", sa.String(), default=""),
|
||||
sa.Column("content", sa.Text(), default=""),
|
||||
sa.Column("created_at", sa.String(), default=""),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("content_comments")
|
||||
op.drop_column("content_items", "read_count")
|
||||
@@ -91,9 +91,8 @@ async def opc_policy(
|
||||
async def opc_content(
|
||||
type: str = "news",
|
||||
db: Database = Depends(get_db),
|
||||
_u: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
"""C 端资讯:type ∈ policy(政策)/news(资讯)/skill(技能)/dynamic(动态),仅 published。"""
|
||||
"""C 端资讯:type ∈ policy(政策)/news(资讯)/skill(技能)/dynamic(动态),仅 published。公开浏览。"""
|
||||
return {"items": await db.content.list(ctype=type, status="published")}
|
||||
|
||||
|
||||
@@ -101,14 +100,50 @@ async def opc_content(
|
||||
async def opc_content_detail(
|
||||
content_id: str,
|
||||
db: Database = Depends(get_db),
|
||||
_u: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
item = await db.content.get(content_id)
|
||||
if item is None or item.get("status") != "published":
|
||||
raise HTTPException(status_code=404, detail="内容不存在")
|
||||
# 阅读数自增(每次详情浏览 +1)
|
||||
if item.get("status") == "published":
|
||||
await db.content.incr_read_count(content_id)
|
||||
item["read_count"] = (item.get("read_count") or 0) + 1
|
||||
return item
|
||||
|
||||
|
||||
@router.get("/content/{content_id}/comments", summary="资讯留言列表")
|
||||
async def opc_content_comments(
|
||||
content_id: str,
|
||||
db: Database = Depends(get_db),
|
||||
):
|
||||
return {"items": await db.content.content_comments(content_id)}
|
||||
|
||||
|
||||
class ContentCommentBody(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
@router.post("/content/{content_id}/comments", summary="发表留言")
|
||||
async def opc_content_add_comment(
|
||||
content_id: str,
|
||||
body: ContentCommentBody,
|
||||
db: Database = Depends(get_db),
|
||||
user: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
if not body.content.strip():
|
||||
raise HTTPException(status_code=400, detail="留言不能为空")
|
||||
item = await db.content.get(content_id)
|
||||
if item is None or item.get("status") != "published":
|
||||
raise HTTPException(status_code=404, detail="内容不存在")
|
||||
# 取当前用户资料(昵称/头像)用于留言展示
|
||||
me = await db.users.get_by_id(user["id"]) or {}
|
||||
actor = {"id": user["id"], "username": user.get("username", ""),
|
||||
"nickname": (me.get("nickname") or user.get("username", "")),
|
||||
"avatar": me.get("avatar", "")}
|
||||
comment = await db.content.add_content_comment(content_id, actor, body.content.strip())
|
||||
return {"ok": True, "comment": comment}
|
||||
|
||||
|
||||
@router.get("/finance", summary="财务流水")
|
||||
async def opc_finance(
|
||||
db: Database = Depends(get_db),
|
||||
|
||||
@@ -306,11 +306,27 @@ class ContentItem(Base):
|
||||
link: Mapped[str] = mapped_column(String, default="") # 外链
|
||||
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) # 阅读数
|
||||
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="")
|
||||
|
||||
|
||||
class ContentComment(Base):
|
||||
"""资讯留言。"""
|
||||
|
||||
__tablename__ = "content_comments"
|
||||
|
||||
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||
content_id: Mapped[str] = mapped_column(String, default="", index=True)
|
||||
user_id: Mapped[str] = mapped_column(String, default="")
|
||||
username: Mapped[str] = mapped_column(String, default="")
|
||||
nickname: Mapped[str] = mapped_column(String, default="")
|
||||
avatar: Mapped[str] = mapped_column(String, default="")
|
||||
content: Mapped[str] = mapped_column(Text, default="")
|
||||
created_at: Mapped[str] = mapped_column(String, default="")
|
||||
|
||||
|
||||
class SystemConfig(Base):
|
||||
"""平台系统配置(键值)。"""
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from .models import (
|
||||
AuditLog,
|
||||
Bid,
|
||||
ContentItem,
|
||||
ContentComment,
|
||||
Contract,
|
||||
Dispute,
|
||||
Escrow,
|
||||
@@ -1281,6 +1282,7 @@ 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,
|
||||
"created_at": c.created_at, "updated_at": c.updated_at,
|
||||
}
|
||||
|
||||
@@ -1346,6 +1348,38 @@ class ContentRepository:
|
||||
await self.session.commit()
|
||||
return True
|
||||
|
||||
# ---- 阅读数 / 留言 ----
|
||||
|
||||
async def incr_read_count(self, content_id: str) -> None:
|
||||
c = await self.session.get(ContentItem, content_id)
|
||||
if c is None:
|
||||
return
|
||||
c.read_count = (c.read_count or 0) + 1
|
||||
await self.session.commit()
|
||||
|
||||
async def content_comments(self, content_id: str) -> list[dict]:
|
||||
rows = (await self.session.scalars(
|
||||
select(ContentComment).where(ContentComment.content_id == content_id)
|
||||
.order_by(ContentComment.created_at.desc())
|
||||
)).all()
|
||||
return [{
|
||||
"id": r.id, "content_id": r.content_id, "user_id": r.user_id,
|
||||
"username": r.username, "nickname": r.nickname, "avatar": r.avatar,
|
||||
"content": r.content, "created_at": r.created_at,
|
||||
} for r in rows]
|
||||
|
||||
async def add_content_comment(self, content_id: str, user: dict, text: str) -> dict | None:
|
||||
row = ContentComment(id=new_id("cmt"), content_id=content_id,
|
||||
user_id=user.get("id", ""), username=user.get("username", ""),
|
||||
nickname=user.get("nickname") or user.get("username", ""),
|
||||
avatar=user.get("avatar", ""), content=text,
|
||||
created_at=utcnow_iso())
|
||||
self.session.add(row)
|
||||
await self.session.commit()
|
||||
return {"id": row.id, "content_id": row.content_id, "user_id": row.user_id,
|
||||
"username": row.username, "nickname": row.nickname, "avatar": row.avatar,
|
||||
"content": row.content, "created_at": row.created_at}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 系统配置
|
||||
|
||||
Reference in New Issue
Block a user