# -*- coding: utf-8 -*- """内容审核与巡查端点(运营方)+ 用户举报入口。 - /admin/moderation/*:运营方内容治理(隐藏/恢复/删除 + 违规库 + 举报处理 + 巡查) - /content/report:登录用户通用举报入口 """ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Request from ..dependencies import get_current_user, get_db from ...rbac import require_roles, write_audit from ...infrastructure.repositories import Database from ...services import moderation_service from ...services.moderation_service import ModerationError, VIOLATION_REASONS router = APIRouter(prefix="/admin/moderation", tags=["moderation-admin"]) report_router = APIRouter(prefix="/content", tags=["moderation-report"]) # 支持的内容类型白名单(与 moderation_service 一致) CONTENT_TYPES = [ "services", "community_posts", "community_comments", "ratings", "jobs", "talents", "user_pages", "tasks", "dm_messages", "user_profiles", ] def _check_type(content_type: str) -> str: if content_type not in CONTENT_TYPES: raise HTTPException(status_code=400, detail="不支持的内容类型") return content_type def _actor(u: dict) -> tuple[str, str]: return u.get("id", ""), u.get("nickname") or u.get("username", "") @router.get("/types", summary="支持的内容类型与违规原因预设") async def moderation_meta(_u: dict = Depends(require_roles("operator"))): return {"types": CONTENT_TYPES, "reasons": VIOLATION_REASONS} @router.get("/{content_type}", summary="内容列表(含发布者/用户ID/IP/状态)") async def moderation_list(content_type: str, status: str = "", keyword: str = "", db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator"))): _check_type(content_type) try: items = await moderation_service.list_content( db, content_type=content_type, status=status, keyword=keyword) except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e return {"items": items} @router.post("/{content_type}/{content_id}/hide", summary="隐藏并移入违规内容库") async def moderation_hide(content_type: str, content_id: str, body: dict, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_roles("operator"))): _check_type(content_type) reason = str(body.get("reason", "")).strip() if not reason: raise HTTPException(status_code=400, detail="请填写违规原因") oid, oname = _actor(actor) try: r = await moderation_service.hide( db, content_type=content_type, content_id=content_id, reason=reason, severity=str(body.get("severity", "general")), operator_id=oid, operator_name=oname) except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e await write_audit(db, action="admin.moderation.hide", resource=content_type, resource_id=content_id, detail=f"{reason} | {body.get('severity', 'general')}", user=actor, request=request) return r @router.post("/{content_type}/{content_id}/restore", summary="恢复内容(违规库同步 recovered)") async def moderation_restore(content_type: str, content_id: str, body: dict, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_roles("operator"))): _check_type(content_type) oid, oname = _actor(actor) try: r = await moderation_service.restore( db, content_type=content_type, content_id=content_id, note=str(body.get("note", "")), operator_id=oid, operator_name=oname) except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e await write_audit(db, action="admin.moderation.restore", resource=content_type, resource_id=content_id, detail=str(body.get("note", "")), user=actor, request=request) return r @router.delete("/{content_type}/{content_id}", summary="删除内容并归档违规库(留快照取证)") async def moderation_delete(content_type: str, content_id: str, request: Request, reason: str = "内容违法", db: Database = Depends(get_db), actor: dict = Depends(require_roles("operator"))): _check_type(content_type) reason = reason.strip() or "内容违法" oid, oname = _actor(actor) try: r = await moderation_service.delete( db, content_type=content_type, content_id=content_id, reason=reason, operator_id=oid, operator_name=oname) except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e await write_audit(db, action="admin.moderation.delete", resource=content_type, resource_id=content_id, detail=reason, user=actor, request=request) return r @router.get("/violations", summary="违规内容库(统一归档查询)") async def moderation_archive(content_type: str = "", severity: str = "", status: str = "", q: str = "", db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator"))): if content_type: _check_type(content_type) return {"items": await moderation_service.archive_list( db, content_type=content_type, severity=severity, status=status, q=q)} @router.post("/violations/{record_id}/restore", summary="从违规库恢复内容") async def moderation_archive_restore(record_id: str, body: dict, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_roles("operator"))): rec = await db.session.get(moderation_service.ContentModerationRecord, record_id) if rec is None: raise HTTPException(status_code=404, detail="违规记录不存在") oid, oname = _actor(actor) try: r = await moderation_service.restore( db, content_type=rec.content_type, content_id=rec.content_id, note=str(body.get("note", "")), operator_id=oid, operator_name=oname) except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e await write_audit(db, action="admin.moderation.restore", resource="moderation_record", resource_id=record_id, detail=str(body.get("note", "")), user=actor, request=request) return r @router.get("/reports", summary="举报队列") async def moderation_reports(status: str = "pending", db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator"))): from sqlalchemy import select from ...infrastructure.models import ContentReport stmt = select(ContentReport).order_by(ContentReport.created_at.desc()) if status: stmt = stmt.where(ContentReport.status == status) rows = (await db.session.scalars(stmt)).all() return {"items": [ {"id": r.id, "contentType": r.content_type, "contentId": r.content_id, "reporterId": r.reporter_id, "reason": r.reason, "detail": r.detail, "status": r.status, "createdAt": r.created_at, "processedAt": r.processed_at, "processorId": r.processor_id, "result": r.result} for r in rows ]} @router.post("/reports/{report_id}/handle", summary="处理举报(confirm 确认违规并隐藏 / ignore 驳回)") async def moderation_report_handle(report_id: str, body: dict, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_roles("operator"))): action = str(body.get("action", "")) if action not in ("confirm", "ignore"): raise HTTPException(status_code=400, detail="action 须为 confirm/ignore") oid, oname = _actor(actor) try: r = await moderation_service.handle_report( db, report_id=report_id, action=action, reason=str(body.get("reason", "")), operator_id=oid, operator_name=oname) except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e await write_audit(db, action="admin.moderation.report_handle", resource="content_report", resource_id=report_id, detail=f"{action} | {body.get('reason', '')}", user=actor, request=request) return r @router.get("/scan", summary="巡查:关键词命中列表") async def moderation_scan(keyword: str, content_type: str = "", db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator"))): if content_type: _check_type(content_type) try: return {"items": await moderation_service.scan(db, keyword=keyword, content_type=content_type)} except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e @report_router.post("/report", summary="用户举报内容(通用入口)") async def content_report(body: dict, db: Database = Depends(get_db), user: dict = Depends(get_current_user)): content_type = str(body.get("contentType", "")).strip() content_id = str(body.get("contentId", "")).strip() if content_type not in CONTENT_TYPES or not content_id: raise HTTPException(status_code=400, detail="缺少内容类型或内容ID") try: r = await moderation_service.report( db, content_type=content_type, content_id=content_id, reporter_id=user["id"], reason=str(body.get("reason", "")).strip(), detail=str(body.get("detail", "")).strip()) except ModerationError as e: raise HTTPException(status_code=400, detail=str(e)) from e return r