feat: 菜单管理添加排序功能

- Permission 模型新增 sort 字段
- 后端 API 按 sort 升序排列树形菜单
- 前端菜单管理页面增加排序列
- 初始数据添加排序号
- 修复 GET /permissions/{id} 详情接口 bug

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jayhgq 2026-07-20 08:33:51 +00:00
parent 0545d68018
commit 9586c7467a
6 changed files with 1334 additions and 1275 deletions

View File

@ -44,7 +44,9 @@ async def create_permission(
name=permission_data.name,
code=permission_data.code,
parentid=permission_data.parentid,
route=permission_data.route,
description=permission_data.description,
sort=permission_data.sort,
)
db.add(db_permission)
@ -112,6 +114,7 @@ async def get_permission_tree(
"parentid": perm.parentid,
"route": perm.route,
"description": perm.description,
"sort": perm.sort or 0,
"createtime": perm.createtime,
"updatetime": perm.updatetime,
"children": []
@ -128,6 +131,16 @@ async def get_permission_tree(
# 子节点,添加到父节点的 children
perm_dict[parentid]["children"].append(perm_data)
# 递归排序:按 sort 字段升序排列
def sort_tree(nodes):
nodes.sort(key=lambda n: n.get("sort", 0))
for node in nodes:
if node.get("children"):
sort_tree(node["children"])
return nodes
tree = sort_tree(tree)
# 记录查询日志
await create_operation_log(
db, current_user.id, current_user.username, "search", "permission",
@ -187,6 +200,7 @@ async def get_menu(
"parentid": perm.parentid,
"route": perm.route,
"description": perm.description,
"sort": perm.sort or 0,
"createtime": perm.createtime,
"updatetime": perm.updatetime,
"children": []
@ -203,6 +217,16 @@ async def get_menu(
# 子节点,添加到父节点的 children
perm_dict[parentid]["children"].append(perm_data)
# 递归排序:按 sort 字段升序排列
def sort_tree(nodes):
nodes.sort(key=lambda n: n.get("sort", 0))
for node in nodes:
if node.get("children"):
sort_tree(node["children"])
return nodes
tree = sort_tree(tree)
# 记录查询日志
await create_operation_log(
db, current_user.id, current_user.username, "search", "permission",
@ -222,7 +246,7 @@ async def get_permission(
current_user: User = Depends(get_current_user),
):
"""获取单个权限"""
permission = await permission_crud.get(db, id=permission_id)
permission = await permission_crud.get(db, id=permission_id, schema_to_select=PermissionResponse, return_as_model=True)
if not permission:
raise HTTPException(status_code=404, detail="权限不存在")
@ -271,7 +295,7 @@ async def update_permission(
raise HTTPException(status_code=404, detail="权限不存在")
# 记录操作日志
changed = {}
for field in ["name", "code", "parentid", "description"]:
for field in ["name", "code", "parentid", "route", "sort", "description"]:
old_val = getattr(old_permission, field, None)
new_val = update_data.get(field)
if old_val != new_val:
@ -452,6 +476,7 @@ async def get_current_user_permissions(
"parentid": perm.parentid,
"route": perm.route,
"description": perm.description,
"sort": perm.sort or 0,
"createtime": perm.createtime,
"updatetime": perm.updatetime,
"children": []
@ -468,6 +493,16 @@ async def get_current_user_permissions(
# 子节点,添加到父节点的 children
perm_dict[parentid]["children"].append(perm_data)
# 递归排序:按 sort 字段升序排列
def sort_tree(nodes):
nodes.sort(key=lambda n: n.get("sort", 0))
for node in nodes:
if node.get("children"):
sort_tree(node["children"])
return nodes
menu_tree = sort_tree(menu_tree)
# 记录查询日志
await create_operation_log(
db, current_user.id, current_user.username, "search", "permission",

View File

@ -1,452 +1,451 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text
from models import Role, User, Permission, SystemSetting
from utils.password import get_password_hash
async def init_permissions(db: AsyncSession):
"""初始化权限数据(树形结构)"""
# 定义需要初始化的权限(树形结构)
# parentid: 0 = 顶级模块, >0 = 子权限
permission_tree = [
# ========================================
# 顶级模块 (DB ID: 1,2,3)
# ========================================
{"name": "权限管理", "code": "perm", "parentid": 0, "route": "/admin/permissions", "description": "权限管理模块"},
{"name": "系统设置", "code": "system", "parentid": 0, "route": "/admin/settings", "description": "系统设置模块"},
{"name": "日志管理", "code": "log", "parentid": 0, "route": "/admin/logs", "description": "日志管理模块"},
# ========================================
# 权限管理 -> 二级子模块 (parentid=1, DB ID: 4,5,6)
# ========================================
{"name": "用户管理", "code": "perm:user", "parentid": 1, "route": "/admin/users", "description": "用户管理子模块"},
{"name": "角色管理", "code": "perm:role", "parentid": 1, "route": "/admin/roles", "description": "角色管理子模块"},
{"name": "菜单管理", "code": "perm:menu", "parentid": 1, "route": "/admin/menu", "description": "菜单管理子模块"},
# ========================================
# 用户管理操作 (parentid=4, DB ID: 7-14)
# ========================================
{"name": "查看列表", "code": "perm:user:list", "parentid": 4, "description": "查看用户列表"},
{"name": "查看详情", "code": "perm:user:detail", "parentid": 4, "description": "查看用户详情"},
{"name": "搜索", "code": "perm:user:search", "parentid": 4, "description": "搜索用户"},
{"name": "创建", "code": "perm:user:create", "parentid": 4, "description": "创建新用户"},
{"name": "编辑", "code": "perm:user:edit", "parentid": 4, "description": "编辑用户信息"},
{"name": "删除", "code": "perm:user:delete", "parentid": 4, "description": "删除用户"},
{"name": "导出", "code": "perm:user:export", "parentid": 4, "description": "导出用户数据"},
{"name": "批量操作", "code": "perm:user:batch", "parentid": 4, "description": "批量操作用户"},
# ========================================
# 角色管理操作 (parentid=5, DB ID: 15-22)
# ========================================
{"name": "查看列表", "code": "perm:role:list", "parentid": 5, "description": "查看角色列表"},
{"name": "查看详情", "code": "perm:role:detail", "parentid": 5, "description": "查看角色详情"},
{"name": "搜索", "code": "perm:role:search", "parentid": 5, "description": "搜索角色"},
{"name": "创建", "code": "perm:role:create", "parentid": 5, "description": "创建新角色"},
{"name": "编辑", "code": "perm:role:edit", "parentid": 5, "description": "编辑角色信息"},
{"name": "删除", "code": "perm:role:delete", "parentid": 5, "description": "删除角色"},
{"name": "导出", "code": "perm:role:export", "parentid": 5, "description": "导出角色数据"},
{"name": "分配权限", "code": "perm:role:assign", "parentid": 5, "description": "为角色分配权限"},
# ========================================
# 菜单管理操作 (parentid=6, DB ID: 23-29)
# ========================================
{"name": "查看列表", "code": "perm:menu:list", "parentid": 6, "description": "查看菜单列表"},
{"name": "查看详情", "code": "perm:menu:detail", "parentid": 6, "description": "查看菜单详情"},
{"name": "搜索", "code": "perm:menu:search", "parentid": 6, "description": "搜索菜单"},
{"name": "创建", "code": "perm:menu:create", "parentid": 6, "description": "创建新菜单"},
{"name": "编辑", "code": "perm:menu:edit", "parentid": 6, "description": "编辑菜单信息"},
{"name": "删除", "code": "perm:menu:delete", "parentid": 6, "description": "删除菜单"},
{"name": "导出", "code": "perm:menu:export", "parentid": 6, "description": "导出菜单数据"},
# ========================================
# 系统设置 -> 子权限 (parentid=2, DB ID: 30)
# ========================================
{"name": "系统设置", "code": "system:view", "parentid": 2, "description": "查看和修改系统设置"},
# ========================================
# 日志管理 -> 子模块 (parentid=3, DB ID: 31,32)
# ========================================
{"name": "登录日志", "code": "log:login", "parentid": 3, "route": "/admin/login-logs", "description": "登录日志查看"},
{"name": "操作日志", "code": "log:operation", "parentid": 3, "route": "/admin/operation-logs", "description": "操作日志查看"},
# ========================================
# 登录日志操作 (parentid=31, DB ID: 33,34)
# ========================================
{"name": "查看列表", "code": "log:login:list", "parentid": 31, "description": "查看登录日志列表"},
{"name": "搜索", "code": "log:login:search", "parentid": 31, "description": "搜索登录日志"},
# ========================================
# 操作日志操作 (parentid=32, DB ID: 35,36)
# ========================================
{"name": "查看列表", "code": "log:operation:list", "parentid": 32, "description": "查看操作日志列表"},
{"name": "搜索", "code": "log:operation:search", "parentid": 32, "description": "搜索操作日志"},
# ========================================
# 日志清理 (parentid=3, DB ID: 37)
# ========================================
{"name": "清理日志", "code": "log:clean", "parentid": 3, "description": "清理旧日志"},
]
# 检查现有权限
result = await db.execute(select(Permission))
existing_permissions = result.scalars().all()
existing_permission_codes = {perm.code: perm for perm in existing_permissions}
# 创建或更新权限
created_count = 0
updated_count = 0
for perm_data in permission_tree:
perm_code = perm_data["code"]
if perm_code not in existing_permission_codes:
# 权限不存在,需要创建
db.add(Permission(**perm_data))
created_count += 1
else:
# 权限存在,检查是否需要更新
existing_perm = existing_permission_codes[perm_code]
update_needed = False
if existing_perm.name != perm_data["name"]:
existing_perm.name = perm_data["name"]
update_needed = True
if existing_perm.parentid != perm_data["parentid"]:
existing_perm.parentid = perm_data["parentid"]
update_needed = True
if existing_perm.route != perm_data.get("route"):
existing_perm.route = perm_data.get("route")
update_needed = True
if existing_perm.description != perm_data["description"]:
existing_perm.description = perm_data["description"]
update_needed = True
if update_needed:
updated_count += 1
if created_count > 0 or updated_count > 0:
await db.commit()
print(f"创建了 {created_count} 个权限,更新了 {updated_count} 个权限")
else:
print("权限数据已存在且无需更新,跳过初始化")
print("权限数据初始化/更新完成")
async def init_role_permissions(db: AsyncSession):
"""初始化角色权限分配"""
from sqlalchemy.orm import selectinload
# 获取管理员角色(使用预加载避免懒加载问题)
result = await db.execute(
select(Role)
.filter(Role.name == "管理员")
.options(selectinload(Role.permissions))
)
admin_role = result.scalar_one_or_none()
if not admin_role:
print("管理员角色不存在,请先初始化角色数据")
return
# 获取所有权限
result = await db.execute(select(Permission))
all_permissions = result.scalars().all()
if not all_permissions:
print("没有找到任何权限,请先初始化权限数据")
return
# 获取管理员角色当前的权限(使用预加载的数据)
admin_permission_codes = set()
if admin_role.permissions:
admin_permission_codes = {perm.code for perm in admin_role.permissions}
# 检查是否需要添加权限
permissions_to_add = []
for perm in all_permissions:
if perm.code not in admin_permission_codes:
permissions_to_add.append(perm)
if permissions_to_add:
# 直接设置权限(替换而非追加,确保权限完整)
admin_role.permissions = all_permissions
await db.commit()
print(f"为管理员角色分配了 {len(permissions_to_add)} 个权限")
else:
print("管理员角色权限已完整,无需更新")
print("角色权限初始化/更新完成")
async def init_roles(db: AsyncSession):
"""初始化角色数据"""
# 定义需要初始化的角色
required_roles = [
{"name": "管理员", "creator": "system"},
{"name": "普通用户", "creator": "system"},
{"name": "访客", "creator": "system"},
{"name": "会员", "creator": "system"},
]
# 检查现有角色
result = await db.execute(select(Role))
existing_roles = result.scalars().all()
existing_role_names = {role.name for role in existing_roles}
# 检查需要创建或更新的角色
roles_to_add = []
roles_to_update = []
for role_data in required_roles:
role_name = role_data["name"]
if role_name not in existing_role_names:
# 角色不存在,需要创建
roles_to_add.append(Role(**role_data))
else:
# 角色存在,检查是否需要更新
existing_role = next(
role for role in existing_roles if role.name == role_name
)
if existing_role.creator != role_data["creator"]:
# 需要更新
existing_role.creator = role_data["creator"]
roles_to_update.append(existing_role)
# 执行创建和更新操作
if roles_to_add:
for role in roles_to_add:
db.add(role)
await db.commit()
print(f"创建了 {len(roles_to_add)} 个角色")
if roles_to_update:
await db.commit()
print(f"更新了 {len(roles_to_update)} 个角色")
if not roles_to_add and not roles_to_update:
print("角色数据已存在且无需更新,跳过初始化")
print("角色数据初始化/更新完成")
async def init_admin_user(db: AsyncSession):
"""初始化管理员用户"""
# 获取管理员角色
result = await db.execute(select(Role).filter(Role.name == "管理员"))
admin_role = result.scalar_one_or_none()
if not admin_role:
print("管理员角色不存在,请先初始化角色数据")
return
# 定义需要的管理员用户数据
admin_data = {
"username": "admin",
"nickname": "系统管理员",
"email": "admin@example.com",
"phone": "13800138000",
"password_hash": get_password_hash("admin"),
"role_id": admin_role.id,
"avatar": "",
"wx_openid": "",
}
# 检查管理员用户是否存在
result = await db.execute(select(User).filter(User.username == "admin"))
existing_admin = result.scalar_one_or_none()
if not existing_admin:
# 管理员不存在,创建
admin_user = User(**admin_data)
db.add(admin_user)
await db.commit()
print("创建了管理员用户")
else:
# 管理员存在,检查是否需要更新
update_needed = False
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
if existing_admin.nickname != admin_data["nickname"]:
existing_admin.nickname = admin_data["nickname"]
update_needed = True
if existing_admin.email != admin_data["email"]:
existing_admin.email = admin_data["email"]
update_needed = True
if existing_admin.phone != admin_data["phone"]:
existing_admin.phone = admin_data["phone"]
update_needed = True
if existing_admin.role_id != admin_data["role_id"]:
existing_admin.role_id = admin_data["role_id"]
update_needed = True
if existing_admin.avatar != admin_data["avatar"]:
existing_admin.avatar = admin_data["avatar"]
update_needed = True
if existing_admin.wx_openid != admin_data["wx_openid"]:
existing_admin.wx_openid = admin_data["wx_openid"]
update_needed = True
if update_needed:
await db.commit()
print("更新了管理员用户")
else:
print("管理员用户已存在且无需更新,跳过初始化")
print("管理员用户初始化/更新完成")
async def init_test_user(db: AsyncSession):
"""初始化测试用户"""
# 获取普通用户角色
result = await db.execute(select(Role).filter(Role.name == "普通用户"))
user_role = result.scalar_one_or_none()
if not user_role:
print("普通用户角色不存在,请先初始化角色数据")
return
# 定义需要的测试用户数据
test_data = {
"username": "test",
"nickname": "测试用户",
"email": "test@example.com",
"phone": "13800138001",
"password_hash": get_password_hash("test"),
"role_id": user_role.id,
"avatar": "",
"wx_openid": "",
}
# 检查测试用户是否存在
result = await db.execute(select(User).filter(User.username == "test"))
existing_test_user = result.scalar_one_or_none()
if not existing_test_user:
# 测试用户不存在,创建
test_user = User(**test_data)
db.add(test_user)
await db.commit()
print("创建了测试用户")
else:
# 测试用户存在,检查是否需要更新
update_needed = False
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
if existing_test_user.nickname != test_data["nickname"]:
existing_test_user.nickname = test_data["nickname"]
update_needed = True
if existing_test_user.email != test_data["email"]:
existing_test_user.email = test_data["email"]
update_needed = True
if existing_test_user.phone != test_data["phone"]:
existing_test_user.phone = test_data["phone"]
update_needed = True
if existing_test_user.role_id != test_data["role_id"]:
existing_test_user.role_id = test_data["role_id"]
update_needed = True
if existing_test_user.avatar != test_data["avatar"]:
existing_test_user.avatar = test_data["avatar"]
update_needed = True
if existing_test_user.wx_openid != test_data["wx_openid"]:
existing_test_user.wx_openid = test_data["wx_openid"]
update_needed = True
if update_needed:
await db.commit()
print("更新了测试用户")
else:
print("测试用户已存在且无需更新,跳过初始化")
print("测试用户初始化/更新完成")
async def init_system_settings(db: AsyncSession):
"""初始化系统设置数据"""
# 定义需要初始化的系统设置
system_settings = [
{
"key": "系统标题",
"value": "图书管理系统",
"type": "string",
"description": "系统显示标题"
},
{
"key": "页脚信息",
"value": "Copyright © 2026 图书管理系统 版权所有 | 技术支持:强蕊科技",
"type": "string",
"description": "页脚信息"
},
{
"key": "ICP信息",
"value": "京ICP备2023000000号",
"type": "string",
"description": "ICP备案信息"
},
{
"key": "启用手机短信注册",
"value": "false",
"type": "boolean",
"description": "是否允许通过手机短信验证码注册"
},
{
"key": "启用邮箱注册",
"value": "false",
"type": "boolean",
"description": "是否允许通过邮箱验证链接注册"
},
{
"key": "系统Logo",
"value": "",
"type": "image",
"description": "系统Logo图片"
}
]
# 检查现有系统设置
result = await db.execute(select(SystemSetting))
existing_settings = result.scalars().all()
existing_setting_keys = {setting.key: setting for setting in existing_settings}
# 创建或更新系统设置
created_count = 0
updated_count = 0
for setting_data in system_settings:
setting_key = setting_data["key"]
if setting_key not in existing_setting_keys:
# 设置不存在,需要创建
db.add(SystemSetting(**setting_data))
created_count += 1
else:
# 设置存在,检查是否需要更新
existing_setting = existing_setting_keys[setting_key]
update_needed = False
if existing_setting.value != setting_data["value"]:
existing_setting.value = setting_data["value"]
update_needed = True
if existing_setting.type != setting_data["type"]:
existing_setting.type = setting_data["type"]
update_needed = True
if existing_setting.description != setting_data.get("description"):
existing_setting.description = setting_data.get("description")
update_needed = True
if update_needed:
updated_count += 1
if created_count > 0 or updated_count > 0:
await db.commit()
print(f"创建了 {created_count} 个系统设置,更新了 {updated_count} 个系统设置")
else:
print("系统设置数据已存在且无需更新,跳过初始化")
print("系统设置数据初始化/更新完成")
async def init_all_data(db: AsyncSession):
"""初始化所有数据"""
print("开始初始化数据库数据...")
await init_roles(db)
await init_permissions(db)
await init_role_permissions(db)
await init_admin_user(db)
await init_test_user(db)
await init_system_settings(db)
print("数据库数据初始化完成")
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text
from models import Role, User, Permission, SystemSetting
from utils.password import get_password_hash
async def init_permissions(db: AsyncSession):
"""初始化权限数据(树形结构)"""
# 定义需要初始化的权限(树形结构)
# parentid: 0 = 顶级模块, >0 = 子权限
permission_tree = [
# ========================================
# 顶级模块 (DB ID: 1,2,3)
# ========================================
{"name": "权限管理", "code": "perm", "parentid": 0, "route": "/admin/permissions", "description": "权限管理模块", "sort": 1},
{"name": "系统设置", "code": "system", "parentid": 0, "route": "/admin/settings", "description": "系统设置模块", "sort": 2},
{"name": "日志管理", "code": "log", "parentid": 0, "route": "/admin/logs", "description": "日志管理模块", "sort": 3},
# ========================================
# 权限管理 -> 二级子模块 (parentid=1, DB ID: 4,5,6)
# ========================================
{"name": "用户管理", "code": "perm:user", "parentid": 1, "route": "/admin/users", "description": "用户管理子模块", "sort": 1},
{"name": "角色管理", "code": "perm:role", "parentid": 1, "route": "/admin/roles", "description": "角色管理子模块", "sort": 2},
{"name": "菜单管理", "code": "perm:menu", "parentid": 1, "route": "/admin/menu", "description": "菜单管理子模块", "sort": 3},
# ========================================
# 用户管理操作 (parentid=4, DB ID: 7-14)
# ========================================
{"name": "查看列表", "code": "perm:user:list", "parentid": 4, "description": "查看用户列表", "sort": 1},
{"name": "查看详情", "code": "perm:user:detail", "parentid": 4, "description": "查看用户详情", "sort": 2},
{"name": "搜索", "code": "perm:user:search", "parentid": 4, "description": "搜索用户", "sort": 3},
{"name": "创建", "code": "perm:user:create", "parentid": 4, "description": "创建新用户", "sort": 4},
{"name": "编辑", "code": "perm:user:edit", "parentid": 4, "description": "编辑用户信息", "sort": 5},
{"name": "删除", "code": "perm:user:delete", "parentid": 4, "description": "删除用户", "sort": 6},
{"name": "导出", "code": "perm:user:export", "parentid": 4, "description": "导出用户数据", "sort": 7},
{"name": "批量操作", "code": "perm:user:batch", "parentid": 4, "description": "批量操作用户", "sort": 8},
# ========================================
# 角色管理操作 (parentid=5, DB ID: 15-22)
# ========================================
{"name": "查看列表", "code": "perm:role:list", "parentid": 5, "description": "查看角色列表", "sort": 1},
{"name": "查看详情", "code": "perm:role:detail", "parentid": 5, "description": "查看角色详情", "sort": 2},
{"name": "搜索", "code": "perm:role:search", "parentid": 5, "description": "搜索角色", "sort": 3},
{"name": "创建", "code": "perm:role:create", "parentid": 5, "description": "创建新角色", "sort": 4},
{"name": "编辑", "code": "perm:role:edit", "parentid": 5, "description": "编辑角色信息", "sort": 5},
{"name": "删除", "code": "perm:role:delete", "parentid": 5, "description": "删除角色", "sort": 6},
{"name": "导出", "code": "perm:role:export", "parentid": 5, "description": "导出角色数据", "sort": 7},
{"name": "分配权限", "code": "perm:role:assign", "parentid": 5, "description": "为角色分配权限", "sort": 8},
# ========================================
# 菜单管理操作 (parentid=6, DB ID: 23-29)
# ========================================
{"name": "查看列表", "code": "perm:menu:list", "parentid": 6, "description": "查看菜单列表", "sort": 1},
{"name": "查看详情", "code": "perm:menu:detail", "parentid": 6, "description": "查看菜单详情", "sort": 2},
{"name": "搜索", "code": "perm:menu:search", "parentid": 6, "description": "搜索菜单", "sort": 3},
{"name": "创建", "code": "perm:menu:create", "parentid": 6, "description": "创建新菜单", "sort": 4},
{"name": "编辑", "code": "perm:menu:edit", "parentid": 6, "description": "编辑菜单信息", "sort": 5},
{"name": "删除", "code": "perm:menu:delete", "parentid": 6, "description": "删除菜单", "sort": 6},
{"name": "导出", "code": "perm:menu:export", "parentid": 6, "description": "导出菜单数据", "sort": 7},
# ========================================
# 系统设置 -> 子权限 (parentid=2, DB ID: 30)
# ========================================
{"name": "系统设置", "code": "system:view", "parentid": 2, "description": "查看和修改系统设置", "sort": 1},
# ========================================
# 日志管理 -> 子模块 (parentid=3, DB ID: 31,32,37)
# ========================================
{"name": "登录日志", "code": "log:login", "parentid": 3, "route": "/admin/login-logs", "description": "登录日志查看", "sort": 1},
{"name": "操作日志", "code": "log:operation", "parentid": 3, "route": "/admin/operation-logs", "description": "操作日志查看", "sort": 2},
{"name": "清理日志", "code": "log:clean", "parentid": 3, "description": "清理旧日志", "sort": 3},
# ========================================
# 登录日志操作 (parentid=31, DB ID: 33,34)
# ========================================
{"name": "查看列表", "code": "log:login:list", "parentid": 31, "description": "查看登录日志列表", "sort": 1},
{"name": "搜索", "code": "log:login:search", "parentid": 31, "description": "搜索登录日志", "sort": 2},
# ========================================
# 操作日志操作 (parentid=32, DB ID: 35,36)
# ========================================
{"name": "查看列表", "code": "log:operation:list", "parentid": 32, "description": "查看操作日志列表", "sort": 1},
{"name": "搜索", "code": "log:operation:search", "parentid": 32, "description": "搜索操作日志", "sort": 2},
]
# 检查现有权限
result = await db.execute(select(Permission))
existing_permissions = result.scalars().all()
existing_permission_codes = {perm.code: perm for perm in existing_permissions}
# 创建或更新权限
created_count = 0
updated_count = 0
for perm_data in permission_tree:
perm_code = perm_data["code"]
if perm_code not in existing_permission_codes:
# 权限不存在,需要创建
db.add(Permission(**perm_data))
created_count += 1
else:
# 权限存在,检查是否需要更新
existing_perm = existing_permission_codes[perm_code]
update_needed = False
if existing_perm.name != perm_data["name"]:
existing_perm.name = perm_data["name"]
update_needed = True
if existing_perm.parentid != perm_data["parentid"]:
existing_perm.parentid = perm_data["parentid"]
update_needed = True
if existing_perm.route != perm_data.get("route"):
existing_perm.route = perm_data.get("route")
update_needed = True
if existing_perm.description != perm_data["description"]:
existing_perm.description = perm_data["description"]
update_needed = True
if existing_perm.sort != perm_data.get("sort", 0):
existing_perm.sort = perm_data.get("sort", 0)
update_needed = True
if update_needed:
updated_count += 1
if created_count > 0 or updated_count > 0:
await db.commit()
print(f"创建了 {created_count} 个权限,更新了 {updated_count} 个权限")
else:
print("权限数据已存在且无需更新,跳过初始化")
print("权限数据初始化/更新完成")
async def init_role_permissions(db: AsyncSession):
"""初始化角色权限分配"""
from sqlalchemy.orm import selectinload
# 获取管理员角色(使用预加载避免懒加载问题)
result = await db.execute(
select(Role)
.filter(Role.name == "管理员")
.options(selectinload(Role.permissions))
)
admin_role = result.scalar_one_or_none()
if not admin_role:
print("管理员角色不存在,请先初始化角色数据")
return
# 获取所有权限
result = await db.execute(select(Permission))
all_permissions = result.scalars().all()
if not all_permissions:
print("没有找到任何权限,请先初始化权限数据")
return
# 获取管理员角色当前的权限(使用预加载的数据)
admin_permission_codes = set()
if admin_role.permissions:
admin_permission_codes = {perm.code for perm in admin_role.permissions}
# 检查是否需要添加权限
permissions_to_add = []
for perm in all_permissions:
if perm.code not in admin_permission_codes:
permissions_to_add.append(perm)
if permissions_to_add:
# 直接设置权限(替换而非追加,确保权限完整)
admin_role.permissions = all_permissions
await db.commit()
print(f"为管理员角色分配了 {len(permissions_to_add)} 个权限")
else:
print("管理员角色权限已完整,无需更新")
print("角色权限初始化/更新完成")
async def init_roles(db: AsyncSession):
"""初始化角色数据"""
# 定义需要初始化的角色
required_roles = [
{"name": "管理员", "creator": "system"},
{"name": "普通用户", "creator": "system"},
{"name": "访客", "creator": "system"},
{"name": "会员", "creator": "system"},
]
# 检查现有角色
result = await db.execute(select(Role))
existing_roles = result.scalars().all()
existing_role_names = {role.name for role in existing_roles}
# 检查需要创建或更新的角色
roles_to_add = []
roles_to_update = []
for role_data in required_roles:
role_name = role_data["name"]
if role_name not in existing_role_names:
# 角色不存在,需要创建
roles_to_add.append(Role(**role_data))
else:
# 角色存在,检查是否需要更新
existing_role = next(
role for role in existing_roles if role.name == role_name
)
if existing_role.creator != role_data["creator"]:
# 需要更新
existing_role.creator = role_data["creator"]
roles_to_update.append(existing_role)
# 执行创建和更新操作
if roles_to_add:
for role in roles_to_add:
db.add(role)
await db.commit()
print(f"创建了 {len(roles_to_add)} 个角色")
if roles_to_update:
await db.commit()
print(f"更新了 {len(roles_to_update)} 个角色")
if not roles_to_add and not roles_to_update:
print("角色数据已存在且无需更新,跳过初始化")
print("角色数据初始化/更新完成")
async def init_admin_user(db: AsyncSession):
"""初始化管理员用户"""
# 获取管理员角色
result = await db.execute(select(Role).filter(Role.name == "管理员"))
admin_role = result.scalar_one_or_none()
if not admin_role:
print("管理员角色不存在,请先初始化角色数据")
return
# 定义需要的管理员用户数据
admin_data = {
"username": "admin",
"nickname": "系统管理员",
"email": "admin@example.com",
"phone": "13800138000",
"password_hash": get_password_hash("admin"),
"role_id": admin_role.id,
"avatar": "",
"wx_openid": "",
}
# 检查管理员用户是否存在
result = await db.execute(select(User).filter(User.username == "admin"))
existing_admin = result.scalar_one_or_none()
if not existing_admin:
# 管理员不存在,创建
admin_user = User(**admin_data)
db.add(admin_user)
await db.commit()
print("创建了管理员用户")
else:
# 管理员存在,检查是否需要更新
update_needed = False
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
if existing_admin.nickname != admin_data["nickname"]:
existing_admin.nickname = admin_data["nickname"]
update_needed = True
if existing_admin.email != admin_data["email"]:
existing_admin.email = admin_data["email"]
update_needed = True
if existing_admin.phone != admin_data["phone"]:
existing_admin.phone = admin_data["phone"]
update_needed = True
if existing_admin.role_id != admin_data["role_id"]:
existing_admin.role_id = admin_data["role_id"]
update_needed = True
if existing_admin.avatar != admin_data["avatar"]:
existing_admin.avatar = admin_data["avatar"]
update_needed = True
if existing_admin.wx_openid != admin_data["wx_openid"]:
existing_admin.wx_openid = admin_data["wx_openid"]
update_needed = True
if update_needed:
await db.commit()
print("更新了管理员用户")
else:
print("管理员用户已存在且无需更新,跳过初始化")
print("管理员用户初始化/更新完成")
async def init_test_user(db: AsyncSession):
"""初始化测试用户"""
# 获取普通用户角色
result = await db.execute(select(Role).filter(Role.name == "普通用户"))
user_role = result.scalar_one_or_none()
if not user_role:
print("普通用户角色不存在,请先初始化角色数据")
return
# 定义需要的测试用户数据
test_data = {
"username": "test",
"nickname": "测试用户",
"email": "test@example.com",
"phone": "13800138001",
"password_hash": get_password_hash("test"),
"role_id": user_role.id,
"avatar": "",
"wx_openid": "",
}
# 检查测试用户是否存在
result = await db.execute(select(User).filter(User.username == "test"))
existing_test_user = result.scalar_one_or_none()
if not existing_test_user:
# 测试用户不存在,创建
test_user = User(**test_data)
db.add(test_user)
await db.commit()
print("创建了测试用户")
else:
# 测试用户存在,检查是否需要更新
update_needed = False
# 检查字段是否需要更新(不包括密码,因为密码只在首次创建时设置)
if existing_test_user.nickname != test_data["nickname"]:
existing_test_user.nickname = test_data["nickname"]
update_needed = True
if existing_test_user.email != test_data["email"]:
existing_test_user.email = test_data["email"]
update_needed = True
if existing_test_user.phone != test_data["phone"]:
existing_test_user.phone = test_data["phone"]
update_needed = True
if existing_test_user.role_id != test_data["role_id"]:
existing_test_user.role_id = test_data["role_id"]
update_needed = True
if existing_test_user.avatar != test_data["avatar"]:
existing_test_user.avatar = test_data["avatar"]
update_needed = True
if existing_test_user.wx_openid != test_data["wx_openid"]:
existing_test_user.wx_openid = test_data["wx_openid"]
update_needed = True
if update_needed:
await db.commit()
print("更新了测试用户")
else:
print("测试用户已存在且无需更新,跳过初始化")
print("测试用户初始化/更新完成")
async def init_system_settings(db: AsyncSession):
"""初始化系统设置数据"""
# 定义需要初始化的系统设置
system_settings = [
{
"key": "系统标题",
"value": "图书管理系统",
"type": "string",
"description": "系统显示标题"
},
{
"key": "页脚信息",
"value": "Copyright © 2026 图书管理系统 版权所有 | 技术支持:强蕊科技",
"type": "string",
"description": "页脚信息"
},
{
"key": "ICP信息",
"value": "京ICP备2023000000号",
"type": "string",
"description": "ICP备案信息"
},
{
"key": "启用手机短信注册",
"value": "false",
"type": "boolean",
"description": "是否允许通过手机短信验证码注册"
},
{
"key": "启用邮箱注册",
"value": "false",
"type": "boolean",
"description": "是否允许通过邮箱验证链接注册"
},
{
"key": "系统Logo",
"value": "",
"type": "image",
"description": "系统Logo图片"
}
]
# 检查现有系统设置
result = await db.execute(select(SystemSetting))
existing_settings = result.scalars().all()
existing_setting_keys = {setting.key: setting for setting in existing_settings}
# 创建或更新系统设置
created_count = 0
updated_count = 0
for setting_data in system_settings:
setting_key = setting_data["key"]
if setting_key not in existing_setting_keys:
# 设置不存在,需要创建
db.add(SystemSetting(**setting_data))
created_count += 1
else:
# 设置存在,检查是否需要更新
existing_setting = existing_setting_keys[setting_key]
update_needed = False
if existing_setting.value != setting_data["value"]:
existing_setting.value = setting_data["value"]
update_needed = True
if existing_setting.type != setting_data["type"]:
existing_setting.type = setting_data["type"]
update_needed = True
if existing_setting.description != setting_data.get("description"):
existing_setting.description = setting_data.get("description")
update_needed = True
if update_needed:
updated_count += 1
if created_count > 0 or updated_count > 0:
await db.commit()
print(f"创建了 {created_count} 个系统设置,更新了 {updated_count} 个系统设置")
else:
print("系统设置数据已存在且无需更新,跳过初始化")
print("系统设置数据初始化/更新完成")
async def init_all_data(db: AsyncSession):
"""初始化所有数据"""
print("开始初始化数据库数据...")
await init_roles(db)
await init_permissions(db)
await init_role_permissions(db)
await init_admin_user(db)
await init_test_user(db)
await init_system_settings(db)
print("数据库数据初始化完成")

View File

@ -1,172 +1,182 @@
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.exceptions import RequestValidationError
from contextlib import asynccontextmanager
from apps.urls import api_router
from apps.settings.urls import UPLOAD_DIR
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
async def lifespan(app: FastAPI):
# 启动时初始化数据库
async with engine.begin() as conn:
# 创建所有表
await conn.run_sync(Base.metadata.create_all)
# 初始化数据
async for db in get_db():
await init_all_data(db)
break
yield
# 关闭时的清理工作
await engine.dispose()
# 实例化FastAPI
app = FastAPI(
title="图书系统授权服务API",
description="""图书系统授权服务API是一套用于用户认证和授权的服务提供用户注册、登录、权限校验等功能。
同时实现了基于角色的访问控制RBAC支持自定义角色和权限还增加了日志记录功能方便监控和调试""",
version="1.0.0",
lifespan=lifespan,
docs_url=None,
redoc_url="/redoc",
)
# 开发环境前后端分离时的跨域(生产环境请收窄 allow_origins
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:3000",
"http://127.0.0.1:3000",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Captcha-ID"],
)
# 挂载swagger-ui静态文件目录
swagger_ui_path = Path(__file__).parent.parent / "swagger-ui"
app.mount("/static", StaticFiles(directory=str(swagger_ui_path)), name="static")
# 上传图片通过自定义端点提供(和 settings/urls.py 中的 UPLOAD_DIR 保持一致)
@app.get("/uploads/{filename}")
async def serve_upload(filename: str):
"""提供上传的图片文件"""
file_path = UPLOAD_DIR / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="文件不存在")
return FileResponse(str(file_path))
# 自定义Swagger UI页面
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
html_content = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book System API - Swagger UI</title>
<link rel="stylesheet" type="text/css" href="/static/swagger-ui.css">
<link rel="icon" type="image/png" href="/static/favicon-32x32.png" sizes="32x32"/>
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="/static/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
persistAuthorization: true,
docExpansion: "list"
});
window.ui = ui;
};
</script>
</body>
</html>
"""
return HTMLResponse(content=html_content)
# 包含路由
app.include_router(api_router)
# 声明装饰器方法和路径
@app.get("/")
# 声明装饰器函数
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__":
import uvicorn
import os
name = f"{os.path.splitext(os.path.basename(os.path.abspath(__file__)))[0]}:app"
uvicorn.run(name, host="0.0.0.0", port=8000, reload=True, reload_dirs=["_"])
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.exceptions import RequestValidationError
from contextlib import asynccontextmanager
from apps.urls import api_router
from apps.settings.urls import UPLOAD_DIR
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
from sqlalchemy import text
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时初始化数据库
async with engine.begin() as conn:
# 创建所有表
await conn.run_sync(Base.metadata.create_all)
# 为已有表添加 sort 列(兼容 PostgreSQL 和 SQLite
try:
await conn.execute(text("ALTER TABLE permission ADD COLUMN IF NOT EXISTS sort INTEGER DEFAULT 0"))
except Exception:
# SQLite 不支持 IF NOT EXISTS
try:
await conn.execute(text("ALTER TABLE permission ADD COLUMN sort INTEGER DEFAULT 0"))
except Exception:
pass # 列已存在则忽略
# 初始化数据
async for db in get_db():
await init_all_data(db)
break
yield
# 关闭时的清理工作
await engine.dispose()
# 实例化FastAPI
app = FastAPI(
title="图书系统授权服务API",
description="""图书系统授权服务API是一套用于用户认证和授权的服务提供用户注册、登录、权限校验等功能。
同时实现了基于角色的访问控制RBAC支持自定义角色和权限还增加了日志记录功能方便监控和调试""",
version="1.0.0",
lifespan=lifespan,
docs_url=None,
redoc_url="/redoc",
)
# 开发环境前后端分离时的跨域(生产环境请收窄 allow_origins
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:3000",
"http://127.0.0.1:3000",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Captcha-ID"],
)
# 挂载swagger-ui静态文件目录
swagger_ui_path = Path(__file__).parent.parent / "swagger-ui"
app.mount("/static", StaticFiles(directory=str(swagger_ui_path)), name="static")
# 上传图片通过自定义端点提供(和 settings/urls.py 中的 UPLOAD_DIR 保持一致)
@app.get("/uploads/{filename}")
async def serve_upload(filename: str):
"""提供上传的图片文件"""
file_path = UPLOAD_DIR / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="文件不存在")
return FileResponse(str(file_path))
# 自定义Swagger UI页面
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
html_content = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book System API - Swagger UI</title>
<link rel="stylesheet" type="text/css" href="/static/swagger-ui.css">
<link rel="icon" type="image/png" href="/static/favicon-32x32.png" sizes="32x32"/>
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="/static/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
persistAuthorization: true,
docExpansion: "list"
});
window.ui = ui;
};
</script>
</body>
</html>
"""
return HTMLResponse(content=html_content)
# 包含路由
app.include_router(api_router)
# 声明装饰器方法和路径
@app.get("/")
# 声明装饰器函数
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__":
import uvicorn
import os
name = f"{os.path.splitext(os.path.basename(os.path.abspath(__file__)))[0]}:app"
uvicorn.run(name, host="0.0.0.0", port=8000, reload=True, reload_dirs=["_"])

View File

@ -1,140 +1,141 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table, Index, Text
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class Role(Base):
"""角色数据库模型"""
__tablename__ = "role"
id = Column(Integer, primary_key=True, index=True, comment="角色ID")
name = Column(String(50), nullable=False, unique=True, comment="角色名称")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
creator = Column(String(50), nullable=True, comment="创建人")
# 关联到User
users = relationship("User", back_populates="role_rel")
# 关联到Permission多对多
permissions = relationship(
"Permission",
secondary="role_permission",
back_populates="roles"
)
class User(Base):
"""用户数据库模型"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, comment="登录名")
nickname = Column(String(50), nullable=False, comment="昵称")
email = Column(String(50), nullable=True, comment="电子邮箱")
phone = Column(String(11), nullable=True, comment="手机号码")
wx_openid = Column(String(100), nullable=True, comment="微信OpenID")
password_hash = Column(String(128), nullable=False, comment="密码哈希值")
avatar = Column(String(255), nullable=True, comment="头像路径")
role_id = Column(Integer, ForeignKey("role.id"), default=2, comment="角色ID")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
lastlogintime = Column(DateTime, comment="最后登录时间")
# 关联到Role
role_rel = relationship("Role", back_populates="users")
class Permission(Base):
"""权限数据库模型"""
__tablename__ = "permission"
id = Column(Integer, primary_key=True, index=True, comment="权限ID")
name = Column(String(100), nullable=False, comment="权限名称")
code = Column(String(100), nullable=False, unique=True, comment="权限编码")
parentid = Column(Integer, default=0, comment="父权限ID0表示顶级模块")
route = Column(String(255), nullable=True, comment="前端路由路径")
description = Column(String(255), nullable=True, comment="权限描述")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
# 关联到Role多对多
roles = relationship(
"Role",
secondary="role_permission",
back_populates="permissions"
)
# 角色权限关联表(多对多)
role_permission = Table(
"role_permission",
Base.metadata,
Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"),
Column("permission_id", Integer, ForeignKey("permission.id"), primary_key=True, comment="权限ID")
)
class SystemSetting(Base):
"""系统设置数据库模型"""
__tablename__ = "system_setting"
id = Column(Integer, primary_key=True, index=True, comment="设置ID")
key = Column(String(100), nullable=False, unique=True, comment="设置键名")
value = Column(String(2000), nullable=False, comment="设置值")
type = Column(String(20), default="string", comment="类型string/image/select/boolean")
options = Column(String(500), nullable=True, comment="下拉选项(用|分隔)")
description = Column(String(255), nullable=True, comment="设置描述")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
class LoginLog(Base):
"""登录日志"""
__tablename__ = "login_log"
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
username = Column(String(50), nullable=False, comment="用户名")
login_time = Column(DateTime, default=datetime.utcnow, comment="登录时间")
ip_address = Column(String(50), nullable=True, comment="IP地址")
user_agent = Column(String(500), nullable=True, comment="User-Agent")
status = Column(String(20), nullable=False, comment="状态: success/failure")
failure_reason = Column(String(255), nullable=True, comment="失败原因")
Index("ix_login_log_login_time", LoginLog.login_time)
Index("ix_login_log_username", LoginLog.username)
Index("ix_login_log_status", LoginLog.status)
class OperationLog(Base):
"""操作日志"""
__tablename__ = "operation_log"
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
user_id = Column(Integer, nullable=False, comment="用户ID")
username = Column(String(50), nullable=False, comment="用户名")
action_type = Column(String(20), nullable=False, comment="操作类型: create/update/delete")
target_type = Column(String(50), nullable=False, comment="目标类型: user/role/permission/setting")
target_id = Column(Integer, nullable=True, comment="目标ID")
target_name = Column(String(100), nullable=True, comment="目标名称")
detail = Column(Text, nullable=True, comment="变更详情(JSON)")
ip_address = Column(String(50), nullable=True, comment="IP地址")
user_agent = Column(String(500), nullable=True, comment="User-Agent")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
Index("ix_operation_log_createtime", OperationLog.createtime)
Index("ix_operation_log_username", OperationLog.username)
Index("ix_operation_log_action_type", OperationLog.action_type)
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table, Index, Text
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class Role(Base):
"""角色数据库模型"""
__tablename__ = "role"
id = Column(Integer, primary_key=True, index=True, comment="角色ID")
name = Column(String(50), nullable=False, unique=True, comment="角色名称")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
creator = Column(String(50), nullable=True, comment="创建人")
# 关联到User
users = relationship("User", back_populates="role_rel")
# 关联到Permission多对多
permissions = relationship(
"Permission",
secondary="role_permission",
back_populates="roles"
)
class User(Base):
"""用户数据库模型"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, comment="登录名")
nickname = Column(String(50), nullable=False, comment="昵称")
email = Column(String(50), nullable=True, comment="电子邮箱")
phone = Column(String(11), nullable=True, comment="手机号码")
wx_openid = Column(String(100), nullable=True, comment="微信OpenID")
password_hash = Column(String(128), nullable=False, comment="密码哈希值")
avatar = Column(String(255), nullable=True, comment="头像路径")
role_id = Column(Integer, ForeignKey("role.id"), default=2, comment="角色ID")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
lastlogintime = Column(DateTime, comment="最后登录时间")
# 关联到Role
role_rel = relationship("Role", back_populates="users")
class Permission(Base):
"""权限数据库模型"""
__tablename__ = "permission"
id = Column(Integer, primary_key=True, index=True, comment="权限ID")
name = Column(String(100), nullable=False, comment="权限名称")
code = Column(String(100), nullable=False, unique=True, comment="权限编码")
parentid = Column(Integer, default=0, comment="父权限ID0表示顶级模块")
route = Column(String(255), nullable=True, comment="前端路由路径")
description = Column(String(255), nullable=True, comment="权限描述")
sort = Column(Integer, default=0, comment="排序号(升序排列)")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
# 关联到Role多对多
roles = relationship(
"Role",
secondary="role_permission",
back_populates="permissions"
)
# 角色权限关联表(多对多)
role_permission = Table(
"role_permission",
Base.metadata,
Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"),
Column("permission_id", Integer, ForeignKey("permission.id"), primary_key=True, comment="权限ID")
)
class SystemSetting(Base):
"""系统设置数据库模型"""
__tablename__ = "system_setting"
id = Column(Integer, primary_key=True, index=True, comment="设置ID")
key = Column(String(100), nullable=False, unique=True, comment="设置键名")
value = Column(String(2000), nullable=False, comment="设置值")
type = Column(String(20), default="string", comment="类型string/image/select/boolean")
options = Column(String(500), nullable=True, comment="下拉选项(用|分隔)")
description = Column(String(255), nullable=True, comment="设置描述")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
updatetime = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
class LoginLog(Base):
"""登录日志"""
__tablename__ = "login_log"
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
username = Column(String(50), nullable=False, comment="用户名")
login_time = Column(DateTime, default=datetime.utcnow, comment="登录时间")
ip_address = Column(String(50), nullable=True, comment="IP地址")
user_agent = Column(String(500), nullable=True, comment="User-Agent")
status = Column(String(20), nullable=False, comment="状态: success/failure")
failure_reason = Column(String(255), nullable=True, comment="失败原因")
Index("ix_login_log_login_time", LoginLog.login_time)
Index("ix_login_log_username", LoginLog.username)
Index("ix_login_log_status", LoginLog.status)
class OperationLog(Base):
"""操作日志"""
__tablename__ = "operation_log"
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
user_id = Column(Integer, nullable=False, comment="用户ID")
username = Column(String(50), nullable=False, comment="用户名")
action_type = Column(String(20), nullable=False, comment="操作类型: create/update/delete")
target_type = Column(String(50), nullable=False, comment="目标类型: user/role/permission/setting")
target_id = Column(Integer, nullable=True, comment="目标ID")
target_name = Column(String(100), nullable=True, comment="目标名称")
detail = Column(Text, nullable=True, comment="变更详情(JSON)")
ip_address = Column(String(50), nullable=True, comment="IP地址")
user_agent = Column(String(500), nullable=True, comment="User-Agent")
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
Index("ix_operation_log_createtime", OperationLog.createtime)
Index("ix_operation_log_username", OperationLog.username)
Index("ix_operation_log_action_type", OperationLog.action_type)
Index("ix_operation_log_target_type", OperationLog.target_type)

View File

@ -1,335 +1,337 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
"""用户基础模型"""
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[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: Optional[int] = Field(2, description="角色ID")
model_config = {"extra": "ignore"}
class UserCreateRequest(UserBase):
"""用户创建请求模型"""
password: str = Field(
...,
min_length=6,
max_length=50,
description="密码",
json_schema_extra={
"error_messages": {
"min_length": "密码长度至少为6个字符",
"max_length": "密码长度不能超过50个字符"
}
}
)
class UserCreate(UserBase):
"""用户创建模型"""
password_hash: str = Field(
..., min_length=6, max_length=128, description="密码哈希值"
)
class UserUpdate(BaseModel):
"""用户更新模型"""
username: str = Field(..., min_length=3, max_length=50, description="登录名")
nickname: str = Field(..., 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: Optional[int] = Field(2, description="角色ID")
password: Optional[str] = Field(None, max_length=128, description="密码")
class UserLogin(BaseModel):
"""用户登录模型"""
username: str = Field(..., description="用户名/邮箱/手机号")
password: str = Field(..., description="密码")
captcha_id: str = Field(..., description="验证码ID")
captcha_code: str = Field(..., description="验证码")
class UserResponse(BaseModel):
"""用户响应模型"""
id: int = Field(..., description="用户ID")
username: str = Field(..., description="登录名")
nickname: str = Field(..., 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="头像路径")
role_id: int = Field(..., description="角色ID")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
lastlogintime: Optional[datetime] = Field(None, description="最后登录时间")
class Config:
from_attributes = True
class Token(BaseModel):
"""令牌模型"""
access_token: str = Field(..., description="访问令牌")
token_type: str = Field(..., description="令牌类型")
class TokenData(BaseModel):
"""令牌数据模型"""
user_id: Optional[int] = None
email: Optional[str] = None
class RoleBase(BaseModel):
"""角色基础模型"""
name: str = Field(..., max_length=50, description="角色名称")
class RoleCreate(RoleBase):
"""角色创建模型"""
creator: Optional[str] = Field(None, max_length=50, description="创建人")
class RoleResponse(RoleBase):
"""角色响应模型"""
id: int = Field(..., description="角色ID")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
creator: Optional[str] = Field(None, description="创建人")
class Config:
from_attributes = True
# 权限相关模型
class PermissionBase(BaseModel):
"""权限基础模型"""
name: str = Field(..., max_length=100, description="权限名称")
code: str = Field(..., max_length=100, description="权限编码")
parentid: int = Field(0, description="父权限ID0表示顶级模块")
route: Optional[str] = Field(None, max_length=255, description="前端路由路径")
description: Optional[str] = Field(None, max_length=255, description="权限描述")
class PermissionCreate(PermissionBase):
"""权限创建模型"""
pass
class PermissionResponse(PermissionBase):
"""权限响应模型"""
id: int = Field(..., description="权限ID")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
class Config:
from_attributes = True
class PermissionTreeResponse(BaseModel):
"""权限树响应模型"""
id: int = Field(..., description="权限ID")
name: str = Field(..., description="权限名称")
code: str = Field(..., description="权限编码")
parentid: int = Field(..., description="父权限ID")
route: Optional[str] = Field(None, description="前端路由路径")
createtime: Optional[datetime] = Field(None, description="创建时间")
updatetime: Optional[datetime] = Field(None, description="更新时间")
description: Optional[str] = Field(None, description="权限描述")
children: list = Field([], description="子权限列表")
# 角色权限关联相关模型
class RolePermissionAssign(BaseModel):
"""角色权限分配模型"""
role_id: int = Field(..., description="角色ID")
permission_ids: list[int] = Field(..., description="权限ID列表")
class RolePermissionResponse(BaseModel):
"""角色权限响应模型"""
role_id: int = Field(..., description="角色ID")
role_name: str = Field(..., description="角色名称")
permissions: list[PermissionResponse] = Field(..., description="权限列表")
menu: list = Field([], description="菜单树形结构")
# 系统设置相关模型
class SystemSettingBase(BaseModel):
"""系统设置基础模型"""
key: str = Field(..., max_length=100, description="设置键名")
value: str = Field(..., max_length=2000, description="设置值")
type: Optional[str] = Field("string", max_length=20, description="类型string/image/select/boolean")
options: Optional[str] = Field(None, max_length=500, description="下拉选项(用|分隔)")
description: Optional[str] = Field(None, max_length=255, description="设置描述")
model_config = {"extra": "ignore"}
class SystemSettingCreate(SystemSettingBase):
"""系统设置创建模型"""
pass
class SystemSettingUpdate(SystemSettingBase):
"""系统设置更新模型"""
key: Optional[str] = Field(None, max_length=100, description="设置键名")
class SystemSettingResponse(BaseModel):
"""系统设置响应模型"""
id: int = Field(..., description="设置ID")
key: str = Field(..., description="设置键名")
value: str = Field(..., description="设置值")
type: str = Field(..., description="类型")
options: Optional[str] = Field(None, description="下拉选项")
description: Optional[str] = Field(None, description="设置描述")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
class Config:
from_attributes = True
class LoginLogResponse(BaseModel):
"""登录日志响应模型"""
id: int = Field(..., description="日志ID")
username: str = Field(..., description="用户名")
login_time: datetime = Field(..., description="登录时间")
ip_address: Optional[str] = Field(None, description="IP地址")
user_agent: Optional[str] = Field(None, description="User-Agent")
status: str = Field(..., description="状态: success/failure")
failure_reason: Optional[str] = Field(None, description="失败原因")
class Config:
from_attributes = True
class LoginLogSearch(BaseModel):
"""登录日志搜索模型"""
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
status: Optional[str] = Field(None, description="状态")
start_time: Optional[datetime] = Field(None, description="开始时间")
end_time: Optional[datetime] = Field(None, description="结束时间")
class LoginLogCleanRequest(BaseModel):
"""登录日志清理请求模型"""
before_date: datetime = Field(..., description="清理此日期之前的日志")
class OperationLogResponse(BaseModel):
"""操作日志响应模型"""
id: int = Field(..., description="日志ID")
user_id: int = Field(..., description="用户ID")
username: str = Field(..., description="用户名")
action_type: str = Field(..., description="操作类型: create/update/delete")
target_type: str = Field(..., description="目标类型: user/role/permission/setting")
target_id: Optional[int] = Field(None, description="目标ID")
target_name: Optional[str] = Field(None, description="目标名称")
detail: Optional[str] = Field(None, description="变更详情(JSON)")
ip_address: Optional[str] = Field(None, description="IP地址")
user_agent: Optional[str] = Field(None, description="User-Agent")
createtime: datetime = Field(..., description="创建时间")
class Config:
from_attributes = True
class OperationLogSearch(BaseModel):
"""操作日志搜索模型"""
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
action_type: Optional[str] = Field(None, description="操作类型")
target_type: Optional[str] = Field(None, description="目标类型")
start_time: Optional[datetime] = Field(None, description="开始时间")
end_time: Optional[datetime] = Field(None, description="结束时间")
# ========================================
# 用户注册相关模型
# ========================================
class PhoneSendCodeRequest(BaseModel):
"""发送短信验证码请求"""
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号11位数字")
class PhoneSendCodeResponse(BaseModel):
"""发送短信验证码响应"""
message: str = "验证码已发送"
expire_seconds: int = 300
class PhoneRegisterRequest(BaseModel):
"""手机号注册请求"""
username: str = Field(..., min_length=3, max_length=50, description="用户名")
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号")
code: str = Field(..., min_length=6, max_length=6, description="短信验证码")
password: str = Field(..., min_length=6, max_length=50, description="密码")
nickname: str = Field(..., max_length=50, description="昵称")
class EmailRegisterInitRequest(BaseModel):
"""邮箱注册初始化请求"""
username: str = Field(..., min_length=3, max_length=50, description="用户名")
email: str = Field(..., max_length=50, description="电子邮箱")
password: str = Field(..., min_length=6, max_length=50, description="密码")
nickname: str = Field(..., max_length=50, description="昵称")
class EmailRegisterInitResponse(BaseModel):
"""邮箱注册初始化响应"""
message: str = "验证邮件已发送,请查收并点击链接完成注册"
expire_minutes: int = 30
class RegisterSuccessResponse(BaseModel):
"""注册成功响应"""
id: int
username: str
nickname: str
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
"""用户基础模型"""
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[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: Optional[int] = Field(2, description="角色ID")
model_config = {"extra": "ignore"}
class UserCreateRequest(UserBase):
"""用户创建请求模型"""
password: str = Field(
...,
min_length=6,
max_length=50,
description="密码",
json_schema_extra={
"error_messages": {
"min_length": "密码长度至少为6个字符",
"max_length": "密码长度不能超过50个字符"
}
}
)
class UserCreate(UserBase):
"""用户创建模型"""
password_hash: str = Field(
..., min_length=6, max_length=128, description="密码哈希值"
)
class UserUpdate(BaseModel):
"""用户更新模型"""
username: str = Field(..., min_length=3, max_length=50, description="登录名")
nickname: str = Field(..., 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: Optional[int] = Field(2, description="角色ID")
password: Optional[str] = Field(None, max_length=128, description="密码")
class UserLogin(BaseModel):
"""用户登录模型"""
username: str = Field(..., description="用户名/邮箱/手机号")
password: str = Field(..., description="密码")
captcha_id: str = Field(..., description="验证码ID")
captcha_code: str = Field(..., description="验证码")
class UserResponse(BaseModel):
"""用户响应模型"""
id: int = Field(..., description="用户ID")
username: str = Field(..., description="登录名")
nickname: str = Field(..., 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="头像路径")
role_id: int = Field(..., description="角色ID")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
lastlogintime: Optional[datetime] = Field(None, description="最后登录时间")
class Config:
from_attributes = True
class Token(BaseModel):
"""令牌模型"""
access_token: str = Field(..., description="访问令牌")
token_type: str = Field(..., description="令牌类型")
class TokenData(BaseModel):
"""令牌数据模型"""
user_id: Optional[int] = None
email: Optional[str] = None
class RoleBase(BaseModel):
"""角色基础模型"""
name: str = Field(..., max_length=50, description="角色名称")
class RoleCreate(RoleBase):
"""角色创建模型"""
creator: Optional[str] = Field(None, max_length=50, description="创建人")
class RoleResponse(RoleBase):
"""角色响应模型"""
id: int = Field(..., description="角色ID")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
creator: Optional[str] = Field(None, description="创建人")
class Config:
from_attributes = True
# 权限相关模型
class PermissionBase(BaseModel):
"""权限基础模型"""
name: str = Field(..., max_length=100, description="权限名称")
code: str = Field(..., max_length=100, description="权限编码")
parentid: int = Field(0, description="父权限ID0表示顶级模块")
route: Optional[str] = Field(None, max_length=255, description="前端路由路径")
description: Optional[str] = Field(None, max_length=255, description="权限描述")
sort: int = Field(0, description="排序号(升序排列)")
class PermissionCreate(PermissionBase):
"""权限创建模型"""
pass
class PermissionResponse(PermissionBase):
"""权限响应模型"""
id: int = Field(..., description="权限ID")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
class Config:
from_attributes = True
class PermissionTreeResponse(BaseModel):
"""权限树响应模型"""
id: int = Field(..., description="权限ID")
name: str = Field(..., description="权限名称")
code: str = Field(..., description="权限编码")
parentid: int = Field(..., description="父权限ID")
route: Optional[str] = Field(None, description="前端路由路径")
createtime: Optional[datetime] = Field(None, description="创建时间")
updatetime: Optional[datetime] = Field(None, description="更新时间")
description: Optional[str] = Field(None, description="权限描述")
sort: int = Field(0, description="排序号")
children: list = Field([], description="子权限列表")
# 角色权限关联相关模型
class RolePermissionAssign(BaseModel):
"""角色权限分配模型"""
role_id: int = Field(..., description="角色ID")
permission_ids: list[int] = Field(..., description="权限ID列表")
class RolePermissionResponse(BaseModel):
"""角色权限响应模型"""
role_id: int = Field(..., description="角色ID")
role_name: str = Field(..., description="角色名称")
permissions: list[PermissionResponse] = Field(..., description="权限列表")
menu: list = Field([], description="菜单树形结构")
# 系统设置相关模型
class SystemSettingBase(BaseModel):
"""系统设置基础模型"""
key: str = Field(..., max_length=100, description="设置键名")
value: str = Field(..., max_length=2000, description="设置值")
type: Optional[str] = Field("string", max_length=20, description="类型string/image/select/boolean")
options: Optional[str] = Field(None, max_length=500, description="下拉选项(用|分隔)")
description: Optional[str] = Field(None, max_length=255, description="设置描述")
model_config = {"extra": "ignore"}
class SystemSettingCreate(SystemSettingBase):
"""系统设置创建模型"""
pass
class SystemSettingUpdate(SystemSettingBase):
"""系统设置更新模型"""
key: Optional[str] = Field(None, max_length=100, description="设置键名")
class SystemSettingResponse(BaseModel):
"""系统设置响应模型"""
id: int = Field(..., description="设置ID")
key: str = Field(..., description="设置键名")
value: str = Field(..., description="设置值")
type: str = Field(..., description="类型")
options: Optional[str] = Field(None, description="下拉选项")
description: Optional[str] = Field(None, description="设置描述")
createtime: datetime = Field(..., description="创建时间")
updatetime: datetime = Field(..., description="更新时间")
class Config:
from_attributes = True
class LoginLogResponse(BaseModel):
"""登录日志响应模型"""
id: int = Field(..., description="日志ID")
username: str = Field(..., description="用户名")
login_time: datetime = Field(..., description="登录时间")
ip_address: Optional[str] = Field(None, description="IP地址")
user_agent: Optional[str] = Field(None, description="User-Agent")
status: str = Field(..., description="状态: success/failure")
failure_reason: Optional[str] = Field(None, description="失败原因")
class Config:
from_attributes = True
class LoginLogSearch(BaseModel):
"""登录日志搜索模型"""
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
status: Optional[str] = Field(None, description="状态")
start_time: Optional[datetime] = Field(None, description="开始时间")
end_time: Optional[datetime] = Field(None, description="结束时间")
class LoginLogCleanRequest(BaseModel):
"""登录日志清理请求模型"""
before_date: datetime = Field(..., description="清理此日期之前的日志")
class OperationLogResponse(BaseModel):
"""操作日志响应模型"""
id: int = Field(..., description="日志ID")
user_id: int = Field(..., description="用户ID")
username: str = Field(..., description="用户名")
action_type: str = Field(..., description="操作类型: create/update/delete")
target_type: str = Field(..., description="目标类型: user/role/permission/setting")
target_id: Optional[int] = Field(None, description="目标ID")
target_name: Optional[str] = Field(None, description="目标名称")
detail: Optional[str] = Field(None, description="变更详情(JSON)")
ip_address: Optional[str] = Field(None, description="IP地址")
user_agent: Optional[str] = Field(None, description="User-Agent")
createtime: datetime = Field(..., description="创建时间")
class Config:
from_attributes = True
class OperationLogSearch(BaseModel):
"""操作日志搜索模型"""
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
action_type: Optional[str] = Field(None, description="操作类型")
target_type: Optional[str] = Field(None, description="目标类型")
start_time: Optional[datetime] = Field(None, description="开始时间")
end_time: Optional[datetime] = Field(None, description="结束时间")
# ========================================
# 用户注册相关模型
# ========================================
class PhoneSendCodeRequest(BaseModel):
"""发送短信验证码请求"""
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号11位数字")
class PhoneSendCodeResponse(BaseModel):
"""发送短信验证码响应"""
message: str = "验证码已发送"
expire_seconds: int = 300
class PhoneRegisterRequest(BaseModel):
"""手机号注册请求"""
username: str = Field(..., min_length=3, max_length=50, description="用户名")
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号")
code: str = Field(..., min_length=6, max_length=6, description="短信验证码")
password: str = Field(..., min_length=6, max_length=50, description="密码")
nickname: str = Field(..., max_length=50, description="昵称")
class EmailRegisterInitRequest(BaseModel):
"""邮箱注册初始化请求"""
username: str = Field(..., min_length=3, max_length=50, description="用户名")
email: str = Field(..., max_length=50, description="电子邮箱")
password: str = Field(..., min_length=6, max_length=50, description="密码")
nickname: str = Field(..., max_length=50, description="昵称")
class EmailRegisterInitResponse(BaseModel):
"""邮箱注册初始化响应"""
message: str = "验证邮件已发送,请查收并点击链接完成注册"
expire_minutes: int = 30
class RegisterSuccessResponse(BaseModel):
"""注册成功响应"""
id: int
username: str
nickname: str
message: str = "注册成功"

View File

@ -1,176 +1,188 @@
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import { http } from "@/modules/admin/api/http";
import { toSearchParams } from "@/modules/admin/utils/list-query";
export type PermissionRow = {
id: number;
name: string;
code: string;
parentid: number;
route?: string | null;
description?: string | null;
createtime: string;
updatetime: string;
children?: PermissionRow[];
};
export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = () => {
return {
crudOptions: {
pagination: { show: false },
search: {
show: true,
options: {
layout: 'inline',
},
expand: false,
},
tree: {
enable: true,
childrenKey: "children",
parentKey: "parentid",
hasChildField: "children",
},
request: {
transformQuery: ({ form }) =>
toSearchParams(form as Record<string, unknown>, ["name", "code"]),
pageRequest: async (_query) => {
const { data } = await http.get<PermissionRow[]>("/permissions/tree");
return data;
},
transformRes: ({ res }) => {
const records = Array.isArray(res) ? res : [];
return {
currentPage: 1,
pageSize: Math.max(records.length, 1),
total: records.length,
records,
};
},
addRequest: async ({ form }) => {
await http.post("/permissions/", form);
ElMessage.success({ message: "添加成功", duration: 5000 });
},
editRequest: async ({ form, row }) => {
if (!row) return;
await http.put(`/permissions/${row.id}`, form);
ElMessage.success({ message: "修改成功", duration: 5000 });
},
delRequest: async ({ row }) => {
if (!row) return;
await http.delete(`/permissions/${row.id}`);
ElMessage.success({ message: "删除成功", duration: 5000 });
},
onSuccess: ({ action }: { action: string }) => {
if (action === "add") {
ElMessage.success({ message: "添加成功", duration: 5000 });
} else if (action === "edit") {
ElMessage.success({ message: "修改成功", duration: 5000 });
}
},
onError: (error: any) => {
let message = "操作失败";
if (error.response?.data) {
const data = error.response.data;
if (data.message) {
message = data.message;
} else if (data.detail) {
message = typeof data.detail === "string" ? data.detail : "操作失败";
}
}
ElMessage.error({ message, duration: 5000 });
},
},
table: {
remove: {
showSuccessNotification: false,
},
},
rowHandle: {
buttons: {
edit: { text: "编辑" },
remove: { text: "删除" },
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: { width: 70 },
form: { show: false },
},
name: {
title: "名称",
key: "name",
type: "text",
search: { show: true },
form: {
rules: [{ required: true, message: "必填" }],
},
},
code: {
title: "编码",
key: "code",
type: "text",
search: { show: true },
form: {
rules: [{ required: true, message: "必填" }],
},
},
parentid: {
title: "父级ID",
key: "parentid",
type: "number",
form: {
value: 0,
rules: [{ required: true, message: "必填" }],
},
},
description: {
title: "描述",
key: "description",
type: "textarea",
column: { show: false },
},
route: {
title: "路由",
key: "route",
type: "text",
column: { width: 200 },
form: {
placeholder: "如:/admin/users",
},
},
createtime: {
title: "创建时间",
key: "createtime",
type: "text",
form: { show: false },
column: { width: 170 },
valueBuilder({ value, row, key }) {
if (!row || value == null) return;
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
"YYYY-MM-DD HH:mm:ss",
);
},
},
updatetime: {
title: "更新时间",
key: "updatetime",
type: "text",
form: { show: false },
column: { width: 170 },
valueBuilder({ value, row, key }) {
if (!row || value == null) return;
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
"YYYY-MM-DD HH:mm:ss",
);
},
},
},
},
};
};
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import { http } from "@/modules/admin/api/http";
import { toSearchParams } from "@/modules/admin/utils/list-query";
export type PermissionRow = {
id: number;
name: string;
code: string;
parentid: number;
route?: string | null;
description?: string | null;
sort: number;
createtime: string;
updatetime: string;
children?: PermissionRow[];
};
export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = () => {
return {
crudOptions: {
pagination: { show: false },
search: {
show: true,
options: {
layout: 'inline',
},
expand: false,
},
tree: {
enable: true,
childrenKey: "children",
parentKey: "parentid",
hasChildField: "children",
},
request: {
transformQuery: ({ form }) =>
toSearchParams(form as Record<string, unknown>, ["name", "code"]),
pageRequest: async (_query) => {
const { data } = await http.get<PermissionRow[]>("/permissions/tree");
return data;
},
transformRes: ({ res }) => {
const records = Array.isArray(res) ? res : [];
return {
currentPage: 1,
pageSize: Math.max(records.length, 1),
total: records.length,
records,
};
},
addRequest: async ({ form }) => {
await http.post("/permissions/", form);
ElMessage.success({ message: "添加成功", duration: 5000 });
},
editRequest: async ({ form, row }) => {
if (!row) return;
await http.put(`/permissions/${row.id}`, form);
ElMessage.success({ message: "修改成功", duration: 5000 });
},
delRequest: async ({ row }) => {
if (!row) return;
await http.delete(`/permissions/${row.id}`);
ElMessage.success({ message: "删除成功", duration: 5000 });
},
onSuccess: ({ action }: { action: string }) => {
if (action === "add") {
ElMessage.success({ message: "添加成功", duration: 5000 });
} else if (action === "edit") {
ElMessage.success({ message: "修改成功", duration: 5000 });
}
},
onError: (error: any) => {
let message = "操作失败";
if (error.response?.data) {
const data = error.response.data;
if (data.message) {
message = data.message;
} else if (data.detail) {
message = typeof data.detail === "string" ? data.detail : "操作失败";
}
}
ElMessage.error({ message, duration: 5000 });
},
},
table: {
remove: {
showSuccessNotification: false,
},
},
rowHandle: {
buttons: {
edit: { text: "编辑" },
remove: { text: "删除" },
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: { width: 70 },
form: { show: false },
},
name: {
title: "名称",
key: "name",
type: "text",
search: { show: true },
form: {
rules: [{ required: true, message: "必填" }],
},
},
code: {
title: "编码",
key: "code",
type: "text",
search: { show: true },
form: {
rules: [{ required: true, message: "必填" }],
},
},
parentid: {
title: "父级ID",
key: "parentid",
type: "number",
form: {
value: 0,
rules: [{ required: true, message: "必填" }],
},
},
description: {
title: "描述",
key: "description",
type: "textarea",
column: { show: false },
},
route: {
title: "路由",
key: "route",
type: "text",
column: { width: 200 },
form: {
placeholder: "如:/admin/users",
},
},
sort: {
title: "排序",
key: "sort",
type: "number",
column: { width: 80, align: "center" },
form: {
value: 0,
rules: [{ required: true, message: "必填" }],
helper: "数字越小,排序越靠前",
},
},
createtime: {
title: "创建时间",
key: "createtime",
type: "text",
form: { show: false },
column: { width: 170 },
valueBuilder({ value, row, key }) {
if (!row || value == null) return;
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
"YYYY-MM-DD HH:mm:ss",
);
},
},
updatetime: {
title: "更新时间",
key: "updatetime",
type: "text",
form: { show: false },
column: { width: 170 },
valueBuilder({ value, row, key }) {
if (!row || value == null) return;
(row as Record<string, unknown>)[key as string] = dayjs(value).format(
"YYYY-MM-DD HH:mm:ss",
);
},
},
},
},
};
};