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