diff --git a/.gitignore b/.gitignore index f53fa9f..4887577 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ .trae/documents/add_traefik_config_plan.md redis/data/dump.rdb redis/logs/redis-server.log -backend/auth/__pycache__/* -backend/auth/apps/__pycache__/* -backend/auth/apps/permissions/__pycache__/* -backend/auth/apps/roles/__pycache__/* -backend/auth/utils/__pycache__/* -postgres_data/* \ No newline at end of file +backend/auth/__pycache__/ +backend/auth/apps/__pycache__/ +backend/auth/apps/permissions/__pycache__/ +backend/auth/apps/roles/__pycache__/ +backend/auth/utils/__pycache__/ +postgres_data/ +frontend/node_modules/ +frontend/dist/ + +*.pyc \ No newline at end of file diff --git a/.trae/documents/permission_menu_fix_plan.md b/.trae/documents/permission_menu_fix_plan.md new file mode 100644 index 0000000..7cbc256 --- /dev/null +++ b/.trae/documents/permission_menu_fix_plan.md @@ -0,0 +1,44 @@ +# 角色权限菜单显示问题修复计划 + +## 问题分析 + +用户反馈:创建了 `jayhgq` 用户,分配了"大会员"角色(该角色只能看到用户管理),但登录后仍然能看到角色和权限管理菜单。 + +**根本原因**:前端 `AdminLayout.vue` 中的菜单项是硬编码的,没有根据用户权限动态过滤。 + +## 修复方案 + +### 步骤 1:后端添加获取当前用户权限接口 + +在 `/permissions` 路由中添加一个新接口,用于获取当前登录用户的权限列表。 + +**文件**:`backend/auth/apps/permissions/urls.py` + +### 步骤 2:前端认证 Store 添加权限获取方法 + +在 `auth.ts` 中添加获取用户权限的方法,并存储权限列表。 + +**文件**:`frontend/src/modules/admin/stores/auth.ts` + +### 步骤 3:修改前端布局组件动态渲染菜单 + +修改 `AdminLayout.vue`,根据用户权限动态显示菜单项。 + +**文件**:`frontend/src/modules/admin/layout/AdminLayout.vue` + +### 步骤 4:路由权限控制(可选增强) + +在路由守卫中添加权限验证,防止无权限用户直接访问受保护的路由。 + +**文件**:`frontend/src/router/index.ts` + +## 预期效果 + +- 用户登录后,系统会根据其角色权限动态显示菜单项 +- "大会员"角色只能看到"用户管理"菜单 +- "管理员"角色可以看到所有菜单 + +## 风险评估 + +- 低风险:修改范围明确,不影响核心业务逻辑 +- 需要确保权限编码与菜单的对应关系正确 \ No newline at end of file diff --git a/backend/auth/apps/permissions/urls.py b/backend/auth/apps/permissions/urls.py index 0a42551..8835897 100644 --- a/backend/auth/apps/permissions/urls.py +++ b/backend/auth/apps/permissions/urls.py @@ -215,6 +215,31 @@ async def get_role_permissions( raise HTTPException(status_code=404, detail="角色不存在") # 返回角色权限信息 + return RolePermissionResponse( + role_id=role.id, + role_name=role.name, + permissions=[PermissionResponse.from_orm(p) for p in role.permissions] + ) + + +@router.get("/user/me", response_model=RolePermissionResponse) +async def get_current_user_permissions( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """获取当前登录用户的权限列表""" + # 获取用户角色的权限 + result = await db.execute( + select(Role) + .options(selectinload(Role.permissions)) + .filter(Role.id == current_user.role_id) + ) + role = result.scalar_one_or_none() + + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 返回用户权限信息 return RolePermissionResponse( role_id=role.id, role_name=role.name, diff --git a/backend/auth/apps/users/urls.py b/backend/auth/apps/users/urls.py index 71d0dfa..8dad310 100644 --- a/backend/auth/apps/users/urls.py +++ b/backend/auth/apps/users/urls.py @@ -92,7 +92,11 @@ async def login( @router.post("/", response_model=UserResponse) -async def create_user(user_data: UserCreateRequest, db: AsyncSession = Depends(get_db)): +async def create_user( + user_data: UserCreateRequest, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): """创建用户""" # 检查用户名是否已存在 result = await db.execute(select(User).filter(User.username == user_data.username)) diff --git a/backend/auth/main.py b/backend/auth/main.py index 3c81906..861dded 100644 --- a/backend/auth/main.py +++ b/backend/auth/main.py @@ -1,13 +1,16 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse +from fastapi.exceptions import RequestValidationError from contextlib import asynccontextmanager from apps.urls import api_router from database import engine, get_db from models import Base from init_data import init_all_data from pathlib import Path +from starlette.requests import Request +from starlette.responses import JSONResponse @asynccontextmanager @@ -122,6 +125,33 @@ async def home(): return {"message": "Hello World!!!"} +# 自定义验证错误处理器 +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + errors = [] + for err in exc.errors(): + field = err.get("loc", ["unknown"])[-1] + msg = err.get("msg", "验证失败") + + if "min_length" in msg.lower(): + if field == "username": + msg = "用户名长度至少为3个字符" + elif field == "password": + msg = "密码长度至少为6个字符" + elif "max_length" in msg.lower(): + if field == "username": + msg = "用户名长度不能超过50个字符" + elif field == "password": + msg = "密码长度不能超过50个字符" + + errors.append({"field": field, "message": msg}) + + return JSONResponse( + status_code=422, + content={"code": 422, "message": "验证失败", "errors": errors} + ) + + # 如果使用命令行启动,使用uvicorn 文件名:app --reload启动即可,下面命令就不用写 # 如果写下面的命令,就不需要命令行启动了,直接用IDE运行即可 if __name__ == "__main__": diff --git a/backend/auth/schemas.py b/backend/auth/schemas.py index e535743..927e66d 100644 --- a/backend/auth/schemas.py +++ b/backend/auth/schemas.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, EmailStr, Field +from pydantic import BaseModel, Field from typing import Optional from datetime import datetime @@ -6,19 +6,43 @@ from datetime import datetime class UserBase(BaseModel): """用户基础模型""" - username: str = Field(..., min_length=3, max_length=50, description="登录名") + username: str = Field( + ..., + min_length=3, + max_length=50, + description="登录名", + json_schema_extra={ + "error_messages": { + "min_length": "用户名长度至少为3个字符", + "max_length": "用户名长度不能超过50个字符" + } + } + ) nickname: str = Field(..., max_length=50, description="昵称") - email: Optional[EmailStr] = Field(None, max_length=50, description="电子邮箱") + email: Optional[str] = Field(None, max_length=50, description="电子邮箱") phone: Optional[str] = Field(None, max_length=11, description="手机号码") wx_openid: Optional[str] = Field(None, max_length=100, description="微信OpenID") avatar: Optional[str] = Field(None, max_length=255, description="头像路径") - role_id: int = Field(2, description="角色ID") + role_id: Optional[int] = Field(2, description="角色ID") + + model_config = {"extra": "ignore"} class UserCreateRequest(UserBase): """用户创建请求模型""" - password: str = Field(..., min_length=6, max_length=50, description="密码") + password: str = Field( + ..., + min_length=6, + max_length=50, + description="密码", + json_schema_extra={ + "error_messages": { + "min_length": "密码长度至少为6个字符", + "max_length": "密码长度不能超过50个字符" + } + } + ) class UserCreate(UserBase): @@ -34,7 +58,7 @@ class UserUpdate(BaseModel): username: str = Field(..., min_length=3, max_length=50, description="登录名") nickname: str = Field(..., max_length=50, description="昵称") - email: Optional[EmailStr] = Field(None, max_length=50, description="电子邮箱") + email: Optional[str] = Field(None, max_length=50, description="电子邮箱") phone: Optional[str] = Field(None, max_length=11, description="手机号码") wx_openid: Optional[str] = Field(None, max_length=100, description="微信OpenID") avatar: Optional[str] = Field(None, max_length=255, description="头像路径") @@ -57,7 +81,7 @@ class UserResponse(BaseModel): id: int = Field(..., description="用户ID") username: str = Field(..., description="登录名") nickname: str = Field(..., description="昵称") - email: Optional[EmailStr] = Field(None, description="电子邮箱") + email: Optional[str] = Field(None, description="电子邮箱") phone: Optional[str] = Field(None, description="手机号码") wx_openid: Optional[str] = Field(None, description="微信OpenID") avatar: Optional[str] = Field(None, description="头像路径") @@ -81,7 +105,7 @@ class TokenData(BaseModel): """令牌数据模型""" user_id: Optional[int] = None - email: Optional[EmailStr] = None + email: Optional[str] = None class RoleBase(BaseModel): diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 357b681..101bfc0 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -4,7 +4,7 @@