✨ feat: WP-11 社区——发布、互动、关注与治理

- 迁移 12:community_content(帖子/角色 Release、publication 六态、可见性)、comment(父级树)、reaction((content,actor,kind) 唯一防重复)、follow(双 FK 显式命名修复 convention 冲突)、report + moderation_case(举报即建司议案)
- 发布:帖子直发;角色 Release 由服务端构造公开快照(name/道体/版本数)——私密记忆/关系/对话结构性缺席,测试以注入私密记忆后发布验证零泄露
- 互动:评论(父级校验同内容可见)、点赞/收藏(重复 409、取消后计数正确)
- 关注/取关与计数;举报自动生成司议案(治理队列入口,status=pending)
- 下架传播:removed 后列表/详情/评论统一 404 或过滤(require_content 单点);非作者下架 404 fail-closed
- 测试基建修复:login 返回自包含 headers(显式 Cookie 优先),多用户交替不再串会话
- 测试 79 项全绿(+7)
This commit is contained in:
chenxin committed 2026-09-12 09:02:36 +08:00
1 parent 34c481ab9e
commit 99985f5855
7 files changed
+823 -2

No files matched your search

+169
View File
@@ -796,3 +796,172 @@ class WebhookEvent(Base):
__table_args__ = (
CheckConstraint("status IN ('processed', 'ignored')", name="status_values"),
)
class CommunityContent(Base):
"""社区内容:帖子与角色 Release(WP-11)。
角色 Release 由服务端构造公开快照;私密记忆/关系/对话在结构上不进入发布物。
"""
__tablename__ = "community_content"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("content_")
)
author_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
kind: Mapped[str] = mapped_column(String, nullable=False, default="post")
title: Mapped[str] = mapped_column(String, nullable=False)
body: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
character_id: Mapped[str | None] = mapped_column(
ForeignKey("character.id", ondelete="set null"), nullable=True
)
visibility: Mapped[str] = mapped_column(String, nullable=False, default="public")
status: Mapped[str] = mapped_column(String, nullable=False, default="published")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
CheckConstraint("kind IN ('post', 'character_release')", name="kind_values"),
CheckConstraint("visibility IN ('public', 'followers')", name="visibility_values"),
CheckConstraint(
"status IN ('draft', 'submitted', 'published', 'limited', 'suspended', 'removed')",
name="status_values",
),
)
class Comment(Base):
"""社区评论:平铺 + 可选父级;下架内容下的评论不可见。"""
__tablename__ = "comment"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("comment_"))
content_id: Mapped[str] = mapped_column(
ForeignKey("community_content.id", ondelete="cascade"), nullable=False, index=True
)
author_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False
)
parent_comment_id: Mapped[str | None] = mapped_column(
ForeignKey("comment.id", ondelete="cascade"), nullable=True
)
body: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False, default="visible")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("status IN ('visible', 'removed')", name="status_values"),
)
class Reaction(Base):
"""互动:点赞/收藏;(content, actor, kind) 唯一防重复。"""
__tablename__ = "reaction"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("reaction_"))
content_id: Mapped[str] = mapped_column(
ForeignKey("community_content.id", ondelete="cascade"), nullable=False, index=True
)
actor_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False
)
kind: Mapped[str] = mapped_column(String, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("kind IN ('like', 'save')", name="kind_values"),
Index(
"uq_reaction_content_actor_kind",
"content_id", "actor_domain_id", "kind",
unique=True,
),
)
class Follow(Base):
"""关注关系(域→域)。"""
__tablename__ = "follow"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("follow_"))
# 双 FK 同指 personal_domain:显式命名避免 convention 同名冲突
follower_domain_id: Mapped[str] = mapped_column(
ForeignKey(
"personal_domain.id", ondelete="cascade", name="fk_follow_follower_domain"
),
nullable=False,
index=True,
)
followee_domain_id: Mapped[str] = mapped_column(
ForeignKey(
"personal_domain.id", ondelete="cascade", name="fk_follow_followee_domain"
),
nullable=False,
index=True,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
Index(
"uq_follow_pair", "follower_domain_id", "followee_domain_id", unique=True
),
)
class Report(Base):
"""举报:创建即生成司议案(治理队列入口)。"""
__tablename__ = "report"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("report_"))
reporter_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
target_type: Mapped[str] = mapped_column(String, nullable=False)
target_id: Mapped[str] = mapped_column(String, nullable=False)
reason: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("target_type IN ('content', 'comment')", name="target_type_values"),
CheckConstraint("status IN ('pending', 'reviewed', 'actioned')", name="status_values"),
)
class ModerationCase(Base):
"""司议案:由举报生成,处置动作与结果留痕。"""
__tablename__ = "moderation_case"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("moderation_")
)
report_id: Mapped[str] = mapped_column(
ForeignKey("report.id", ondelete="cascade"), nullable=False, index=True
)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
action_taken: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("status IN ('pending', 'resolved')", name="status_values"),
)