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