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:
Pine
2026-08-27 15:28:27 +08:00
parent b80fba6150
commit a60f0ed180
4 changed files with 123 additions and 3 deletions
+35
View File
@@ -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")
+38 -3
View File
@@ -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),
+16
View File
@@ -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):
"""平台系统配置(键值)。"""
+34
View File
@@ -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}
# ---------------------------------------------------------------------------
# 系统配置