e831fdd34e
- 迁移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
129 lines
6.3 KiB
Python
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")
|