Files
JARVIS/development-doc/plan/forum-update/phase-f-3-permissions.md

14 KiB
Raw Blame History

Phase F.3:权限系统

日期2026-04-04 状态:待开始 依赖F.2(待完成) 前置models/user.py


1. 本阶段目的

实现 Jarvis Forum 的权限系统:

  • 用户角色管理user/moderator/admin
  • 板块权限控制
  • 操作日志记录
  • 积分/奖励系统

2. 用户角色系统

2.1 角色定义

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 角色权限映射

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 新增字段

# 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 服务实现

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 依赖注入

# 在 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 日志模型

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 日志记录

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 积分规则

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 管理端点

@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
  • 所有管理操作记录日志
  • 积分根据规则正确增减
  • 排行榜正确排序
  • 禁言功能正常工作
  • 单元测试覆盖核心逻辑