Files
server-core/alembic/versions/0070_content_moderation.py
T
Pine e831fdd34e feat: 内容审核体系(0070)— 违规内容库/举报表/IP采集/统一审核服务/审核API
- 迁移0070:content_moderation_records(违规库,快照+发布者+IP+原因+处置人)、
  content_reports(举报队列);各内容表加 ip 列,dm_messages 加 hidden,
  community_comments/ratings 加 status,users 加 profile_hidden/original_nickname
- moderation_service:hide/restore/delete(物理删除留档)/list/archive/report/
  handle_report/scan + 申诉通知;用户资料隐藏自动脱敏昵称
- rbac_moderation:/admin/moderation/{type} 列表、hide/restore/delete、
  violations 违规库、reports 举报处理、scan 巡查;/content/report 用户举报入口
- IP 采集:任务/岗位/服务/社区帖子/评论/私信/评价/主页提交创建端点写入发布者 IP
- 前台可见性:社区评论/私信查询过滤 hidden
2026-09-07 15:07:43 +08:00

129 lines
6.3 KiB
Python

"""内容审核体系:违规内容库 + 用户举报 + 各内容表 IP/隐藏状态列。
依据《网络安全法》第47条、《网络信息内容生态治理规定》(网信办令第5号):
平台须对违法/不良信息及时处置并保存记录。本迁移落地:
- content_moderation_records 违规内容库(统一归档,隐藏/删除均留快照取证)
- content_reports 用户举报队列
- 各内容表补充 ip 字段(发布者 IP,仅用于安全与内容治理)
- 评论/评价补充 status 列(hidden/published);私信补充 hidden 标记
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "0070_content_moderation"
down_revision = "0069_user_page_social"
branch_labels = None
depends_on = None
def _table_exists(name: str) -> bool:
bind = op.get_bind()
return bool(
bind.execute(
sa.text(
"SELECT COUNT(*) FROM information_schema.TABLES "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :n"
),
{"n": name},
).scalar()
)
def _column_exists(table: str, column: str) -> bool:
bind = op.get_bind()
return bool(
bind.execute(
sa.text(
"SELECT COUNT(*) FROM information_schema.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t AND COLUMN_NAME = :c"
),
{"t": table, "c": column},
).scalar()
)
def _add_column_if_missing(table: str, column: sa.Column) -> None:
if not _column_exists(table, column.name):
op.add_column(table, column)
def upgrade() -> None:
# ── 违规内容库 ─────────────────────────────────────────
if not _table_exists("content_moderation_records"):
op.create_table(
"content_moderation_records",
sa.Column("id", sa.String(40), primary_key=True),
sa.Column("content_type", sa.String(32), nullable=False, index=True),
sa.Column("content_id", sa.String(64), nullable=False),
sa.Column("snapshot_json", sa.Text, default="{}"),
sa.Column("publisher_id", sa.String(64), nullable=True, index=True),
sa.Column("publisher_name", sa.String(128), default=""),
sa.Column("ip", sa.String(64), default=""),
sa.Column("reason", sa.String(255), default=""),
sa.Column("severity", sa.String(16), default="general"), # general/severe
sa.Column("action", sa.String(16), default="hidden"), # hidden/deleted
sa.Column("status", sa.String(16), default="active"), # active/recovered
sa.Column("operator_id", sa.String(64), default=""),
sa.Column("operator_name", sa.String(128), default=""),
sa.Column("operated_at", sa.String(32), default=""),
sa.Column("recover_note", sa.Text, default=""),
)
# ── 用户举报 ───────────────────────────────────────────
if not _table_exists("content_reports"):
op.create_table(
"content_reports",
sa.Column("id", sa.String(40), primary_key=True),
sa.Column("content_type", sa.String(32), nullable=False, index=True),
sa.Column("content_id", sa.String(64), nullable=False),
sa.Column("reporter_id", sa.String(64), nullable=True, index=True),
sa.Column("reason", sa.String(255), default=""),
sa.Column("detail", sa.Text, default=""),
sa.Column("status", sa.String(16), default="pending"), # pending/processed/ignored
sa.Column("created_at", sa.String(32), default=""),
sa.Column("processed_at", sa.String(32), default=""),
sa.Column("processor_id", sa.String(64), default=""),
sa.Column("result", sa.String(255), default=""),
)
# ── 各内容表:ip 列 ────────────────────────────────────
for tbl in ("community_posts", "community_comments", "jobs",
"opc_services", "tasks", "user_pages", "ratings"):
_add_column_if_missing(tbl, sa.Column("ip", sa.String(64), nullable=False, server_default=""))
# 私信:ip + 隐藏标记
_add_column_if_missing("dm_messages", sa.Column("ip", sa.String(64), nullable=False, server_default=""))
_add_column_if_missing("dm_messages", sa.Column("hidden", sa.Integer(), nullable=False, server_default="0"))
# ── 评论/评价:隐藏状态列(默认 published) ─────────────
_add_column_if_missing("community_comments",
sa.Column("status", sa.String(16), nullable=False, server_default="published"))
_add_column_if_missing("ratings",
sa.Column("status", sa.String(16), nullable=False, server_default="published"))
# ── 用户资料审核标记(users.profile_hidden + 原昵称留档) ──
_add_column_if_missing("users", sa.Column("profile_hidden", sa.Integer(), nullable=False, server_default="0"))
_add_column_if_missing("users", sa.Column("profile_original_nickname", sa.String(128), nullable=False, server_default=""))
def downgrade() -> None:
bind = op.get_bind()
for tbl in ("community_posts", "community_comments", "jobs",
"opc_services", "tasks", "user_pages", "ratings", "dm_messages"):
if _column_exists(tbl, "ip"):
op.drop_column(tbl, "ip")
if _column_exists("dm_messages", "hidden"):
op.drop_column("dm_messages", "hidden")
if _column_exists("community_comments", "status"):
op.drop_column("community_comments", "status")
if _column_exists("ratings", "status"):
op.drop_column("ratings", "status")
if _column_exists("users", "profile_hidden"):
op.drop_column("users", "profile_hidden")
if _column_exists("users", "profile_original_nickname"):
op.drop_column("users", "profile_original_nickname")
if _table_exists("content_reports"):
op.drop_table("content_reports")
if _table_exists("content_moderation_records"):
op.drop_table("content_moderation_records")