# Phase F.3:权限系统 日期:2026-04-04 状态:待开始 依赖:F.2(待完成) 前置:models/user.py --- ## 1. 本阶段目的 实现 Jarvis Forum 的权限系统: - 用户角色管理(user/moderator/admin) - 板块权限控制 - 操作日志记录 - 积分/奖励系统 --- ## 2. 用户角色系统 ### 2.1 角色定义 ```python class UserRole(str, Enum): """用户角色枚举""" GUEST = "guest" # 访客 - 只能浏览 USER = "user" # 普通用户 - 可发帖/回复 MODERATOR = "moderator" # 版主 - 可管理板块 ADMIN = "admin" # 管理员 - 全权限 class Permission(str, Enum): """权限枚举""" # 帖子权限 POST_CREATE = "post:create" POST_EDIT_OWN = "post:edit:own" POST_EDIT_ANY = "post:edit:any" POST_DELETE_OWN = "post:delete:own" POST_DELETE_ANY = "post:delete:any" POST_PIN = "post:pin" POST_LOCK = "post:lock" POST_VIEW = "post:view" # 回复权限 REPLY_CREATE = "reply:create" REPLY_EDIT_OWN = "reply:edit:own" REPLY_DELETE_OWN = "reply:delete:own" REPLY_DELETE_ANY = "reply:delete:any" REPLY_PIN = "reply:pin" REPLY_BEST = "reply:best" # 板块权限 BOARD_CREATE = "board:create" BOARD_EDIT = "board:edit" BOARD_DELETE = "board:delete" # 标签权限 TAG_CREATE = "tag:create" TAG_MANAGE = "tag:manage" # 用户权限 USER_BAN = "user:ban" USER_ROLE = "user:role" # 积分权限 SCORE_VIEW = "score:view" SCORE_MANAGE = "score:manage" ``` ### 2.2 角色权限映射 ```python ROLE_PERMISSIONS: Dict[UserRole, Set[Permission]] = { UserRole.GUEST: { Permission.POST_VIEW, }, UserRole.USER: { Permission.POST_VIEW, Permission.POST_CREATE, Permission.POST_EDIT_OWN, Permission.POST_DELETE_OWN, Permission.REPLY_CREATE, Permission.REPLY_EDIT_OWN, Permission.REPLY_DELETE_OWN, Permission.SCORE_VIEW, }, UserRole.MODERATOR: { # 用户权限 Permission.POST_VIEW, Permission.POST_CREATE, Permission.POST_EDIT_OWN, Permission.POST_DELETE_OWN, Permission.REPLY_CREATE, Permission.REPLY_EDIT_OWN, Permission.REPLY_DELETE_OWN, # 版主权限 Permission.POST_PIN, Permission.POST_LOCK, Permission.POST_DELETE_ANY, Permission.REPLY_PIN, Permission.REPLY_BEST, Permission.TAG_MANAGE, Permission.SCORE_VIEW, }, UserRole.ADMIN: set(Permission), # 所有权限 } ``` --- ## 3. User 模型扩展 ### 3.1 新增字段 ```python # models/user.py 扩展 class User(BaseModel): __tablename__ = "users" # === 现有字段 === id: str username: str email: str hashed_password: str avatar: Optional[str] created_at: datetime # === Forum 相关扩展 === role: str = "user" # guest/user/moderator/admin forum_score: int = 0 # 论坛积分 # 论坛统计 post_count: int = 0 reply_count: int = 0 like_received: int = 0 best_reply_count: int = 0 # 状态 is_forum_banned: bool = False # 论坛禁言 forum_ban_until: Optional[datetime] = None # 禁言截止时间 # 板块版主关系 moderated_boards: List[str] = [] # 管理的板块 ID 列表 ``` --- ## 4. Permission Service ### 4.1 服务实现 ```python class PermissionService: """权限服务""" def __init__(self, db: AsyncSession): self.db = db def has_permission(self, user: User, permission: Permission) -> bool: """检查用户是否拥有某权限""" if user.is_forum_banned: return False if user.forum_ban_until and user.forum_ban_until > datetime.utcnow(): return False role = UserRole(user.role) return permission in ROLE_PERMISSIONS.get(role, set()) def has_board_permission( self, user: User, permission: Permission, board_id: str, ) -> bool: """检查用户在特定板块的权限""" if not self.has_permission(user, permission): return False # 版主只能管理自己板块 if role == UserRole.MODERATOR: return board_id in user.moderated_boards return True async def can_edit_post(self, user: User, post: ForumPost) -> bool: """检查用户是否可以编辑帖子""" if user.role == UserRole.ADMIN: return True if user.role == UserRole.MODERATOR: return post.board_id in user.moderated_boards return post.user_id == user.id async def can_delete_post(self, user: User, post: ForumPost) -> bool: """检查用户是否可以删除帖子""" if user.role == UserRole.ADMIN: return True if user.role == UserRole.MODERATOR: return post.board_id in user.moderated_boards return post.user_id == user.id async def ban_user( self, admin: User, target_user_id: str, reason: str, duration: Optional[timedelta] = None, ) -> None: """禁言用户""" if admin.role != UserRole.ADMIN: raise PermissionError("需要管理员权限") result = await self.db.execute( select(User).where(User.id == target_user_id) ) target_user = result.scalar_one_or_none() if not target_user: raise ValueError("用户不存在") target_user.is_forum_banned = True target_user.forum_ban_until = ( datetime.utcnow() + duration if duration else None ) # 记录操作日志 await self._log_action( admin=admin, action="ban_user", target_id=target_user_id, details={"reason": reason, "duration": str(duration)}, ) ``` ### 4.2 依赖注入 ```python # 在 ForumService 中注入 class ForumService: def __init__( self, db: AsyncSession, lock_manager: ForumLockManager, permission_service: PermissionService, ): self.db = db self.lock = lock_manager self.perms = permission_service ``` --- ## 5. 操作日志 ### 5.1 日志模型 ```python class ForumLog(BaseModel): __tablename__ = "forum_logs" id: str = Field(default_factory=lambda: str(uuid.uuid4())) user_id: str # 操作者 action: str # 操作类型 target_type: str # post/reply/board/user/tag target_id: str # 目标 ID details: Optional[str] = None # JSON 详情 ip_address: Optional[str] = None user_agent: Optional[str] = None created_at: datetime = Field(default_factory=datetime.utcnow) ``` ### 5.2 日志记录 ```python async def _log_action( self, admin: User, action: str, target_id: str, details: Optional[dict] = None, request: Optional[Request] = None, ) -> None: """记录操作日志""" log = ForumLog( user_id=admin.id, action=action, target_type=action.split(":")[0], target_id=target_id, details=json.dumps(details) if details else None, ip_address=request.client.host if request else None, user_agent=request.headers.get("user-agent") if request else None, ) self.db.add(log) await self.db.commit() # 日志操作类型 class ForumAction(str, Enum): POST_CREATE = "post:create" POST_UPDATE = "post:update" POST_DELETE = "post:delete" POST_PIN = "post:pin" POST_LOCK = "post:lock" REPLY_CREATE = "reply:create" REPLY_UPDATE = "reply:update" REPLY_DELETE = "reply:delete" USER_BAN = "user:ban" USER_UNBAN = "user:unban" SCORE_CHANGE = "score:change" BOARD_CREATE = "board:create" BOARD_UPDATE = "board:update" BOARD_DELETE = "board:delete" ``` --- ## 6. 积分系统 ### 6.1 积分规则 ```python SCORE_RULES = { # 帖子相关 "post_create": 5, # 发帖 "post_delete": -5, # 删除帖子 "post_liked": 2, # 帖子被点赞 "post_best": 10, # 帖子被设为精华 # 回复相关 "reply_create": 2, # 回复 "reply_delete": -2, # 删除回复 "reply_liked": 1, # 回复被点赞 "reply_best": 5, # 回复被设为最佳 "reply_adopted": 10, # 提问被采纳 # 活跃相关 "daily_login": 1, # 每日登录 "daily_post": 3, # 每日首次发帖 "daily_reply": 1, # 每日首次回复 } class ScoreService: """积分服务""" def __init__(self, db: AsyncSession): self.db = db async def add_score( self, user_id: str, action: str, reason: str, ) -> int: """增加积分""" score_delta = SCORE_RULES.get(action, 0) result = await self.db.execute( select(User).where(User.id == user_id) ) user = result.scalar_one_or_none() if user: user.forum_score += score_delta # 更新统计 if action.startswith("post_"): user.post_count += 1 elif action.startswith("reply_"): user.reply_count += 1 if action == "post_liked": user.like_received += 1 elif action == "reply_liked": user.like_received += 1 elif action == "reply_best": user.best_reply_count += 1 await self.db.commit() return score_delta async def get_leaderboard( self, limit: int = 10, period: str = "all", # all/month/week ) -> List[dict]: """获取积分排行榜""" query = select(User).where(User.forum_score > 0) if period == "month": # 本月排行 pass elif period == "week": # 本周排行 pass query = query.order_by(desc(User.forum_score)).limit(limit) result = await self.db.execute(query) return [ { "rank": i + 1, "user_id": user.id, "username": user.username, "avatar": user.avatar, "score": user.forum_score, } for i, user in enumerate(result.scalars().all()) ] ``` --- ## 7. API 端点 ### 7.1 管理端点 ```python @router.post("/admin/ban/{user_id}") async def ban_user( user_id: str, reason: str, duration: Optional[int] = None, # 天数 current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """禁言用户(仅管理员)""" if current_user.role != "admin": raise HTTPException(status_code=403, detail="需要管理员权限") perm_service = PermissionService(db) await perm_service.ban_user( admin=current_user, target_user_id=user_id, reason=reason, duration=timedelta(days=duration) if duration else None, ) return {"success": True} @router.post("/admin/unban/{user_id}") async def unban_user( user_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """解除禁言""" if current_user.role != "admin": raise HTTPException(status_code=403, detail="需要管理员权限") perm_service = PermissionService(db) await perm_service.unban_user(current_user, user_id) return {"success": True} @router.get("/admin/logs") async def get_logs( target_id: Optional[str] = None, action: Optional[str] = None, page: int = 1, page_size: int = 50, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """查看操作日志(仅管理员/版主)""" if current_user.role not in ["admin", "moderator"]: raise HTTPException(status_code=403, detail="权限不足") # 查询日志... return {"logs": [], "total": 0} @router.get("/leaderboard") async def get_leaderboard( limit: int = 10, period: str = "all", db: AsyncSession = Depends(get_db), ): """获取积分排行榜""" score_service = ScoreService(db) return await score_service.get_leaderboard(limit, period) ``` --- ## 8. 实现步骤 | 步骤 | 任务 | 优先级 | |------|------|--------| | 1 | 扩展 User 模型 | 🟢 高 | | 2 | 创建 PermissionService | 🟢 高 | | 3 | 实现权限检查装饰器 | 🟢 高 | | 4 | 创建 ForumLog 模型 | 🟡 中 | | 5 | 实现操作日志记录 | 🟡 中 | | 6 | 创建 ScoreService | 🟡 中 | | 7 | 实现积分规则 | 🟡 中 | | 8 | 扩展管理 API | 🟡 中 | | 9 | 单元测试 | 🟡 中 | --- ## 9. 核心文件变更 | 文件 | 变更 | |------|------| | `models/user.py` | 扩展角色和统计字段 | | `models/forum.py` | 新增 ForumLog | | `services/permission_service.py` | 新增 | | `services/score_service.py` | 新增 | | `services/forum_service.py` | 集成权限检查 | | `routers/forum.py` | 扩展管理端点 | --- ## 10. 工作量估算 | 任务 | 工作量 | |------|--------| | User 模型扩展 | 0.5 天 | | PermissionService | 0.5 天 | | 操作日志 | 0.5 天 | | ScoreService | 0.5 天 | | API 端点 | 0.5 天 | | 单元测试 | 0.5 天 | | **总计** | **3 天** | --- ## 11. 验收标准 - [ ] User 模型正确存储角色和积分 - [ ] PermissionService 可正确检查权限 - [ ] 权限不足时返回 403 - [ ] 所有管理操作记录日志 - [ ] 积分根据规则正确增减 - [ ] 排行榜正确排序 - [ ] 禁言功能正常工作 - [ ] 单元测试覆盖核心逻辑