From 9586c7467aeab14ed839c07319cd057043180009 Mon Sep 17 00:00:00 2001 From: jayhgq Date: Mon, 20 Jul 2026 08:33:51 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=8F=9C=E5=8D=95=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Permission 模型新增 sort 字段 - 后端 API 按 sort 升序排列树形菜单 - 前端菜单管理页面增加排序列 - 初始数据添加排序号 - 修复 GET /permissions/{id} 详情接口 bug Co-Authored-By: Claude Opus 4.8 --- backend/auth/apps/permissions/urls.py | 39 +- backend/auth/init_data.py | 903 +++++++++--------- backend/auth/main.py | 354 +++---- backend/auth/models.py | 279 +++--- backend/auth/schemas.py | 670 ++++++------- .../views/permissions/permission-crud.ts | 364 +++---- 6 files changed, 1334 insertions(+), 1275 deletions(-) diff --git a/backend/auth/apps/permissions/urls.py b/backend/auth/apps/permissions/urls.py index f9fec4e..30acde6 100644 --- a/backend/auth/apps/permissions/urls.py +++ b/backend/auth/apps/permissions/urls.py @@ -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", diff --git a/backend/auth/init_data.py b/backend/auth/init_data.py index a6db11e..85a0096 100644 --- a/backend/auth/init_data.py +++ b/backend/auth/init_data.py @@ -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("数据库数据初始化完成") diff --git a/backend/auth/main.py b/backend/auth/main.py index 4180034..e6f03d7 100644 --- a/backend/auth/main.py +++ b/backend/auth/main.py @@ -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 = """ - - - - - - Book System API - Swagger UI - - - - - -
- - - - - """ - 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 = """ + + + + + + Book System API - Swagger UI + + + + + +
+ + + + + """ + 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=["_"]) diff --git a/backend/auth/models.py b/backend/auth/models.py index ccae4e4..60c5049 100644 --- a/backend/auth/models.py +++ b/backend/auth/models.py @@ -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="父权限ID(0表示顶级模块)") - 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="父权限ID(0表示顶级模块)") + 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) \ No newline at end of file diff --git a/backend/auth/schemas.py b/backend/auth/schemas.py index f94a2f7..a6ca708 100644 --- a/backend/auth/schemas.py +++ b/backend/auth/schemas.py @@ -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="父权限ID(0表示顶级模块)") - 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="父权限ID(0表示顶级模块)") + 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 = "注册成功" \ No newline at end of file diff --git a/frontend/src/modules/admin/views/permissions/permission-crud.ts b/frontend/src/modules/admin/views/permissions/permission-crud.ts index 66603d1..b3fe497 100644 --- a/frontend/src/modules/admin/views/permissions/permission-crud.ts +++ b/frontend/src/modules/admin/views/permissions/permission-crud.ts @@ -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 = () => { - 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, ["name", "code"]), - pageRequest: async (_query) => { - const { data } = await http.get("/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)[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)[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 = () => { + 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, ["name", "code"]), + pageRequest: async (_query) => { + const { data } = await http.get("/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)[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)[key as string] = dayjs(value).format( + "YYYY-MM-DD HH:mm:ss", + ); + }, + }, + }, + }, + }; +};