|
|
@@ -19,7 +19,7 @@ from fastapi import FastAPI, HTTPException, Depends, Request
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
|
from pydantic import BaseModel
|
|
|
-from typing import Optional
|
|
|
+from typing import Optional, Any
|
|
|
import hashlib
|
|
|
import secrets
|
|
|
# 修复JWT导入 - 确保使用正确的JWT库
|
|
|
@@ -54,6 +54,9 @@ from datetime import datetime, timedelta, timezone
|
|
|
import pymysql
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
+# 导入RBAC API - 移除循环导入
|
|
|
+# from rbac_api import get_user_menus, get_all_menus, get_all_roles, get_user_permissions
|
|
|
+
|
|
|
# 数据模型
|
|
|
class LoginRequest(BaseModel):
|
|
|
username: str
|
|
|
@@ -81,7 +84,7 @@ class UserInfo(BaseModel):
|
|
|
class ApiResponse(BaseModel):
|
|
|
code: int
|
|
|
message: str
|
|
|
- data: Optional[dict] = None
|
|
|
+ data: Optional[Any] = None
|
|
|
timestamp: str
|
|
|
|
|
|
# 配置
|
|
|
@@ -348,6 +351,17 @@ async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(s
|
|
|
""", (user_id,))
|
|
|
|
|
|
user_data = cursor.fetchone()
|
|
|
+
|
|
|
+ # 获取用户角色
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT r.name
|
|
|
+ FROM user_roles ur
|
|
|
+ JOIN roles r ON ur.role_id = r.id
|
|
|
+ WHERE ur.user_id = %s AND ur.is_active = 1
|
|
|
+ """, (user_id,))
|
|
|
+
|
|
|
+ roles = [row[0] for row in cursor.fetchall()]
|
|
|
+
|
|
|
cursor.close()
|
|
|
conn.close()
|
|
|
|
|
|
@@ -373,7 +387,8 @@ async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(s
|
|
|
"real_name": user_data[10],
|
|
|
"company": user_data[11],
|
|
|
"department": user_data[12],
|
|
|
- "position": user_data[13]
|
|
|
+ "position": user_data[13],
|
|
|
+ "roles": roles
|
|
|
}
|
|
|
|
|
|
return ApiResponse(
|
|
|
@@ -1418,9 +1433,23 @@ async def get_apps(
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
+ # 检查用户角色,决定是否显示所有应用
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT COUNT(*) FROM user_roles ur
|
|
|
+ JOIN roles r ON ur.role_id = r.id
|
|
|
+ WHERE ur.user_id = %s AND r.name IN ('super_admin', 'admin', 'app_manager') AND ur.is_active = 1
|
|
|
+ """, (user_id,))
|
|
|
+
|
|
|
+ is_app_manager = cursor.fetchone()[0] > 0
|
|
|
+
|
|
|
# 构建查询条件
|
|
|
- where_conditions = ["created_by = %s"]
|
|
|
- params = [user_id]
|
|
|
+ where_conditions = []
|
|
|
+ params = []
|
|
|
+
|
|
|
+ # 如果不是应用管理员,只显示自己创建的应用
|
|
|
+ if not is_app_manager:
|
|
|
+ where_conditions.append("created_by = %s")
|
|
|
+ params.append(user_id)
|
|
|
|
|
|
if keyword:
|
|
|
where_conditions.append("(name LIKE %s OR description LIKE %s)")
|
|
|
@@ -1431,7 +1460,7 @@ async def get_apps(
|
|
|
elif status == "inactive":
|
|
|
where_conditions.append("is_active = 0")
|
|
|
|
|
|
- where_clause = " AND ".join(where_conditions)
|
|
|
+ where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
|
|
|
|
|
# 查询总数
|
|
|
cursor.execute(f"SELECT COUNT(*) FROM apps WHERE {where_clause}", params)
|
|
|
@@ -2184,6 +2213,1299 @@ def generate_captcha():
|
|
|
# 返回默认验证码
|
|
|
return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="
|
|
|
|
|
|
+# RBAC权限管理API
|
|
|
+@app.get("/api/v1/user/menus")
|
|
|
+async def api_get_user_menus(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """获取用户菜单"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(
|
|
|
+ code=401,
|
|
|
+ message="无效的访问令牌",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ user_id = payload.get("sub")
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="数据库连接失败",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查用户是否是超级管理员
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT COUNT(*) FROM user_roles ur
|
|
|
+ JOIN roles r ON ur.role_id = r.id
|
|
|
+ WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
|
|
|
+ """, (user_id,))
|
|
|
+
|
|
|
+ is_super_admin = cursor.fetchone()[0] > 0
|
|
|
+
|
|
|
+ if is_super_admin:
|
|
|
+ # 超级管理员返回所有活跃菜单
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT m.id, m.parent_id, m.name, m.title, m.path,
|
|
|
+ m.component, m.icon, m.sort_order, m.menu_type,
|
|
|
+ m.is_hidden, m.is_active
|
|
|
+ FROM menus m
|
|
|
+ WHERE m.is_active = 1
|
|
|
+ ORDER BY m.sort_order
|
|
|
+ """)
|
|
|
+ else:
|
|
|
+ # 普通用户根据角色权限获取菜单
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT m.id, m.parent_id, m.name, m.title, m.path,
|
|
|
+ m.component, m.icon, m.sort_order, m.menu_type,
|
|
|
+ m.is_hidden, m.is_active
|
|
|
+ FROM menus m
|
|
|
+ JOIN role_menus rm ON m.id = rm.menu_id
|
|
|
+ JOIN user_roles ur ON rm.role_id = ur.role_id
|
|
|
+ WHERE ur.user_id = %s
|
|
|
+ AND ur.is_active = 1
|
|
|
+ AND m.is_active = 1
|
|
|
+ GROUP BY m.id, m.parent_id, m.name, m.title, m.path,
|
|
|
+ m.component, m.icon, m.sort_order, m.menu_type,
|
|
|
+ m.is_hidden, m.is_active
|
|
|
+ ORDER BY m.sort_order
|
|
|
+ """, (user_id,))
|
|
|
+
|
|
|
+ menus = []
|
|
|
+ for row in cursor.fetchall():
|
|
|
+ menu = {
|
|
|
+ "id": row[0],
|
|
|
+ "parent_id": row[1],
|
|
|
+ "name": row[2],
|
|
|
+ "title": row[3],
|
|
|
+ "path": row[4],
|
|
|
+ "component": row[5],
|
|
|
+ "icon": row[6],
|
|
|
+ "sort_order": row[7],
|
|
|
+ "menu_type": row[8],
|
|
|
+ "is_hidden": bool(row[9]),
|
|
|
+ "is_active": bool(row[10]),
|
|
|
+ "children": []
|
|
|
+ }
|
|
|
+ menus.append(menu)
|
|
|
+
|
|
|
+ # 构建菜单树
|
|
|
+ menu_tree = build_menu_tree(menus)
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="获取用户菜单成功",
|
|
|
+ data=menu_tree,
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取用户菜单错误: {e}")
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="服务器内部错误",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+def build_menu_tree(menus):
|
|
|
+ """构建菜单树结构"""
|
|
|
+ menu_map = {menu["id"]: menu for menu in menus}
|
|
|
+ tree = []
|
|
|
+
|
|
|
+ for menu in menus:
|
|
|
+ if menu["parent_id"] is None:
|
|
|
+ tree.append(menu)
|
|
|
+ else:
|
|
|
+ parent = menu_map.get(menu["parent_id"])
|
|
|
+ if parent:
|
|
|
+ parent["children"].append(menu)
|
|
|
+
|
|
|
+ return tree
|
|
|
+
|
|
|
+@app.get("/api/v1/admin/menus")
|
|
|
+async def api_get_all_menus(
|
|
|
+ page: int = 1,
|
|
|
+ page_size: int = 1000, # 增大默认页面大小,确保返回所有菜单
|
|
|
+ keyword: Optional[str] = None,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """获取所有菜单(管理员)"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(
|
|
|
+ code=401,
|
|
|
+ message="无效的访问令牌",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 简化权限检查 - 只检查是否为管理员
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(
|
|
|
+ code=403,
|
|
|
+ message="权限不足",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="数据库连接失败",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 构建查询条件
|
|
|
+ where_conditions = []
|
|
|
+ params = []
|
|
|
+
|
|
|
+ if keyword:
|
|
|
+ where_conditions.append("(m.title LIKE %s OR m.name LIKE %s)")
|
|
|
+ params.extend([f"%{keyword}%", f"%{keyword}%"])
|
|
|
+
|
|
|
+ where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
|
|
+
|
|
|
+ # 查询总数
|
|
|
+ cursor.execute(f"SELECT COUNT(*) FROM menus m WHERE {where_clause}", params)
|
|
|
+ total = cursor.fetchone()[0]
|
|
|
+
|
|
|
+ # 查询菜单列表 - 修改排序逻辑以支持树形结构
|
|
|
+ cursor.execute(f"""
|
|
|
+ SELECT m.id, m.parent_id, m.name, m.title, m.path, m.component,
|
|
|
+ m.icon, m.sort_order, m.menu_type, m.is_hidden, m.is_active,
|
|
|
+ m.description, m.created_at, m.updated_at,
|
|
|
+ pm.title as parent_title
|
|
|
+ FROM menus m
|
|
|
+ LEFT JOIN menus pm ON m.parent_id = pm.id
|
|
|
+ WHERE {where_clause}
|
|
|
+ ORDER BY
|
|
|
+ CASE WHEN m.parent_id IS NULL THEN 0 ELSE 1 END,
|
|
|
+ m.sort_order,
|
|
|
+ CASE WHEN m.menu_type = 'menu' THEN 0 ELSE 1 END,
|
|
|
+ m.created_at
|
|
|
+ LIMIT %s OFFSET %s
|
|
|
+ """, params + [page_size, (page - 1) * page_size])
|
|
|
+
|
|
|
+ menus = []
|
|
|
+ for row in cursor.fetchall():
|
|
|
+ menu = {
|
|
|
+ "id": row[0],
|
|
|
+ "parent_id": row[1],
|
|
|
+ "name": row[2],
|
|
|
+ "title": row[3],
|
|
|
+ "path": row[4],
|
|
|
+ "component": row[5],
|
|
|
+ "icon": row[6],
|
|
|
+ "sort_order": row[7],
|
|
|
+ "menu_type": row[8],
|
|
|
+ "is_hidden": bool(row[9]),
|
|
|
+ "is_active": bool(row[10]),
|
|
|
+ "description": row[11],
|
|
|
+ "created_at": row[12].isoformat() if row[12] else None,
|
|
|
+ "updated_at": row[13].isoformat() if row[13] else None,
|
|
|
+ "parent_title": row[14]
|
|
|
+ }
|
|
|
+ menus.append(menu)
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="获取菜单列表成功",
|
|
|
+ data={
|
|
|
+ "items": menus,
|
|
|
+ "total": total,
|
|
|
+ "page": page,
|
|
|
+ "page_size": page_size
|
|
|
+ },
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取菜单列表错误: {e}")
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="服务器内部错误",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+@app.get("/api/v1/admin/roles")
|
|
|
+async def api_get_all_roles(
|
|
|
+ page: int = 1,
|
|
|
+ page_size: int = 20,
|
|
|
+ keyword: Optional[str] = None,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """获取所有角色"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(
|
|
|
+ code=401,
|
|
|
+ message="无效的访问令牌",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 简化权限检查 - 只检查是否为管理员
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(
|
|
|
+ code=403,
|
|
|
+ message="权限不足",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="数据库连接失败",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 构建查询条件
|
|
|
+ where_conditions = []
|
|
|
+ params = []
|
|
|
+
|
|
|
+ if keyword:
|
|
|
+ where_conditions.append("(r.display_name LIKE %s OR r.name LIKE %s)")
|
|
|
+ params.extend([f"%{keyword}%", f"%{keyword}%"])
|
|
|
+
|
|
|
+ where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
|
|
+
|
|
|
+ # 查询总数
|
|
|
+ cursor.execute(f"SELECT COUNT(*) FROM roles r WHERE {where_clause}", params)
|
|
|
+ total = cursor.fetchone()[0]
|
|
|
+
|
|
|
+ # 查询角色列表
|
|
|
+ offset = (page - 1) * page_size
|
|
|
+ cursor.execute(f"""
|
|
|
+ SELECT r.id, r.name, r.display_name, r.description, r.is_active,
|
|
|
+ r.is_system, r.created_at, r.updated_at,
|
|
|
+ COUNT(ur.user_id) as user_count
|
|
|
+ FROM roles r
|
|
|
+ LEFT JOIN user_roles ur ON r.id = ur.role_id AND ur.is_active = 1
|
|
|
+ WHERE {where_clause}
|
|
|
+ GROUP BY r.id
|
|
|
+ ORDER BY r.is_system DESC, r.created_at
|
|
|
+ LIMIT %s OFFSET %s
|
|
|
+ """, params + [page_size, offset])
|
|
|
+
|
|
|
+ roles = []
|
|
|
+ for row in cursor.fetchall():
|
|
|
+ role = {
|
|
|
+ "id": row[0],
|
|
|
+ "name": row[1],
|
|
|
+ "display_name": row[2],
|
|
|
+ "description": row[3],
|
|
|
+ "is_active": bool(row[4]),
|
|
|
+ "is_system": bool(row[5]),
|
|
|
+ "created_at": row[6].isoformat() if row[6] else None,
|
|
|
+ "updated_at": row[7].isoformat() if row[7] else None,
|
|
|
+ "user_count": row[8]
|
|
|
+ }
|
|
|
+ roles.append(role)
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="获取角色列表成功",
|
|
|
+ data={
|
|
|
+ "items": roles,
|
|
|
+ "total": total,
|
|
|
+ "page": page,
|
|
|
+ "page_size": page_size
|
|
|
+ },
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取角色列表错误: {e}")
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="服务器内部错误",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+@app.get("/api/v1/user/permissions")
|
|
|
+async def api_get_user_permissions(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """获取用户权限"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(
|
|
|
+ code=401,
|
|
|
+ message="无效的访问令牌",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ user_id = payload.get("sub")
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="数据库连接失败",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 获取用户权限
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT DISTINCT p.name, p.resource, p.action
|
|
|
+ FROM permissions p
|
|
|
+ JOIN role_permissions rp ON p.id = rp.permission_id
|
|
|
+ JOIN user_roles ur ON rp.role_id = ur.role_id
|
|
|
+ WHERE ur.user_id = %s
|
|
|
+ AND ur.is_active = 1
|
|
|
+ AND p.is_active = 1
|
|
|
+ """, (user_id,))
|
|
|
+
|
|
|
+ permissions = []
|
|
|
+ for row in cursor.fetchall():
|
|
|
+ permissions.append({
|
|
|
+ "name": row[0],
|
|
|
+ "resource": row[1],
|
|
|
+ "action": row[2]
|
|
|
+ })
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="获取用户权限成功",
|
|
|
+ data=permissions,
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取用户权限错误: {e}")
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="服务器内部错误",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+# 用户管理API
|
|
|
+@app.get("/api/v1/admin/users")
|
|
|
+async def get_users(
|
|
|
+ page: int = 1,
|
|
|
+ page_size: int = 20,
|
|
|
+ keyword: Optional[str] = None,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """获取用户列表"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 构建查询条件
|
|
|
+ where_conditions = []
|
|
|
+ params = []
|
|
|
+
|
|
|
+ if keyword:
|
|
|
+ where_conditions.append("(u.username LIKE %s OR u.email LIKE %s OR up.real_name LIKE %s)")
|
|
|
+ params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"])
|
|
|
+
|
|
|
+ where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
|
|
+
|
|
|
+ # 查询总数
|
|
|
+ cursor.execute(f"SELECT COUNT(*) FROM users u LEFT JOIN user_profiles up ON u.id = up.user_id WHERE {where_clause}", params)
|
|
|
+ total = cursor.fetchone()[0]
|
|
|
+
|
|
|
+ # 查询用户列表
|
|
|
+ offset = (page - 1) * page_size
|
|
|
+ cursor.execute(f"""
|
|
|
+ SELECT u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
|
|
|
+ u.last_login_at, u.created_at, up.real_name, up.company, up.department,
|
|
|
+ GROUP_CONCAT(r.display_name) as roles
|
|
|
+ FROM users u
|
|
|
+ LEFT JOIN user_profiles up ON u.id = up.user_id
|
|
|
+ LEFT JOIN user_roles ur ON u.id = ur.user_id AND ur.is_active = 1
|
|
|
+ LEFT JOIN roles r ON ur.role_id = r.id
|
|
|
+ WHERE {where_clause}
|
|
|
+ GROUP BY u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
|
|
|
+ u.last_login_at, u.created_at, up.real_name, up.company, up.department
|
|
|
+ ORDER BY u.created_at DESC
|
|
|
+ LIMIT %s OFFSET %s
|
|
|
+ """, params + [page_size, offset])
|
|
|
+
|
|
|
+ users = []
|
|
|
+ for row in cursor.fetchall():
|
|
|
+ users.append({
|
|
|
+ "id": row[0],
|
|
|
+ "username": row[1],
|
|
|
+ "email": row[2],
|
|
|
+ "phone": row[3],
|
|
|
+ "is_active": bool(row[4]),
|
|
|
+ "is_superuser": bool(row[5]),
|
|
|
+ "last_login_at": row[6].isoformat() if row[6] else None,
|
|
|
+ "created_at": row[7].isoformat() if row[7] else None,
|
|
|
+ "real_name": row[8],
|
|
|
+ "company": row[9],
|
|
|
+ "department": row[10],
|
|
|
+ "roles": row[11].split(',') if row[11] else []
|
|
|
+ })
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="获取用户列表成功",
|
|
|
+ data={"items": users, "total": total, "page": page, "page_size": page_size},
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取用户列表错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.post("/api/v1/admin/users")
|
|
|
+async def create_user(
|
|
|
+ user_data: dict,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """创建用户"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查用户名和邮箱是否已存在
|
|
|
+ cursor.execute("SELECT id FROM users WHERE username = %s OR email = %s",
|
|
|
+ (user_data['username'], user_data['email']))
|
|
|
+ if cursor.fetchone():
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="用户名或邮箱已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 生成用户ID
|
|
|
+ user_id = str(uuid.uuid4())
|
|
|
+
|
|
|
+ # 创建密码哈希
|
|
|
+ password_hash = hash_password_simple(user_data['password'])
|
|
|
+
|
|
|
+ # 插入用户
|
|
|
+ cursor.execute("""
|
|
|
+ INSERT INTO users (id, username, email, phone, password_hash, is_active, is_superuser, created_at, updated_at)
|
|
|
+ VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
|
|
+ """, (user_id, user_data['username'], user_data['email'], user_data.get('phone'),
|
|
|
+ password_hash, user_data.get('is_active', True), user_data.get('is_superuser', False)))
|
|
|
+
|
|
|
+ # 插入用户详情
|
|
|
+ if any(key in user_data for key in ['real_name', 'company', 'department']):
|
|
|
+ profile_id = str(uuid.uuid4())
|
|
|
+ cursor.execute("""
|
|
|
+ INSERT INTO user_profiles (id, user_id, real_name, company, department, created_at, updated_at)
|
|
|
+ VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
|
|
|
+ """, (profile_id, user_id, user_data.get('real_name'), user_data.get('company'), user_data.get('department')))
|
|
|
+
|
|
|
+ # 分配角色
|
|
|
+ if 'role_ids' in user_data and user_data['role_ids']:
|
|
|
+ for role_id in user_data['role_ids']:
|
|
|
+ role_assignment_id = str(uuid.uuid4())
|
|
|
+ cursor.execute("""
|
|
|
+ INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
|
|
|
+ VALUES (%s, %s, %s, %s, NOW())
|
|
|
+ """, (role_assignment_id, user_id, role_id, payload.get("sub")))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="用户创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"创建用户错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.put("/api/v1/admin/users/{user_id}")
|
|
|
+async def update_user(
|
|
|
+ user_id: str,
|
|
|
+ user_data: dict,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """更新用户"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 更新用户基本信息
|
|
|
+ update_fields = []
|
|
|
+ update_values = []
|
|
|
+
|
|
|
+ for field in ['email', 'phone', 'is_active', 'is_superuser']:
|
|
|
+ if field in user_data:
|
|
|
+ update_fields.append(f'{field} = %s')
|
|
|
+ update_values.append(user_data[field])
|
|
|
+
|
|
|
+ if update_fields:
|
|
|
+ update_values.append(user_id)
|
|
|
+ cursor.execute(f"""
|
|
|
+ UPDATE users
|
|
|
+ SET {', '.join(update_fields)}, updated_at = NOW()
|
|
|
+ WHERE id = %s
|
|
|
+ """, update_values)
|
|
|
+
|
|
|
+ # 更新用户详情
|
|
|
+ profile_fields = ['real_name', 'company', 'department']
|
|
|
+ profile_updates = {k: v for k, v in user_data.items() if k in profile_fields}
|
|
|
+
|
|
|
+ if profile_updates:
|
|
|
+ # 检查是否已有记录
|
|
|
+ cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
|
|
|
+ profile_exists = cursor.fetchone()
|
|
|
+
|
|
|
+ if profile_exists:
|
|
|
+ update_fields = []
|
|
|
+ update_values = []
|
|
|
+ for field, value in profile_updates.items():
|
|
|
+ update_fields.append(f'{field} = %s')
|
|
|
+ update_values.append(value)
|
|
|
+
|
|
|
+ update_values.append(user_id)
|
|
|
+ cursor.execute(f"""
|
|
|
+ UPDATE user_profiles
|
|
|
+ SET {', '.join(update_fields)}, updated_at = NOW()
|
|
|
+ WHERE user_id = %s
|
|
|
+ """, update_values)
|
|
|
+ else:
|
|
|
+ profile_id = str(uuid.uuid4())
|
|
|
+ fields = ['id', 'user_id'] + list(profile_updates.keys())
|
|
|
+ values = [profile_id, user_id] + list(profile_updates.values())
|
|
|
+ placeholders = ', '.join(['%s'] * len(values))
|
|
|
+
|
|
|
+ cursor.execute(f"""
|
|
|
+ INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
|
|
|
+ VALUES ({placeholders}, NOW(), NOW())
|
|
|
+ """, values)
|
|
|
+
|
|
|
+ # 更新用户角色
|
|
|
+ if 'role_ids' in user_data:
|
|
|
+ # 删除现有角色
|
|
|
+ cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
|
|
|
+
|
|
|
+ # 添加新角色
|
|
|
+ for role_id in user_data['role_ids']:
|
|
|
+ assignment_id = str(uuid.uuid4())
|
|
|
+ cursor.execute("""
|
|
|
+ INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
|
|
|
+ VALUES (%s, %s, %s, %s, NOW())
|
|
|
+ """, (assignment_id, user_id, role_id, payload.get("sub")))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="用户更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"更新用户错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.delete("/api/v1/admin/users/{user_id}")
|
|
|
+async def delete_user(
|
|
|
+ user_id: str,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """删除用户"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 不能删除自己
|
|
|
+ if user_id == payload.get("sub"):
|
|
|
+ return ApiResponse(code=400, message="不能删除自己", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查是否为超级管理员
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT COUNT(*) FROM user_roles ur
|
|
|
+ JOIN roles r ON ur.role_id = r.id
|
|
|
+ WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
|
|
|
+ """, (user_id,))
|
|
|
+
|
|
|
+ if cursor.fetchone()[0] > 0:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="不能删除超级管理员", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 删除相关数据
|
|
|
+ cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
|
|
|
+ cursor.execute("DELETE FROM user_profiles WHERE user_id = %s", (user_id,))
|
|
|
+ cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="用户删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"删除用户错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+# 角色管理API
|
|
|
+@app.post("/api/v1/admin/roles")
|
|
|
+async def create_role(
|
|
|
+ role_data: dict,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """创建角色"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查角色名是否已存在
|
|
|
+ cursor.execute("SELECT id FROM roles WHERE name = %s", (role_data['name'],))
|
|
|
+ if cursor.fetchone():
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="角色名已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 创建角色
|
|
|
+ role_id = str(uuid.uuid4())
|
|
|
+ cursor.execute("""
|
|
|
+ INSERT INTO roles (id, name, display_name, description, is_active, is_system, created_at, updated_at)
|
|
|
+ VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
|
|
|
+ """, (role_id, role_data['name'], role_data['display_name'], role_data.get('description'),
|
|
|
+ role_data.get('is_active', True), False))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="角色创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"创建角色错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.put("/api/v1/admin/roles/{role_id}")
|
|
|
+async def update_role(
|
|
|
+ role_id: str,
|
|
|
+ role_data: dict,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """更新角色"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查是否为系统角色
|
|
|
+ cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
|
|
|
+ role = cursor.fetchone()
|
|
|
+ if not role:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ if role[0]: # is_system
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="不能修改系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 更新角色
|
|
|
+ update_fields = []
|
|
|
+ update_values = []
|
|
|
+
|
|
|
+ for field in ['display_name', 'description', 'is_active']:
|
|
|
+ if field in role_data:
|
|
|
+ update_fields.append(f'{field} = %s')
|
|
|
+ update_values.append(role_data[field])
|
|
|
+
|
|
|
+ if update_fields:
|
|
|
+ update_values.append(role_id)
|
|
|
+ cursor.execute(f"""
|
|
|
+ UPDATE roles
|
|
|
+ SET {', '.join(update_fields)}, updated_at = NOW()
|
|
|
+ WHERE id = %s
|
|
|
+ """, update_values)
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="角色更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"更新角色错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.delete("/api/v1/admin/roles/{role_id}")
|
|
|
+async def delete_role(
|
|
|
+ role_id: str,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """删除角色"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查是否为系统角色
|
|
|
+ cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
|
|
|
+ role = cursor.fetchone()
|
|
|
+ if not role:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ if role[0]: # is_system
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="不能删除系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 检查是否有用户使用此角色
|
|
|
+ cursor.execute("SELECT COUNT(*) FROM user_roles WHERE role_id = %s", (role_id,))
|
|
|
+ if cursor.fetchone()[0] > 0:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="该角色正在被使用,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 删除角色相关数据
|
|
|
+ cursor.execute("DELETE FROM role_permissions WHERE role_id = %s", (role_id,))
|
|
|
+ cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
|
|
|
+ cursor.execute("DELETE FROM roles WHERE id = %s", (role_id,))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="角色删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"删除角色错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+# 角色菜单权限管理API
|
|
|
+@app.get("/api/v1/admin/roles/{role_id}/menus")
|
|
|
+async def get_role_menus(
|
|
|
+ role_id: str,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """获取角色的菜单权限"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(
|
|
|
+ code=401,
|
|
|
+ message="无效的访问令牌",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 检查管理员权限
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(
|
|
|
+ code=403,
|
|
|
+ message="权限不足",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="数据库连接失败",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查角色是否存在
|
|
|
+ cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
|
|
|
+ role = cursor.fetchone()
|
|
|
+
|
|
|
+ if not role:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(
|
|
|
+ code=404,
|
|
|
+ message="角色不存在",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 检查是否为超级管理员角色
|
|
|
+ role_name = role[1]
|
|
|
+ is_super_admin_role = role_name == "super_admin"
|
|
|
+
|
|
|
+ if is_super_admin_role:
|
|
|
+ # 超级管理员默认拥有所有菜单权限
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT id, name, title, parent_id, menu_type
|
|
|
+ FROM menus
|
|
|
+ WHERE is_active = 1
|
|
|
+ ORDER BY sort_order
|
|
|
+ """)
|
|
|
+ menu_permissions = cursor.fetchall()
|
|
|
+ else:
|
|
|
+ # 普通角色查询已分配的菜单权限
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT m.id, m.name, m.title, m.parent_id, m.menu_type
|
|
|
+ FROM role_menus rm
|
|
|
+ JOIN menus m ON rm.menu_id = m.id
|
|
|
+ WHERE rm.role_id = %s AND m.is_active = 1
|
|
|
+ ORDER BY m.sort_order
|
|
|
+ """, (role_id,))
|
|
|
+ menu_permissions = cursor.fetchall()
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ # 构建返回数据
|
|
|
+ menu_ids = [menu[0] for menu in menu_permissions]
|
|
|
+ menu_details = []
|
|
|
+
|
|
|
+ for menu in menu_permissions:
|
|
|
+ menu_details.append({
|
|
|
+ "id": menu[0],
|
|
|
+ "name": menu[1],
|
|
|
+ "title": menu[2],
|
|
|
+ "parent_id": menu[3],
|
|
|
+ "menu_type": menu[4]
|
|
|
+ })
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="获取角色菜单权限成功",
|
|
|
+ data={
|
|
|
+ "role_id": role_id,
|
|
|
+ "role_name": role[1],
|
|
|
+ "menu_ids": menu_ids,
|
|
|
+ "menu_details": menu_details,
|
|
|
+ "total": len(menu_ids)
|
|
|
+ },
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取角色菜单权限错误: {e}")
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="服务器内部错误",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+@app.put("/api/v1/admin/roles/{role_id}/menus")
|
|
|
+async def update_role_menus(
|
|
|
+ role_id: str,
|
|
|
+ request: Request,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """更新角色的菜单权限"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(
|
|
|
+ code=401,
|
|
|
+ message="无效的访问令牌",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 检查管理员权限
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(
|
|
|
+ code=403,
|
|
|
+ message="权限不足",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 获取请求数据
|
|
|
+ body = await request.json()
|
|
|
+ menu_ids = body.get("menu_ids", [])
|
|
|
+
|
|
|
+ if not isinstance(menu_ids, list):
|
|
|
+ return ApiResponse(
|
|
|
+ code=400,
|
|
|
+ message="菜单ID列表格式错误",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="数据库连接失败",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查角色是否存在
|
|
|
+ cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
|
|
|
+ role = cursor.fetchone()
|
|
|
+
|
|
|
+ if not role:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(
|
|
|
+ code=404,
|
|
|
+ message="角色不存在",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 检查是否为超级管理员角色
|
|
|
+ role_name = role[1]
|
|
|
+ is_super_admin_role = role_name == "super_admin"
|
|
|
+
|
|
|
+ if is_super_admin_role:
|
|
|
+ # 超级管理员角色不允许修改权限,始终拥有全部权限
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(
|
|
|
+ code=400,
|
|
|
+ message="超级管理员角色拥有全部权限,无需修改",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 验证菜单ID是否存在
|
|
|
+ if menu_ids:
|
|
|
+ placeholders = ','.join(['%s'] * len(menu_ids))
|
|
|
+ cursor.execute(f"""
|
|
|
+ SELECT id FROM menus
|
|
|
+ WHERE id IN ({placeholders}) AND is_active = 1
|
|
|
+ """, menu_ids)
|
|
|
+
|
|
|
+ valid_menu_ids = [row[0] for row in cursor.fetchall()]
|
|
|
+ invalid_menu_ids = set(menu_ids) - set(valid_menu_ids)
|
|
|
+
|
|
|
+ if invalid_menu_ids:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(
|
|
|
+ code=400,
|
|
|
+ message=f"无效的菜单ID: {', '.join(invalid_menu_ids)}",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ # 开始事务
|
|
|
+ cursor.execute("START TRANSACTION")
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 删除角色现有的菜单权限
|
|
|
+ cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
|
|
|
+
|
|
|
+ # 添加新的菜单权限
|
|
|
+ if menu_ids:
|
|
|
+ values = [(role_id, menu_id) for menu_id in menu_ids]
|
|
|
+ cursor.executemany("""
|
|
|
+ INSERT INTO role_menus (role_id, menu_id, created_at)
|
|
|
+ VALUES (%s, %s, NOW())
|
|
|
+ """, values)
|
|
|
+
|
|
|
+ # 提交事务
|
|
|
+ conn.commit()
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="角色菜单权限更新成功",
|
|
|
+ data={
|
|
|
+ "role_id": role_id,
|
|
|
+ "role_name": role[1],
|
|
|
+ "menu_ids": menu_ids,
|
|
|
+ "updated_count": len(menu_ids)
|
|
|
+ },
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ # 回滚事务
|
|
|
+ conn.rollback()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ raise e
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"更新角色菜单权限错误: {e}")
|
|
|
+ return ApiResponse(
|
|
|
+ code=500,
|
|
|
+ message="服务器内部错误",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+
|
|
|
+# 菜单管理API
|
|
|
+@app.post("/api/v1/admin/menus")
|
|
|
+async def create_menu(
|
|
|
+ menu_data: dict,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """创建菜单"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查菜单名是否已存在
|
|
|
+ cursor.execute("SELECT id FROM menus WHERE name = %s", (menu_data['name'],))
|
|
|
+ if cursor.fetchone():
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="菜单标识已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 创建菜单
|
|
|
+ menu_id = str(uuid.uuid4())
|
|
|
+ cursor.execute("""
|
|
|
+ INSERT INTO menus (id, parent_id, name, title, path, component, icon,
|
|
|
+ sort_order, menu_type, is_hidden, is_active, description, created_at, updated_at)
|
|
|
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
|
|
+ """, (
|
|
|
+ menu_id, menu_data.get('parent_id'), menu_data['name'], menu_data['title'],
|
|
|
+ menu_data.get('path'), menu_data.get('component'), menu_data.get('icon'),
|
|
|
+ menu_data.get('sort_order', 0), menu_data.get('menu_type', 'menu'),
|
|
|
+ menu_data.get('is_hidden', False), menu_data.get('is_active', True),
|
|
|
+ menu_data.get('description')
|
|
|
+ ))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="菜单创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"创建菜单错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.put("/api/v1/admin/menus/{menu_id}")
|
|
|
+async def update_menu(
|
|
|
+ menu_id: str,
|
|
|
+ menu_data: dict,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """更新菜单"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 更新菜单
|
|
|
+ update_fields = []
|
|
|
+ update_values = []
|
|
|
+
|
|
|
+ for field in ['parent_id', 'title', 'path', 'component', 'icon', 'sort_order',
|
|
|
+ 'menu_type', 'is_hidden', 'is_active', 'description']:
|
|
|
+ if field in menu_data:
|
|
|
+ update_fields.append(f'{field} = %s')
|
|
|
+ update_values.append(menu_data[field])
|
|
|
+
|
|
|
+ if update_fields:
|
|
|
+ update_values.append(menu_id)
|
|
|
+ cursor.execute(f"""
|
|
|
+ UPDATE menus
|
|
|
+ SET {', '.join(update_fields)}, updated_at = NOW()
|
|
|
+ WHERE id = %s
|
|
|
+ """, update_values)
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="菜单更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"更新菜单错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.delete("/api/v1/admin/menus/{menu_id}")
|
|
|
+async def delete_menu(
|
|
|
+ menu_id: str,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """删除菜单"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ is_superuser = payload.get("is_superuser", False)
|
|
|
+ if not is_superuser:
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 检查是否有子菜单
|
|
|
+ cursor.execute("SELECT COUNT(*) FROM menus WHERE parent_id = %s", (menu_id,))
|
|
|
+ if cursor.fetchone()[0] > 0:
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+ return ApiResponse(code=400, message="该菜单下有子菜单,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 删除菜单相关数据
|
|
|
+ cursor.execute("DELETE FROM role_menus WHERE menu_id = %s", (menu_id,))
|
|
|
+ cursor.execute("DELETE FROM menus WHERE id = %s", (menu_id,))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="菜单删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"删除菜单错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+# 获取所有角色(用于下拉选择)
|
|
|
+@app.get("/api/v1/roles/all")
|
|
|
+async def get_all_roles_simple(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """获取所有角色(简化版,用于下拉选择)"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload:
|
|
|
+ return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT id, name, display_name, is_system, is_active
|
|
|
+ FROM roles
|
|
|
+ WHERE is_active = 1
|
|
|
+ ORDER BY is_system DESC, display_name
|
|
|
+ """)
|
|
|
+
|
|
|
+ roles = []
|
|
|
+ for row in cursor.fetchall():
|
|
|
+ roles.append({
|
|
|
+ "id": row[0],
|
|
|
+ "name": row[1],
|
|
|
+ "display_name": row[2],
|
|
|
+ "is_system": bool(row[3]),
|
|
|
+ "is_active": bool(row[4])
|
|
|
+ })
|
|
|
+
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="获取角色列表成功", data=roles, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取角色列表错误: {e}")
|
|
|
+ return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
if __name__ == "__main__":
|
|
|
import uvicorn
|
|
|
|