From 2b252e1b3cb5f411201641eb0b32e2927e8f2cad Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 30 Oct 2025 16:40:56 +0800 Subject: [PATCH] =?UTF-8?q?serv\web-=20=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.bin/mysql/sql/initMenuData.sql | 6 +- .../achievement/mapper/AchievementMapper.java | 15 +- .../impl/ACHAchievementServiceImpl.java | 7 +- .../resources/mapper/AchievementMapper.xml | 35 +- .../src/main/resources/log4j2-spring.xml | 11 + .../api/system/dept/DepartmentService.java | 7 +- .../xyzh/api/system/log/LoginLogService.java | 16 + .../api/system/log/OperationLogService.java | 35 + .../org/xyzh/api/system/menu/MenuService.java | 7 +- .../org/xyzh/api/system/role/RoleService.java | 15 +- .../org/xyzh/api/system/user/UserService.java | 21 + .../xyzh/auth/service/LoginLogService.java | 69 -- .../xyzh/auth/service/LoginServiceImpl.java | 45 +- .../strategy/impl/PasswordLoginStrategy.java | 7 +- .../xyzh/common/dto/system/TbSysLoginLog.java | 8 +- .../java/org/xyzh/common/vo/PermissionVO.java | 120 ++ .../java/org/xyzh/common/vo/TaskItemVO.java | 22 +- .../main/java/org/xyzh/common/vo/UserVO.java | 17 + .../org/xyzh/common/utils/ServletUtil.java | 189 ---- .../org/xyzh/common/utils/ServletUtils.java | 673 +++++------ .../utils/spring/SpringContextUtil.java | 64 ++ .../crontab/mapper/CrontabTaskMapper.java | 29 +- .../scheduler/SchedulerInitializer.java | 4 +- .../service/impl/CrontabServiceImpl.java | 8 +- .../resources/mapper/CrontabTaskMapper.xml | 125 +- .../org/xyzh/news/mapper/BannerMapper.java | 15 +- .../org/xyzh/news/mapper/ResourceMapper.java | 35 +- .../java/org/xyzh/news/mapper/TagMapper.java | 15 +- .../service/impl/NCBannerServiceImpl.java | 10 +- .../service/impl/NCResourceServiceImpl.java | 10 +- .../news/service/impl/NCTagServiceImpl.java | 4 +- .../main/resources/mapper/BannerMapper.xml | 39 +- .../main/resources/mapper/ResourceMapper.xml | 87 +- .../src/main/resources/mapper/TagMapper.xml | 35 +- .../org/xyzh/study/mapper/CourseMapper.java | 40 +- .../xyzh/study/mapper/LearningTaskMapper.java | 15 +- .../main/resources/mapper/CourseMapper.xml | 99 +- .../resources/mapper/LearningTaskMapper.xml | 47 +- .../main/resources/mapper/TaskUserMapper.xml | 42 +- .../xyzh/system/config/AsyncLogConfig.java | 58 + .../system/controller/DeptController.java | 6 +- .../xyzh/system/controller/LogController.java | 58 + .../system/controller/MenuController.java | 9 +- .../system/controller/RoleController.java | 15 +- .../system/controller/UserController.java | 9 +- .../impl/SysDepartmentServiceImpl.java | 7 +- .../org/xyzh/system/log/DatabaseAppender.java | 177 +++ .../org/xyzh/system/log/LogEventContext.java | 75 ++ .../org/xyzh/system/log/LogEventHandler.java | 109 ++ .../main/java/org/xyzh/system/log/README.md | 160 +++ .../log/service/impl/LoginLogServiceImpl.java | 74 ++ .../impl/SysOperationLogServiceImpl.java | 64 ++ .../xyzh/system/mapper/DeptRoleMapper.java | 7 +- .../system/mapper/MenuPermissionMapper.java | 7 +- .../xyzh/system/mapper/PermissionMapper.java | 12 +- .../org/xyzh/system/mapper/RoleMapper.java | 43 +- .../xyzh/system/mapper/SysLoginLogMapper.java | 23 + .../system/mapper/SysOperationLogMapper.java | 46 +- .../org/xyzh/system/mapper/UserMapper.java | 43 +- .../menu/service/impl/SysMenuServiceImpl.java | 14 +- .../impl/SysPermissionServiceImpl.java | 12 +- .../role/service/impl/SysRoleServiceImpl.java | 53 +- .../user/service/impl/SysUserServiceImpl.java | 82 +- .../java/org/xyzh/system/utils/LoginUtil.java | 6 +- .../main/resources/mapper/DeptRoleMapper.xml | 50 +- .../resources/mapper/MenuPermissionMapper.xml | 47 +- .../resources/mapper/PermissionMapper.xml | 51 +- .../src/main/resources/mapper/RoleMapper.xml | 141 ++- .../resources/mapper/SysLoginLogMapper.xml | 131 +++ .../mapper/SysOperationLogMapper.xml | 118 +- .../src/main/resources/mapper/UserMapper.xml | 303 ++++- schoolNewsWeb/src/apis/study/learning-task.ts | 5 + schoolNewsWeb/src/apis/system/dept.ts | 10 +- schoolNewsWeb/src/apis/system/index.ts | 1 + schoolNewsWeb/src/apis/system/log.ts | 33 + schoolNewsWeb/src/apis/system/user.ts | 4 +- .../src/components/base/GenericSelector.vue | 38 +- .../src/components/base/TreeNode.vue | 45 +- schoolNewsWeb/src/types/index.ts | 3 + schoolNewsWeb/src/types/log/index.ts | 84 ++ schoolNewsWeb/src/types/permission/index.ts | 9 + schoolNewsWeb/src/types/study/index.ts | 3 + schoolNewsWeb/src/types/user/index.ts | 9 +- .../views/admin/manage/logs/LoginLogsView.vue | 649 +++++++++-- .../admin/manage/logs/OperationLogsView.vue | 1001 +++++++++++++++-- .../admin/manage/logs/SystemLogsView.vue | 974 +++++++++++++++- .../admin/manage/system/DeptManageView.vue | 14 +- .../admin/manage/system/MenuManageView.vue | 88 +- .../system/ModulePermissionManageView.vue | 123 +- .../admin/manage/system/RoleManageView.vue | 159 ++- .../views/public/task/LearningTaskList.vue | 178 ++- 91 files changed, 6003 insertions(+), 1485 deletions(-) create mode 100644 schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/LoginLogService.java create mode 100644 schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/OperationLogService.java delete mode 100644 schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginLogService.java delete mode 100644 schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtil.java create mode 100644 schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/spring/SpringContextUtil.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/config/AsyncLogConfig.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/controller/LogController.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/log/DatabaseAppender.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventContext.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventHandler.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/log/README.md create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/LoginLogServiceImpl.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/SysOperationLogServiceImpl.java create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysLoginLogMapper.java create mode 100644 schoolNewsServ/system/src/main/resources/mapper/SysLoginLogMapper.xml create mode 100644 schoolNewsWeb/src/apis/system/log.ts create mode 100644 schoolNewsWeb/src/types/log/index.ts diff --git a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql index 74fd8bc..62be18f 100644 --- a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql +++ b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql @@ -134,10 +134,10 @@ INSERT INTO `tb_sys_menu` VALUES ('6002', 'menu_admin_ai_config', 'AI配置', 'menu_admin_ai_manage', '/admin/manage/ai/config', 'admin/manage/ai/AIConfigView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('6003', 'menu_admin_knowledge', '知识库管理', 'menu_admin_ai_manage', '/admin/manage/ai/knowledge', 'admin/manage/ai/KnowledgeManagementView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('7000', 'menu_admin_logs_manage', '系统日志', NULL, '', '', 'admin/logs.svg', 7, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:53', NULL, 0), -('7001', 'menu_admin_system_logs', '系统日志', 'menu_admin_logs_manage', '/admin/manage/logs/system', 'admin/manage/logs/SystemLogsView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), +-- ('7001', 'menu_admin_system_logs', '系统日志', 'menu_admin_logs_manage', '/admin/manage/logs/system', 'admin/manage/logs/SystemLogsView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('7002', 'menu_admin_login_logs', '登录日志', 'menu_admin_logs_manage', '/admin/manage/logs/login', 'admin/manage/logs/LoginLogsView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), -('7003', 'menu_admin_operation_logs', '操作日志', 'menu_admin_logs_manage', '/admin/manage/logs/operation', 'admin/manage/logs/OperationLogsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), -('7004', 'menu_admin_system_config', '系统配置', 'menu_admin_logs_manage', '/admin/manage/logs/config', 'admin/manage/logs/SystemConfigView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), +-- ('7003', 'menu_admin_operation_logs', '操作日志', 'menu_admin_logs_manage', '/admin/manage/logs/operation', 'admin/manage/logs/OperationLogsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), +-- ('7004', 'menu_admin_system_config', '系统配置', 'menu_admin_logs_manage', '/admin/manage/logs/config', 'admin/manage/logs/SystemConfigView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('8000', 'menu_admin_crontab_manage', '定时任务管理', NULL, '', '', NULL, 8, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('8001', 'menu_admin_crontab_task', '任务管理', 'menu_admin_crontab_manage', '/admin/manage/crontab/task', 'admin/manage/crontab/TaskManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), diff --git a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/mapper/AchievementMapper.java b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/mapper/AchievementMapper.java index 2f83739..a11aba6 100644 --- a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/mapper/AchievementMapper.java +++ b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/mapper/AchievementMapper.java @@ -36,19 +36,21 @@ public interface AchievementMapper extends BaseMapper { TbAchievement selectByAchievementId(@Param("achievementId") String achievementId); /** - * @description 根据类型查询成就列表 + * @description 根据类型查询成就列表(包含权限过滤) * @param type 成就类型 + * @param userDeptRoles 用户部门角色列表 * @return List 成就列表 */ - List selectByType(@Param("type") Integer type); + List selectByType(@Param("type") Integer type, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据类型和等级查询成就列表 + * @description 根据类型和等级查询成就列表(包含权限过滤) * @param type 成就类型 * @param level 成就等级 + * @param userDeptRoles 用户部门角色列表 * @return List 成就列表 */ - List selectByTypeAndLevel(@Param("type") Integer type, @Param("level") Integer level); + List selectByTypeAndLevel(@Param("type") Integer type, @Param("level") Integer level, @Param("userDeptRoles") List userDeptRoles); /** * @description 根据条件类型查询成就列表 @@ -65,12 +67,13 @@ public interface AchievementMapper extends BaseMapper { List selectAchievementRanking(@Param("limit") Integer limit); /** - * @description 检查成就名称是否存在 + * @description 检查成就名称是否存在(包含权限过滤) * @param name 成就名称 * @param excludeId 排除的成就ID(用于更新时排除自身) + * @param userDeptRoles 用户部门角色列表 * @return int 存在的数量 */ - int countByName(@Param("name") String name, @Param("excludeId") String excludeId); + int countByName(@Param("name") String name, @Param("excludeId") String excludeId, @Param("userDeptRoles") List userDeptRoles); /** * @description 插入成就 diff --git a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java index dbedfdf..7985deb 100644 --- a/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java +++ b/schoolNewsServ/achievement/src/main/java/org/xyzh/achievement/service/impl/ACHAchievementServiceImpl.java @@ -71,7 +71,8 @@ public class ACHAchievementServiceImpl implements AchievementService { } // 检查名称是否已存在 - int count = achievementMapper.countByName(achievement.getName(), null); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + int count = achievementMapper.countByName(achievement.getName(), null, userDeptRoles); if (count > 0) { resultDomain.fail("成就名称已存在"); return resultDomain; @@ -92,7 +93,6 @@ public class ACHAchievementServiceImpl implements AchievementService { if (result > 0) { // 创建成就资源权限 try { - List userDeptRoles = LoginUtil.getCurrentDeptRole(); if (userDeptRoles != null && !userDeptRoles.isEmpty()) { // 使用用户的第一个部门角色创建权限 resourcePermissionService.createResourcePermission( @@ -137,7 +137,8 @@ public class ACHAchievementServiceImpl implements AchievementService { // 检查名称是否重复 if (StringUtils.hasText(achievement.getName())) { - int count = achievementMapper.countByName(achievement.getName(), achievement.getAchievementID()); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + int count = achievementMapper.countByName(achievement.getName(), achievement.getAchievementID(), userDeptRoles); if (count > 0) { resultDomain.fail("成就名称已存在"); return resultDomain; diff --git a/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml b/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml index 77b84f4..fd96160 100644 --- a/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml +++ b/schoolNewsServ/achievement/src/main/resources/mapper/AchievementMapper.xml @@ -126,22 +126,22 @@ WHERE achievement_id = #{achievementId} AND deleted = 0 - + - + @@ -165,13 +165,14 @@ - + diff --git a/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml b/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml index f50b119..1870b58 100644 --- a/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml +++ b/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml @@ -77,6 +77,11 @@ + + + + + @@ -111,6 +116,7 @@ + @@ -120,6 +126,7 @@ + @@ -129,6 +136,7 @@ + @@ -138,6 +146,7 @@ + @@ -147,6 +156,7 @@ + @@ -155,6 +165,7 @@ + diff --git a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/dept/DepartmentService.java b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/dept/DepartmentService.java index 2a7619a..83e3ea6 100644 --- a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/dept/DepartmentService.java +++ b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/dept/DepartmentService.java @@ -5,7 +5,6 @@ import java.util.List; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.dept.TbSysDept; import org.xyzh.common.dto.dept.TbSysDeptRole; -import org.xyzh.common.dto.role.TbSysRole; import org.xyzh.common.vo.UserDeptRoleVO; /** @@ -90,13 +89,13 @@ public interface DepartmentService { ResultDomain checkDepartmentNameExists(String deptName, String excludeId); /** - * @description 查询部门绑定角色 + * @description 查询部门绑定角色(包含名称) * @param deptId 部门ID - * @return ResultDomain 角色信息 + * @return ResultDomain 部门角色信息 * @author yslg * @since 2025-09-28 */ - ResultDomain getDeptByRole(String deptId); + ResultDomain getDeptByRole(String deptId); /** * @description 查询部门绑定角色列表(包含名称) diff --git a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/LoginLogService.java b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/LoginLogService.java new file mode 100644 index 0000000..71ec128 --- /dev/null +++ b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/LoginLogService.java @@ -0,0 +1,16 @@ +package org.xyzh.api.system.log; + +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.dto.system.TbSysLoginLog; + +public interface LoginLogService { + + ResultDomain insertLoginLog(TbSysLoginLog loginLog); + + ResultDomain selectLoginLogList(TbSysLoginLog loginLog); + + ResultDomain selectLoginLogPage(TbSysLoginLog loginLog, PageParam pageParam); + + +} \ No newline at end of file diff --git a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/OperationLogService.java b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/OperationLogService.java new file mode 100644 index 0000000..f931339 --- /dev/null +++ b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/log/OperationLogService.java @@ -0,0 +1,35 @@ +package org.xyzh.api.system.log; + +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.dto.system.TbSysOperationLog; + +/** + * @description 操作日志服务接口 + * @filename OperationLogService.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +public interface OperationLogService { + + /** + * @description 分页查询操作日志 + * @param operationLog 查询条件 + * @param pageParam 分页参数 + * @return ResultDomain 操作日志分页列表 + * @author yslg + * @since 2025-10-30 + */ + ResultDomain selectOperationLogPage(TbSysOperationLog operationLog, PageParam pageParam); + + /** + * @description 统计操作日志数量 + * @param operationLog 查询条件 + * @return int 日志数量 + * @author yslg + * @since 2025-10-30 + */ + int countOperationLog(TbSysOperationLog operationLog); +} + diff --git a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/menu/MenuService.java b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/menu/MenuService.java index d3bcc50..1205bb6 100644 --- a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/menu/MenuService.java +++ b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/menu/MenuService.java @@ -3,6 +3,7 @@ package org.xyzh.api.system.menu; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.menu.TbSysMenu; import org.xyzh.common.dto.menu.TbSysMenuPermission; +import org.xyzh.common.vo.PermissionVO; /** * @description MenuRemoteService.java文件描述 菜单远程服务接口 @@ -132,11 +133,11 @@ public interface MenuService { ResultDomain changeMenuVisibility(String menuId, Boolean visible); /** - * @description 查询菜单权限 + * @description 查询菜单权限(包含模块名称等信息) * @param menuId 菜单ID - * @return ResultDomain 菜单权限信息 + * @return ResultDomain 权限信息 * @author yslg * @since 2025-09-28 */ - ResultDomain getMenuPermission(String menuId); + ResultDomain getMenuPermission(String menuId); } diff --git a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/role/RoleService.java b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/role/RoleService.java index baa3ed6..b57dca9 100644 --- a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/role/RoleService.java +++ b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/role/RoleService.java @@ -4,6 +4,7 @@ import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.permission.TbSysPermission; import org.xyzh.common.dto.role.TbSysRole; import org.xyzh.common.dto.role.TbSysRolePermission; +import org.xyzh.common.vo.PermissionVO; import org.xyzh.common.vo.UserDeptRoleVO; @@ -22,16 +23,22 @@ public interface RoleService { * @author yslg * @since 2025-09-28 */ - ResultDomain getAllRoles(); + /** + * @description 查询所有角色(包含权限过滤) + * @return ResultDomain 角色VO列表 + * @author yslg + * @since 2025-10-30 + */ + ResultDomain getAllRoles(); /** - * @description 根据过滤条件获取角色列表 + * @description 根据过滤条件获取角色列表(包含权限过滤) * @param filter 过滤条件 - * @return ResultDomain 角色列表 + * @return ResultDomain 角色VO列表 * @author yslg * @since 2025-10-09 */ - ResultDomain getRoleList(TbSysRole filter); + ResultDomain getRoleList(TbSysRole filter); /** * @description 创建角色 diff --git a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/user/UserService.java b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/user/UserService.java index 783adfd..102c8cb 100644 --- a/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/user/UserService.java +++ b/schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/user/UserService.java @@ -39,6 +39,8 @@ public interface UserService { */ ResultDomain checkUserExists(TbSysUser user); + ResultDomain getLoginUser(TbSysUser filter); + /** * @description 获取所有用户 * @return ResultDomain 用户列表 @@ -74,6 +76,15 @@ public interface UserService { */ ResultDomain getUserByFilter(TbSysUser filter); + /** + * @description 根据过滤条件查询用户VO列表(包含userinfo和deptrole信息) + * @param filter 过滤条件 + * @return ResultDomain 用户VO列表 + * @author yslg + * @since 2025-10-30 + */ + ResultDomain getUserVOByFilter(TbSysUser filter); + /** * @description 获取用户列表分页 * @param filter 过滤条件 @@ -84,6 +95,16 @@ public interface UserService { */ ResultDomain getUserPage(TbSysUser filter, PageParam pageParam); + /** + * @description 获取用户VO列表分页(包含userinfo和deptrole信息) + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return ResultDomain 用户VO列表 + * @author yslg + * @since 2025-10-30 + */ + ResultDomain getUserVOPage(TbSysUser filter, PageParam pageParam); + /** * @description 创建用户 * @param user 用户对象 diff --git a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginLogService.java b/schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginLogService.java deleted file mode 100644 index 42cd7e6..0000000 --- a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginLogService.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.xyzh.auth.service; - -import org.springframework.stereotype.Service; -import org.xyzh.common.dto.system.TbSysLoginLog; -import org.xyzh.common.utils.IDUtils; - -/** - * @description LoginLogService.java文件描述 登录日志服务 - * @filename LoginLogService.java - * @author yslg - * @copyright xyzh - * @since 2025-09-28 - */ -@Service -public class LoginLogService { - - /** - * @description 保存登录日志 - * @param loginLog 登录日志 - * @author yslg - * @since 2025-09-28 - */ - public void saveLoginLog(TbSysLoginLog loginLog) { - // 确保登录日志有ID,如果没有则生成一个 - if (loginLog.getID() == null || loginLog.getID().isEmpty()) { - loginLog.setID(IDUtils.generateID()); - } - - // TODO: 实现登录日志保存逻辑 - // 这里应该调用数据层保存日志 - System.out.println("保存登录日志: " + loginLog); - } - - /** - * @description 根据用户ID查询登录日志 - * @param userId 用户ID - * @return List 登录日志列表 - * @author yslg - * @since 2025-09-28 - */ - public java.util.List findLoginLogsByUserId(String userId) { - // TODO: 实现根据用户ID查询登录日志的逻辑 - return new java.util.ArrayList<>(); - } - - /** - * @description 查询登录失败次数 - * @param userId 用户ID - * @param timeRange 时间范围(分钟) - * @return int 失败次数 - * @author yslg - * @since 2025-09-28 - */ - public int countFailedLoginAttempts(String userId, int timeRange) { - // TODO: 实现查询指定时间范围内的登录失败次数 - return 0; - } - - /** - * @description 清除登录失败记录 - * @param userId 用户ID - * @author yslg - * @since 2025-09-28 - */ - public void clearFailedLoginAttempts(String userId) { - // TODO: 实现清除登录失败记录的逻辑 - System.out.println("清除用户 " + userId + " 的登录失败记录"); - } -} diff --git a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginServiceImpl.java b/schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginServiceImpl.java index 2f09cb4..fea8b61 100644 --- a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginServiceImpl.java +++ b/schoolNewsServ/auth/src/main/java/org/xyzh/auth/service/LoginServiceImpl.java @@ -17,6 +17,7 @@ import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.common.dto.menu.TbSysMenu; import org.xyzh.common.exception.auth.AuthException; import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.ServletUtils; import jakarta.servlet.http.HttpServletRequest; @@ -24,6 +25,7 @@ import org.xyzh.api.system.user.UserService; import org.xyzh.api.system.role.RoleService; import org.xyzh.api.system.permission.PermissionService; import org.xyzh.common.redis.service.RedisService; +import org.xyzh.api.system.log.LoginLogService; import org.xyzh.api.system.menu.MenuService; import org.xyzh.common.vo.UserDeptRoleVO; @@ -70,6 +72,21 @@ public class LoginServiceImpl implements LoginService { public ResultDomain login(LoginParam loginParam, HttpServletRequest request) { ResultDomain result = new ResultDomain<>(); String ipAddress = request.getRemoteAddr(); + + // Redis 自动递增(不存在则创建并返回1,存在则递增后返回) + String attemptKey = "login:attempt:" + loginParam.getUsername(); + int loginAttempt = (int) redisService.incr(attemptKey, 1); + + // 如果是第一次尝试,设置10分钟过期时间 + if (loginAttempt == 1) { + redisService.setExpire(attemptKey, 10, TimeUnit.MINUTES); + } + + // 检查是否超过最大尝试次数 + if (loginAttempt > 3) { + result.fail("登录失败次数过多,请稍后再试"); + return result; + } try { // 自动检测登录类型 String loginType = detectLoginType(loginParam); @@ -88,7 +105,7 @@ public class LoginServiceImpl implements LoginService { TbSysUser user = strategy.findUser(loginParam); if (user == null) { result.fail("用户不存在"); - logLoginAttempt(loginParam, null, false, "用户不存在"); + logLoginAttempt(loginParam, null, false, loginAttempt, "用户不存在"); return result; } @@ -96,7 +113,7 @@ public class LoginServiceImpl implements LoginService { UserStatus userStatus = UserStatus.fromCode(String.valueOf(user.getStatus())); if (userStatus != UserStatus.NORMAL) { result.fail("用户状态异常: " + userStatus.getName()); - logLoginAttempt(loginParam, user, false, "用户状态异常: " + userStatus.getName()); + logLoginAttempt(loginParam, user, false, loginAttempt, "用户状态异常: " + userStatus.getName()); return result; } @@ -104,7 +121,7 @@ public class LoginServiceImpl implements LoginService { // 验证凭据(密码或验证码) if (!strategy.verifyCredential(loginParam.getPassword(), user.getPassword())) { result.fail("密码错误"); - logLoginAttempt(loginParam, user, false, "密码错误"); + logLoginAttempt(loginParam, user, false, loginAttempt, "密码错误"); return result; } } @@ -113,7 +130,7 @@ public class LoginServiceImpl implements LoginService { String storedCaptcha = (String) redisService.get("captcha:" + loginParam.getPhone()); if (!strategy.verifyCredential(loginParam.getCaptcha(), storedCaptcha)) { result.fail("验证码错误"); - logLoginAttempt(loginParam, user, false, "验证码错误"); + logLoginAttempt(loginParam, user, false, loginAttempt, "验证码错误"); return result; } // 验证码使用后删除 @@ -129,9 +146,10 @@ public class LoginServiceImpl implements LoginService { String redisKey = "login:token:" + user.getID(); redisService.set(redisKey, loginDomain, 24 * 60 * 60, TimeUnit.SECONDS); - // 记录成功登录日志 - logLoginAttempt(loginParam, user, true, "登录成功"); - + // 登录成功后清除失败次数并记录成功日志 + redisService.delete(attemptKey); + logLoginAttempt(loginParam, user, true, 0, "登录成功"); + result.success("登录成功", loginDomain); result.setData(loginDomain); @@ -253,7 +271,7 @@ public class LoginServiceImpl implements LoginService { * @author yslg * @since 2025-09-28 */ - private void logLoginAttempt(LoginParam loginParam, TbSysUser user, boolean success, String message) { + private void logLoginAttempt(LoginParam loginParam, TbSysUser user, boolean success, int errorCount, String message) { TbSysLoginLog loginLog = new TbSysLoginLog(); // 使用IDUtils生成登录日志ID @@ -266,9 +284,14 @@ public class LoginServiceImpl implements LoginService { // 注意:实际生产中不应记录密码 // loginLog.setPassword(loginParam.getPassword()); loginLog.setStatus(success ? 1 : 0); + loginLog.setErrorCount(errorCount); loginLog.setMessage(message); - loginLog.setLoginTime(new Date().toString()); - - loginLogService.saveLoginLog(loginLog); + loginLog.setLoginTime(new Date()); + loginLog.setIpAddress(ServletUtils.getClientIp()); + loginLog.setIpSource(ServletUtils.getIpSource()); + loginLog.setBrowser(ServletUtils.getBrowser()); + loginLog.setOs(ServletUtils.getOs()); + + loginLogService.insertLoginLog(loginLog); } } diff --git a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PasswordLoginStrategy.java b/schoolNewsServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PasswordLoginStrategy.java index 23d8fd4..aa0f9df 100644 --- a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PasswordLoginStrategy.java +++ b/schoolNewsServ/auth/src/main/java/org/xyzh/auth/strategy/impl/PasswordLoginStrategy.java @@ -62,11 +62,12 @@ public class PasswordLoginStrategy implements LoginStrategy { if (NonUtils.isNotEmpty(loginParam.getPhone())) { filter.setPhone(loginParam.getPhone()); } - List users = userService.getUserByFilter(filter).getDataList(); - if(users.isEmpty()) { + filter.setPassword(passwordEncoder.encode(loginParam.getPassword())); + TbSysUser user = userService.getLoginUser(filter).getData(); + if(user == null) { return null; } - return users.get(0); + return user; } @Override diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/system/TbSysLoginLog.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/system/TbSysLoginLog.java index 051def6..c28e081 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/system/TbSysLoginLog.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/system/TbSysLoginLog.java @@ -1,4 +1,6 @@ package org.xyzh.common.dto.system; +import java.util.Date; + import org.xyzh.common.dto.BaseDTO; /** @@ -65,7 +67,7 @@ public class TbSysLoginLog extends BaseDTO { * @author yslg * @since 2025-09-16 */ - private String loginTime; + private Date loginTime; /** * @description 登录状态(0失败 1成功) @@ -144,11 +146,11 @@ public class TbSysLoginLog extends BaseDTO { this.password = password; } - public String getLoginTime() { + public Date getLoginTime() { return loginTime; } - public void setLoginTime(String loginTime) { + public void setLoginTime(Date loginTime) { this.loginTime = loginTime; } diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/PermissionVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/PermissionVO.java index 9338dc1..e329999 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/PermissionVO.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/PermissionVO.java @@ -28,6 +28,20 @@ public class PermissionVO extends BaseDTO{ */ private String moduleID; + /** + * @description 模块名称 + * @author yslg + * @since 2025-10-30 + */ + private String moduleName; + + /** + * @description 模块描述 + * @author yslg + * @since 2025-10-30 + */ + private String moduleDescription; + /** * @description 权限名称 * @author yslg @@ -77,6 +91,48 @@ public class PermissionVO extends BaseDTO{ */ private String bindType; + /** + * @description 菜单ID(用于权限绑定菜单查询) + * @author yslg + * @since 2025-10-30 + */ + private String menuID; + + /** + * @description 菜单名称(用于权限绑定菜单查询) + * @author yslg + * @since 2025-10-30 + */ + private String menuName; + + /** + * @description 菜单描述(用于权限绑定菜单查询) + * @author yslg + * @since 2025-10-30 + */ + private String menuUrl; + + /** + * @description 角色ID(用于权限绑定角色查询) + * @author yslg + * @since 2025-10-30 + */ + private String roleID; + + /** + * @description 角色名称(用于权限绑定角色查询) + * @author yslg + * @since 2025-10-30 + */ + private String roleName; + + /** + * @description 角色描述(用于权限绑定角色查询) + * @author yslg + * @since 2025-10-30 + */ + private String roleDescription; + /** * @description 绑定菜单 * @author yslg @@ -120,6 +176,22 @@ public class PermissionVO extends BaseDTO{ this.moduleID = moduleID; } + public String getModuleName() { + return moduleName; + } + + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + + public String getModuleDescription() { + return moduleDescription; + } + + public void setModuleDescription(String moduleDescription) { + this.moduleDescription = moduleDescription; + } + public String getPermissionID() { return permissionID; } @@ -224,4 +296,52 @@ public class PermissionVO extends BaseDTO{ this.module = module; } + public String getMenuID() { + return menuID; + } + + public void setMenuID(String menuID) { + this.menuID = menuID; + } + + public String getMenuName() { + return menuName; + } + + public void setMenuName(String menuName) { + this.menuName = menuName; + } + + public String getMenuUrl() { + return menuUrl; + } + + public void setMenuUrl(String menuUrl) { + this.menuUrl = menuUrl; + } + + public String getRoleID() { + return roleID; + } + + public void setRoleID(String roleID) { + this.roleID = roleID; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public String getRoleDescription() { + return roleDescription; + } + + public void setRoleDescription(String roleDescription) { + this.roleDescription = roleDescription; + } + } diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/TaskItemVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/TaskItemVO.java index 541bbb5..934f25f 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/TaskItemVO.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/TaskItemVO.java @@ -16,13 +16,33 @@ public class TaskItemVO extends TbLearningTask { private String resourceName; private String userID; private String username; + private String deptID; + private String deptName; + private String parentID; private Boolean required; private Integer orderNum; private Integer status; private BigDecimal progress; private Date completeTime; - + public String getDeptID() { + return deptID; + } + public void setDeptID(String deptID) { + this.deptID = deptID; + } + public String getDeptName() { + return deptName; + } + public void setDeptName(String deptName) { + this.deptName = deptName; + } + public String getParentID() { + return parentID; + } + public void setParentID(String parentID) { + this.parentID = parentID; + } public TaskItemVO() { } diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/UserVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/UserVO.java index 98051ae..43c7a85 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/UserVO.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/UserVO.java @@ -18,6 +18,8 @@ public class UserVO { private Integer level; private String idCard; private String address; + private String deptID; + private String parentID; private String deptName; private String roleName; private Date createTime; @@ -25,6 +27,21 @@ public class UserVO { private Date deleteTime; private Boolean deleted; private Integer status; + + public String getDeptID() { + return deptID; + } + public void setDeptID(String deptID) { + this.deptID = deptID; + } + public String getParentID() { + return parentID; + } + public void setParentID(String parentID) { + this.parentID = parentID; + } + + public String getId() { return id; } diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtil.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtil.java deleted file mode 100644 index 1d765e9..0000000 --- a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtil.java +++ /dev/null @@ -1,189 +0,0 @@ -package org.xyzh.common.utils; - -import org.springframework.http.HttpHeaders; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * @description ServletUtil.java文件描述 Servlet工具类 - * @filename ServletUtil.java - * @author yslg - * @copyright xyzh - * @since 2025-10-07 - */ -public class ServletUtil { - - private static final String TOKEN_PREFIX = "Bearer "; - - /** - * @description 获取当前请求对象 - * @return HttpServletRequest 当前请求对象,无当前请求时返回null - * @author yslg - * @since 2025-10-07 - */ - public static HttpServletRequest getRequest() { - ServletRequestAttributes requestAttributes = - (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - return requestAttributes != null ? requestAttributes.getRequest() : null; - } - - /** - * @description 获取当前响应对象 - * @return HttpServletResponse 当前响应对象,无当前请求时返回null - * @author yslg - * @since 2025-10-07 - */ - public static HttpServletResponse getResponse() { - ServletRequestAttributes requestAttributes = - (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - return requestAttributes != null ? requestAttributes.getResponse() : null; - } - - /** - * @description 从当前请求中获取token(无参方法) - * @return String token值,未找到返回null - * @author yslg - * @since 2025-10-07 - */ - public static String getToken() { - HttpServletRequest request = getRequest(); - return request != null ? extractTokenFromRequest(request) : null; - } - - /** - * @description 从当前请求中获取指定请求头 - * @param headerName 请求头名称 - * @return String 请求头值,未找到返回null - * @author yslg - * @since 2025-10-07 - */ - public static String getHeader(String headerName) { - HttpServletRequest request = getRequest(); - return request != null ? request.getHeader(headerName) : null; - } - - /** - * @description 从当前请求中获取指定请求参数 - * @param paramName 参数名称 - * @return String 参数值,未找到返回null - * @author yslg - * @since 2025-10-07 - */ - public static String getParameter(String paramName) { - HttpServletRequest request = getRequest(); - return request != null ? request.getParameter(paramName) : null; - } - - /** - * @description 获取当前请求的客户端IP地址 - * @return String IP地址,未找到返回null - * @author yslg - * @since 2025-10-07 - */ - public static String getClientIpAddress() { - HttpServletRequest request = getRequest(); - if (request == null) { - return null; - } - - String ip = request.getHeader("X-Forwarded-For"); - if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { - // 多次反向代理后会有多个IP值,第一个为真实IP - int index = ip.indexOf(','); - if (index != -1) { - return ip.substring(0, index); - } else { - return ip; - } - } - - ip = request.getHeader("X-Real-IP"); - if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { - return ip; - } - - ip = request.getHeader("Proxy-Client-IP"); - if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { - return ip; - } - - ip = request.getHeader("WL-Proxy-Client-IP"); - if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { - return ip; - } - - ip = request.getHeader("HTTP_CLIENT_IP"); - if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { - return ip; - } - - ip = request.getHeader("HTTP_X_FORWARDED_FOR"); - if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { - return ip; - } - - return request.getRemoteAddr(); - } - - /** - * @description 获取当前请求的User-Agent - * @return String User-Agent值,未找到返回null - * @author yslg - * @since 2025-10-07 - */ - public static String getUserAgent() { - return getHeader("User-Agent"); - } - - /** - * @description 获取当前请求的完整URL - * @return String 完整URL,无当前请求时返回null - * @author yslg - * @since 2025-10-07 - */ - public static String getRequestUrl() { - HttpServletRequest request = getRequest(); - if (request == null) { - return null; - } - - StringBuffer url = request.getRequestURL(); - String queryString = request.getQueryString(); - if (NonUtils.isNotEmpty(queryString)) { - url.append('?').append(queryString); - } - return url.toString(); - } - - /** - * @description 从请求中提取token - * @param request HTTP请求对象 - * @return String token - * @author yslg - * @since 2025-10-07 - */ - private static String extractTokenFromRequest(HttpServletRequest request) { - // 优先从Authorization头获取 - String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); - if (NonUtils.isNotEmpty(authHeader) && authHeader.startsWith(TOKEN_PREFIX)) { - return authHeader.substring(TOKEN_PREFIX.length()); - } - - // 从请求参数中获取token - String token = request.getParameter("token"); - if (NonUtils.isNotEmpty(token)) { - return token; - } - - // 从请求头中获取token - token = request.getHeader("token"); - if (NonUtils.isNotEmpty(token)) { - return token; - } - - return null; - } -} diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtils.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtils.java index 12c7a5e..406b6ee 100644 --- a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtils.java +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/ServletUtils.java @@ -1,404 +1,337 @@ package org.xyzh.common.utils; +import org.springframework.http.HttpHeaders; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpSession; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.BufferedReader; -import java.util.Enumeration; -import java.util.Map; -import java.util.HashMap; -import java.util.Set; -import java.util.HashSet; /** - * @description ServletUtils.java文件描述:Servlet相关常用工具方法 - * @filename ServletUtils.java - * @author yslg + * @description ServletUtil.java文件描述 Servlet工具类 + * @filename ServletUtil.java + * @author yslg * @copyright xyzh - * @since 2023-12-19 + * @since 2025-10-07 */ public class ServletUtils { - /** - * @description 获取请求的真实IP地址 - * @param request HTTP请求对象 - * @return 客户端真实IP地址 - * @author yslg - * @since 2023-12-19 - */ - public static String getClientIp(HttpServletRequest request) { - String ip = request.getHeader("X-Forwarded-For"); - if (ip != null && ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { - // 多级代理时取第一个 - int idx = ip.indexOf(','); - if (idx > -1) { - ip = ip.substring(0, idx); - } - return ip.trim(); - } - ip = request.getHeader("Proxy-Client-IP"); - if (ip != null && ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { - return ip; - } - ip = request.getHeader("WL-Proxy-Client-IP"); - if (ip != null && ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { - return ip; - } - ip = request.getRemoteAddr(); - return ip; - } + private static final String TOKEN_PREFIX = "Bearer "; - /** - * @description 向响应写出JSON字符串 - * @param response HTTP响应对象 - * @param json 要写出的JSON字符串 - * @return void - * @author yslg - * @since 2023-12-19 - */ - public static void writeJson(HttpServletResponse response, String json) throws IOException { - response.setContentType("application/json;charset=UTF-8"); - PrintWriter writer = response.getWriter(); - writer.write(json); - writer.flush(); - writer.close(); - } + /** + * @description 获取当前请求对象 + * @return HttpServletRequest 当前请求对象,无当前请求时返回null + * @author yslg + * @since 2025-10-07 + */ + public static HttpServletRequest getRequest() { + ServletRequestAttributes requestAttributes = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return requestAttributes != null ? requestAttributes.getRequest() : null; + } - /** - * @description 获取请求参数(支持默认值) - * @param request HTTP请求对象 - * @param name 参数名 - * @param defaultValue 默认值 - * @return 参数值或默认值 - * @author yslg - * @since 2023-12-19 - */ - public static String getParameter(HttpServletRequest request, String name, String defaultValue) { - String value = request.getParameter(name); - return value != null ? value : defaultValue; - } + /** + * @description 获取当前响应对象 + * @return HttpServletResponse 当前响应对象,无当前请求时返回null + * @author yslg + * @since 2025-10-07 + */ + public static HttpServletResponse getResponse() { + ServletRequestAttributes requestAttributes = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return requestAttributes != null ? requestAttributes.getResponse() : null; + } - /** - * @description 判断请求是否为Ajax - * @param request HTTP请求对象 - * @return 是否为Ajax请求 - * @author yslg - * @since 2023-12-19 - */ - public static boolean isAjaxRequest(HttpServletRequest request) { - String header = request.getHeader("X-Requested-With"); - return "XMLHttpRequest".equalsIgnoreCase(header); - } + /** + * @description 从当前请求中获取token(无参方法) + * @return String token值,未找到返回null + * @author yslg + * @since 2025-10-07 + */ + public static String getToken() { + HttpServletRequest request = getRequest(); + return request != null ? extractTokenFromRequest(request) : null; + } - /** - * @description 获取请求体内容 - * @param request HTTP请求对象 - * @return 请求体内容 - * @author yslg - * @since 2023-12-19 - */ - public static String getRequestBody(HttpServletRequest request) throws IOException { - StringBuilder sb = new StringBuilder(); - BufferedReader reader = request.getReader(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - } - return sb.toString(); - } + /** + * @description 从当前请求中获取指定请求头 + * @param headerName 请求头名称 + * @return String 请求头值,未找到返回null + * @author yslg + * @since 2025-10-07 + */ + public static String getHeader(String headerName) { + HttpServletRequest request = getRequest(); + return request != null ? request.getHeader(headerName) : null; + } - /** - * @description 重定向到指定URL - * @param response HTTP响应对象 - * @param url 目标URL - * @return void - * @author yslg - * @since 2023-12-19 - */ - public static void redirect(HttpServletResponse response, String url) throws IOException { - response.sendRedirect(url); - } + /** + * @description 从当前请求中获取指定请求参数 + * @param paramName 参数名称 + * @return String 参数值,未找到返回null + * @author yslg + * @since 2025-10-07 + */ + public static String getParameter(String paramName) { + HttpServletRequest request = getRequest(); + return request != null ? request.getParameter(paramName) : null; + } - /** - * @description 获取完整请求URL - * @param request HTTP请求对象 - * @return 完整URL - * @author yslg - * @since 2023-12-19 - */ - public static String getFullUrl(HttpServletRequest request) { - StringBuilder url = new StringBuilder(); - url.append(request.getScheme()).append("://"); - url.append(request.getServerName()); - int port = request.getServerPort(); - if (("http".equals(request.getScheme()) && port != 80) || - ("https".equals(request.getScheme()) && port != 443)) { - url.append(":").append(port); - } - url.append(request.getRequestURI()); - String queryString = request.getQueryString(); - if (queryString != null) { - url.append("?").append(queryString); - } - return url.toString(); - } + /** + * @description 获取当前请求的客户端IP地址 + * @return String IP地址,未找到返回null + * @author yslg + * @since 2025-10-07 + */ + public static String getClientIpAddress() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } - /** - * @description 判断请求是否为GET方法 - * @param request HTTP请求对象 - * @return 是否为GET请求 - * @author yslg - * @since 2023-12-19 - */ - public static boolean isGet(HttpServletRequest request) { - return "GET".equalsIgnoreCase(request.getMethod()); - } + String ip = request.getHeader("X-Forwarded-For"); + if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { + // 多次反向代理后会有多个IP值,第一个为真实IP + int index = ip.indexOf(','); + if (index != -1) { + return ip.substring(0, index); + } else { + return ip; + } + } - /** - * @description 判断请求是否为POST方法 - * @param request HTTP请求对象 - * @return 是否为POST请求 - * @author yslg - * @since 2023-12-19 - */ - public static boolean isPost(HttpServletRequest request) { - return "POST".equalsIgnoreCase(request.getMethod()); - } + ip = request.getHeader("X-Real-IP"); + if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { + return ip; + } - /** - * @description 判断请求是否为PUT方法 - * @param request HTTP请求对象 - * @return 是否为PUT请求 - * @author yslg - * @since 2023-12-19 - */ - public static boolean isPut(HttpServletRequest request) { - return "PUT".equalsIgnoreCase(request.getMethod()); - } + ip = request.getHeader("Proxy-Client-IP"); + if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { + return ip; + } - /** - * @description 判断请求是否为DELETE方法 - * @param request HTTP请求对象 - * @return 是否为DELETE请求 - * @author yslg - * @since 2023-12-19 - */ - public static boolean isDelete(HttpServletRequest request) { - return "DELETE".equalsIgnoreCase(request.getMethod()); - } + ip = request.getHeader("WL-Proxy-Client-IP"); + if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { + return ip; + } - /** - * @description 获取所有请求参数 - * @param request HTTP请求对象 - * @return 参数Map - * @author yslg - * @since 2023-12-19 - */ - public static Map getParameterMap(HttpServletRequest request) { - Map paramMap = new HashMap<>(); - Enumeration parameterNames = request.getParameterNames(); - while (parameterNames.hasMoreElements()) { - String paramName = parameterNames.nextElement(); - String paramValue = request.getParameter(paramName); - paramMap.put(paramName, paramValue); - } - return paramMap; - } + ip = request.getHeader("HTTP_CLIENT_IP"); + if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { + return ip; + } - /** - * @description 获取请求头信息 - * @param request HTTP请求对象 - * @return 头信息Map - * @author yslg - * @since 2023-12-19 - */ - public static Map getHeaderMap(HttpServletRequest request) { - Map headerMap = new HashMap<>(); - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String headerName = headerNames.nextElement(); - String headerValue = request.getHeader(headerName); - headerMap.put(headerName, headerValue); - } - return headerMap; - } + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + if (NonUtils.isNotEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) { + return ip; + } - /** - * @description 获取Cookie值 - * @param request HTTP请求对象 - * @param name Cookie名 - * @return Cookie值 - * @author yslg - * @since 2023-12-19 - */ - public static String getCookieValue(HttpServletRequest request, String name) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (name.equals(cookie.getName())) { - return cookie.getValue(); - } - } - } - return null; - } + return request.getRemoteAddr(); + } - /** - * @description 设置Cookie - * @param response HTTP响应对象 - * @param name Cookie名 - * @param value Cookie值 - * @param maxAge 过期时间(秒) - * @return void - * @author yslg - * @since 2023-12-19 - */ - public static void setCookie(HttpServletResponse response, String name, String value, int maxAge) { - Cookie cookie = new Cookie(name, value); - cookie.setMaxAge(maxAge); - cookie.setPath("/"); - cookie.setHttpOnly(true); - response.addCookie(cookie); - } + /** + * @description 获取当前请求的User-Agent + * @return String User-Agent值,未找到返回null + * @author yslg + * @since 2025-10-07 + */ + public static String getUserAgent() { + return getHeader("User-Agent"); + } - /** - * @description 删除Cookie - * @param response HTTP响应对象 - * @param name Cookie名 - * @return void - * @author yslg - * @since 2023-12-19 - */ - public static void removeCookie(HttpServletResponse response, String name) { - Cookie cookie = new Cookie(name, null); - cookie.setMaxAge(0); - cookie.setPath("/"); - response.addCookie(cookie); - } + /** + * @description 获取客户端IP(别名方法) + * @return String IP地址 + * @author yslg + * @since 2025-10-30 + */ + public static String getClientIp() { + return getClientIpAddress(); + } - /** - * @description 判断是否为HTTPS请求 - * @param request HTTP请求对象 - * @return 是否为HTTPS请求 - * @author yslg - * @since 2023-12-19 - */ - public static boolean isHttps(HttpServletRequest request) { - return "https".equals(request.getScheme()) || request.isSecure() || - "443".equals(request.getHeader("X-Forwarded-Port")) || - "https".equals(request.getHeader("X-Forwarded-Proto")); - } + /** + * @description 获取IP来源地址(简化版) + * @return String IP来源 + * @author yslg + * @since 2025-10-30 + */ + public static String getIpSource() { + String ip = getClientIp(); + if (ip == null || ip.isEmpty()) { + return "未知"; + } + + // 判断是否为内网IP + if (isInternalIp(ip)) { + return "内网IP"; + } + + // 对于公网IP,返回简单标识 + // TODO: 可以集成第三方IP地址库进行详细查询 + return "外网IP"; + } - /** - * @description 获取上下文路径 - * @param request HTTP请求对象 - * @return 上下文路径 - * @author yslg - * @since 2023-12-19 - */ - public static String getContextPath(HttpServletRequest request) { - return request.getContextPath(); - } + /** + * @description 获取浏览器信息 + * @return String 浏览器类型和版本 + * @author yslg + * @since 2025-10-30 + */ + public static String getBrowser() { + String userAgent = getUserAgent(); + if (userAgent == null || userAgent.isEmpty()) { + return "Unknown"; + } - /** - * @description 检测请求是否包含指定参数 - * @param request HTTP请求对象 - * @param paramName 参数名 - * @return 是否包含参数 - * @author yslg - * @since 2023-12-19 - */ - public static boolean hasParameter(HttpServletRequest request, String paramName) { - return request.getParameter(paramName) != null; - } + userAgent = userAgent.toLowerCase(); - /** - * @description 获取Session对象 - * @param request HTTP请求对象 - * @return HttpSession对象 - * @author yslg - * @since 2023-12-19 - */ - public static HttpSession getSession(HttpServletRequest request) { - return request.getSession(); - } + // 检测常见浏览器 + if (userAgent.contains("edg")) { + return "Microsoft Edge"; + } else if (userAgent.contains("chrome")) { + return "Chrome"; + } else if (userAgent.contains("firefox")) { + return "Firefox"; + } else if (userAgent.contains("safari") && !userAgent.contains("chrome")) { + return "Safari"; + } else if (userAgent.contains("opera") || userAgent.contains("opr")) { + return "Opera"; + } else if (userAgent.contains("trident") || userAgent.contains("msie")) { + return "Internet Explorer"; + } else if (userAgent.contains("postman")) { + return "Postman"; + } else if (userAgent.contains("apifox")) { + return "Apifox"; + } - /** - * @description 获取Session属性 - * @param request HTTP请求对象 - * @param attributeName 属性名 - * @return 属性值 - * @author yslg - * @since 2023-12-19 - */ - public static Object getSessionAttribute(HttpServletRequest request, String attributeName) { - HttpSession session = request.getSession(false); - return session != null ? session.getAttribute(attributeName) : null; - } + return "Unknown"; + } - /** - * @description 设置Session属性 - * @param request HTTP请求对象 - * @param attributeName 属性名 - * @param attributeValue 属性值 - * @return void - * @author yslg - * @since 2023-12-19 - */ - public static void setSessionAttribute(HttpServletRequest request, String attributeName, Object attributeValue) { - request.getSession().setAttribute(attributeName, attributeValue); - } + /** + * @description 获取操作系统信息 + * @return String 操作系统类型 + * @author yslg + * @since 2025-10-30 + */ + public static String getOs() { + String userAgent = getUserAgent(); + if (userAgent == null || userAgent.isEmpty()) { + return "Unknown"; + } - /** - * @description 移除Session属性 - * @param request HTTP请求对象 - * @param attributeName 属性名 - * @return void - * @author yslg - * @since 2023-12-19 - */ - public static void removeSessionAttribute(HttpServletRequest request, String attributeName) { - HttpSession session = request.getSession(false); - if (session != null) { - session.removeAttribute(attributeName); - } - } + userAgent = userAgent.toLowerCase(); - /** - * @description 防止XSS攻击的字符串过滤 - * @param input 输入字符串 - * @return 过滤后的字符串 - * @author yslg - * @since 2023-12-19 - */ - public static String escapeXss(String input) { - if (input == null) { - return null; - } - return input - .replace("<", "<") - .replace(">", ">") - .replace("'", "'") - .replace("\"", """) - .replace("&", "&"); - } + // 检测操作系统 + if (userAgent.contains("windows nt 10.0")) { + return "Windows 10/11"; + } else if (userAgent.contains("windows nt 6.3")) { + return "Windows 8.1"; + } else if (userAgent.contains("windows nt 6.2")) { + return "Windows 8"; + } else if (userAgent.contains("windows nt 6.1")) { + return "Windows 7"; + } else if (userAgent.contains("windows")) { + return "Windows"; + } else if (userAgent.contains("mac os x")) { + return "macOS"; + } else if (userAgent.contains("linux")) { + if (userAgent.contains("android")) { + return "Android"; + } + return "Linux"; + } else if (userAgent.contains("iphone") || userAgent.contains("ipad")) { + return "iOS"; + } else if (userAgent.contains("android")) { + return "Android"; + } - /** - * @description 获取所有请求参数名 - * @param request HTTP请求对象 - * @return 参数名集合 - * @author yslg - * @since 2023-12-19 - */ - public static Set getParameterNames(HttpServletRequest request) { - Set paramNames = new HashSet<>(); - Enumeration names = request.getParameterNames(); - while (names.hasMoreElements()) { - paramNames.add(names.nextElement()); - } - return paramNames; - } -} \ No newline at end of file + return "Unknown"; + } + + /** + * @description 获取当前请求的完整URL + * @return String 完整URL,无当前请求时返回null + * @author yslg + * @since 2025-10-07 + */ + public static String getRequestUrl() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + + StringBuffer url = request.getRequestURL(); + String queryString = request.getQueryString(); + if (NonUtils.isNotEmpty(queryString)) { + url.append('?').append(queryString); + } + return url.toString(); + } + + /** + * @description 判断是否为内网IP + * @param ip IP地址 + * @return boolean true-内网IP,false-外网IP + * @author yslg + * @since 2025-10-30 + */ + private static boolean isInternalIp(String ip) { + if (ip == null || ip.isEmpty()) { + return false; + } + + // 本地回环地址 + if (ip.startsWith("127.") || ip.equals("localhost") || ip.equals("0:0:0:0:0:0:0:1") || ip.equals("::1")) { + return true; + } + + // 内网IP段 + if (ip.startsWith("10.") || + ip.startsWith("192.168.") || + ip.startsWith("172.")) { + // 172.16.0.0 - 172.31.255.255 + if (ip.startsWith("172.")) { + String[] parts = ip.split("\\."); + if (parts.length >= 2) { + try { + int secondOctet = Integer.parseInt(parts[1]); + return secondOctet >= 16 && secondOctet <= 31; + } catch (NumberFormatException e) { + return false; + } + } + } + return true; + } + + return false; + } + + /** + * @description 从请求中提取token + * @param request HTTP请求对象 + * @return String token + * @author yslg + * @since 2025-10-07 + */ + private static String extractTokenFromRequest(HttpServletRequest request) { + // 优先从Authorization头获取 + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (NonUtils.isNotEmpty(authHeader) && authHeader.startsWith(TOKEN_PREFIX)) { + return authHeader.substring(TOKEN_PREFIX.length()); + } + + // 从请求参数中获取token + String token = request.getParameter("token"); + if (NonUtils.isNotEmpty(token)) { + return token; + } + + // 从请求头中获取token + token = request.getHeader("token"); + if (NonUtils.isNotEmpty(token)) { + return token; + } + + return null; + } +} diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/spring/SpringContextUtil.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/spring/SpringContextUtil.java new file mode 100644 index 0000000..0e953a5 --- /dev/null +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/spring/SpringContextUtil.java @@ -0,0 +1,64 @@ +package org.xyzh.common.utils.spring; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * @description Spring上下文工具类 - 用于在非Spring管理的类中获取Bean + * @filename SpringContextUtil.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +@Component +public class SpringContextUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + applicationContext = context; + } + + /** + * @description 获取Spring容器中的Bean + * @param beanClass Bean的Class类型 + * @return T Bean实例 + */ + public static T getBean(Class beanClass) { + if (applicationContext == null) { + return null; + } + try { + return applicationContext.getBean(beanClass); + } catch (Exception e) { + return null; + } + } + + /** + * @description 获取Spring容器中的Bean + * @param beanName Bean的名称 + * @return Object Bean实例 + */ + public static Object getBean(String beanName) { + if (applicationContext == null) { + return null; + } + try { + return applicationContext.getBean(beanName); + } catch (Exception e) { + return null; + } + } + + /** + * @description 获取ApplicationContext + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } +} + diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java index 33aae00..6f288fa 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java @@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.xyzh.common.core.page.PageParam; import org.xyzh.common.dto.crontab.TbCrontabTask; +import org.xyzh.common.vo.UserDeptRoleVO; import java.util.List; @@ -55,31 +56,42 @@ public interface CrontabTaskMapper extends BaseMapper { TbCrontabTask selectTaskById(@Param("taskId") String taskId); /** - * @description 根据过滤条件查询任务列表 + * @description 根据过滤条件查询任务列表(包含权限过滤) * @param filter 过滤条件 + * @param userDeptRoles 用户部门角色列表 * @return List 任务列表 * @author yslg * @since 2025-10-25 */ - List selectTaskList(@Param("filter") TbCrontabTask filter); + List selectTaskList(@Param("filter") TbCrontabTask filter, @Param("userDeptRoles") List userDeptRoles); /** - * @description 分页查询任务列表 + * @description 分页查询任务列表(包含权限过滤) * @param filter 过滤条件 * @param pageParam 分页参数 + * @param userDeptRoles 用户部门角色列表 * @return List 任务列表 * @author yslg * @since 2025-10-25 */ - List selectTaskPage(@Param("filter") TbCrontabTask filter, @Param("pageParam") PageParam pageParam); + List selectTaskPage(@Param("filter") TbCrontabTask filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List userDeptRoles); /** - * @description 查询所有运行中的任务 + * @description 查询所有运行中的任务(包含权限过滤) + * @param userDeptRoles 用户部门角色列表 * @return List 任务列表 * @author yslg * @since 2025-10-25 */ - List selectRunningTasks(); + List selectRunningTasks(@Param("userDeptRoles") List userDeptRoles); + + /** + * @description 查询所有运行中的任务(系统级,不含权限过滤,用于系统初始化) + * @return List 任务列表 + * @author yslg + * @since 2025-10-25 + */ + List selectAllRunningTasks(); /** * @description 更新任务状态 @@ -102,12 +114,13 @@ public interface CrontabTaskMapper extends BaseMapper { TbCrontabTask selectTaskByBeanAndMethod(@Param("beanName") String beanName, @Param("methodName") String methodName); /** - * @description 查询任务总数 + * @description 查询任务总数(包含权限过滤) * @param filter 过滤条件 + * @param userDeptRoles 用户部门角色列表 * @return int 任务总数 * @author yslg * @since 2025-10-25 */ - int countSelectTask(@Param("filter") TbCrontabTask filter); + int countSelectTask(@Param("filter") TbCrontabTask filter, @Param("userDeptRoles") List userDeptRoles); } diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/scheduler/SchedulerInitializer.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/scheduler/SchedulerInitializer.java index b2ca69c..d9a5c96 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/scheduler/SchedulerInitializer.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/scheduler/SchedulerInitializer.java @@ -33,8 +33,8 @@ public class SchedulerInitializer implements CommandLineRunner { try { logger.info("开始初始化定时任务..."); - // 查询所有运行中的任务 - List runningTasks = taskMapper.selectRunningTasks(); + // 查询所有运行中的任务(系统级,不受权限限制) + List runningTasks = taskMapper.selectAllRunningTasks(); if (runningTasks != null && !runningTasks.isEmpty()) { for (TbCrontabTask task : runningTasks) { diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java index 9c14bbf..8db1966 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java @@ -217,7 +217,8 @@ public class CrontabServiceImpl implements CrontabService { } filter.setDeleted(false); - List list = taskMapper.selectTaskList(filter); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List list = taskMapper.selectTaskList(filter, userDeptRoles); resultDomain.success("查询成功", list); } catch (Exception e) { logger.error("查询定时任务列表异常: ", e); @@ -239,8 +240,9 @@ public class CrontabServiceImpl implements CrontabService { pageParam = new PageParam(); } - List list = taskMapper.selectTaskPage(filter, pageParam); - int total = taskMapper.countSelectTask(filter); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List list = taskMapper.selectTaskPage(filter, pageParam, userDeptRoles); + int total = taskMapper.countSelectTask(filter, userDeptRoles); PageDomain pageDomain = new PageDomain<>(); pageDomain.setDataList(list); diff --git a/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml b/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml index 684f173..c3655d5 100644 --- a/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml +++ b/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml @@ -90,6 +90,37 @@ + + + INNER JOIN tb_resource_permission rp ON ct.task_id = rp.resource_id + AND rp.resource_type = 7 + AND rp.deleted = 0 + AND rp.can_read = 1 + AND ( + -- 全局权限:所有用户可访问 + (rp.dept_id IS NULL AND rp.role_id IS NULL) + + OR EXISTS ( + SELECT 1 + FROM ( + + SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id + + ) user_roles + LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0 + WHERE + -- 部门级权限:当前部门或父部门(通过dept_path判断继承关系) + (rp.role_id IS NULL AND rp.dept_id IS NOT NULL + AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%')) + -- 角色级权限:跨部门的角色权限 + OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id) + -- 精确权限:特定部门的特定角色 + OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id) + ) + + ) + + INSERT INTO tb_crontab_task @@ -168,27 +199,69 @@ - + + + + - - + SELECT COUNT(DISTINCT ct.id) + FROM tb_crontab_task ct + + WHERE ct.deleted = 0 + + + AND ct.id = #{filter.ID} + + + AND ct.task_id = #{filter.taskId} + + + AND ct.task_name LIKE CONCAT('%', #{filter.taskName}, '%') + + + AND ct.task_group = #{filter.taskGroup} + + + AND ct.status = #{filter.status} + + diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/BannerMapper.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/BannerMapper.java index 3e25729..78bfbe4 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/BannerMapper.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/BannerMapper.java @@ -53,13 +53,14 @@ public interface BannerMapper extends BaseMapper { TbBanner selectByBannerId(@Param("bannerId") String bannerId); /** - * @description 根据状态查询Banner列表 + * @description 根据状态查询Banner列表(包含权限过滤) * @param status 状态 + * @param userDeptRoles 用户部门角色列表 * @return List Banner列表 * @author yslg * @since 2025-10-15 */ - List selectByStatus(@Param("status") Integer status); + List selectByStatus(@Param("status") Integer status, @Param("userDeptRoles") List userDeptRoles); /** * @description 根据位置查询Banner列表 @@ -71,22 +72,24 @@ public interface BannerMapper extends BaseMapper { List selectByPosition(@Param("position") String position); /** - * @description 查询有效的Banner列表(按排序) + * @description 查询有效的Banner列表(按排序,包含权限过滤) + * @param userDeptRoles 用户部门角色列表 * @return List Banner列表 * @author yslg * @since 2025-10-15 */ - List selectActiveBanners(); + List selectActiveBanners(@Param("userDeptRoles") List userDeptRoles); /** - * @description 检查Banner标题是否存在 + * @description 检查Banner标题是否存在(包含权限过滤) * @param title Banner标题 * @param excludeId 排除的Banner ID(用于更新时排除自身) + * @param userDeptRoles 用户部门角色列表 * @return int 存在的数量 * @author yslg * @since 2025-10-15 */ - int countByTitle(@Param("title") String title, @Param("excludeId") String excludeId); + int countByTitle(@Param("title") String title, @Param("excludeId") String excludeId, @Param("userDeptRoles") List userDeptRoles); /** * @description 插入Banner diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java index af5be44..5d69fcc 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java @@ -39,68 +39,75 @@ public interface ResourceMapper extends BaseMapper { TbResource selectByResourceId(@Param("resourceId") String resourceId); /** - * @description 根据标签ID查询资源列表 + * @description 根据标签ID查询资源列表(包含权限过滤) * @param tagId 标签ID(文章分类标签,tagType=1) + * @param userDeptRoles 用户部门角色列表 * @return List 资源列表 * @author yslg * @since 2025-10-15 */ - List selectByTagId(@Param("tagId") String tagId); + List selectByTagId(@Param("tagId") String tagId, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据状态查询资源列表 + * @description 根据状态查询资源列表(包含权限过滤) * @param status 状态 + * @param userDeptRoles 用户部门角色列表 * @return List 资源列表 * @author yslg * @since 2025-10-15 */ - List selectByStatus(@Param("status") Integer status); + List selectByStatus(@Param("status") Integer status, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据类型查询资源列表 + * @description 根据类型查询资源列表(包含权限过滤) * @param type 类型 + * @param userDeptRoles 用户部门角色列表 * @return List 资源列表 * @author yslg * @since 2025-10-15 */ - List selectByType(@Param("type") Integer type); + List selectByType(@Param("type") Integer type, @Param("userDeptRoles") List userDeptRoles); /** - * @description 查询热门资源列表 + * @description 查询热门资源列表(包含权限过滤) * @param limit 限制数量 + * @param userDeptRoles 用户部门角色列表 * @return List 资源列表 * @author yslg * @since 2025-10-15 */ - List selectHotResources(@Param("limit") Integer limit); + List selectHotResources(@Param("limit") Integer limit, @Param("userDeptRoles") List userDeptRoles); /** - * @description 查询最新资源列表 + * @description 查询最新资源列表(包含权限过滤) * @param limit 限制数量 + * @param userDeptRoles 用户部门角色列表 * @return List 资源列表 * @author yslg * @since 2025-10-15 */ - List selectLatestResources(@Param("limit") Integer limit); + List selectLatestResources(@Param("limit") Integer limit, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据关键词搜索资源 + * @description 根据关键词搜索资源(包含权限过滤) * @param keyword 关键词 + * @param userDeptRoles 用户部门角色列表 * @return List 资源列表 * @author yslg * @since 2025-10-15 */ - List searchByKeyword(@Param("keyword") String keyword); + List searchByKeyword(@Param("keyword") String keyword, @Param("userDeptRoles") List userDeptRoles); /** - * @description 检查资源标题是否存在 + * @description 检查资源标题是否存在(包含权限过滤) * @param title 资源标题 * @param excludeId 排除的资源ID(用于更新时排除自身) + * @param userDeptRoles 用户部门角色列表 * @return int 存在的数量 * @author yslg * @since 2025-10-15 */ - int countByTitle(@Param("title") String title, @Param("excludeId") String excludeId); + int countByTitle(@Param("title") String title, @Param("excludeId") String excludeId, @Param("userDeptRoles") List userDeptRoles); /** * @description 插入资源 diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/TagMapper.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/TagMapper.java index e61bbe5..0d3e4b6 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/TagMapper.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/TagMapper.java @@ -39,22 +39,24 @@ public interface TagMapper extends BaseMapper { TbTag selectByTagId(@Param("tagId") String tagId); /** - * @description 根据标签名称查询标签 + * @description 根据标签名称查询标签(包含权限过滤) * @param name 标签名称 + * @param userDeptRoles 用户部门角色列表 * @return TbTag 标签信息 * @author yslg * @since 2025-10-15 */ - TbTag selectByName(@Param("name") String name); + TbTag selectByName(@Param("name") String name, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据状态查询标签列表 + * @description 根据状态查询标签列表(包含权限过滤) * @param status 状态 + * @param userDeptRoles 用户部门角色列表 * @return List 标签列表 * @author yslg * @since 2025-10-15 */ - List selectByStatus(@Param("status") Integer status); + List selectByStatus(@Param("status") Integer status, @Param("userDeptRoles") List userDeptRoles); /** * @description 根据标签类型查询标签列表 @@ -75,15 +77,16 @@ public interface TagMapper extends BaseMapper { List selectHotTags(@Param("limit") Integer limit); /** - * @description 检查标签名称是否存在(同类型下) + * @description 检查标签名称是否存在(同类型下,包含权限过滤) * @param name 标签名称 * @param tagType 标签类型 * @param excludeId 排除的标签ID(用于更新时排除自身) + * @param userDeptRoles 用户部门角色列表 * @return int 存在的数量 * @author yslg * @since 2025-10-27 */ - int countByNameAndType(@Param("name") String name, @Param("tagType") Integer tagType, @Param("excludeId") String excludeId); + int countByNameAndType(@Param("name") String name, @Param("tagType") Integer tagType, @Param("excludeId") String excludeId, @Param("userDeptRoles") List userDeptRoles); /** * @description 插入标签 diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCBannerServiceImpl.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCBannerServiceImpl.java index 567ba85..9a4a788 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCBannerServiceImpl.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCBannerServiceImpl.java @@ -85,7 +85,8 @@ public class NCBannerServiceImpl implements BannerService { } // 检查标题是否已存在 - int count = bannerMapper.countByTitle(banner.getTitle(), null); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + int count = bannerMapper.countByTitle(banner.getTitle(), null, userDeptRoles); if (count > 0) { resultDomain.fail("横幅标题已存在"); return resultDomain; @@ -115,7 +116,6 @@ public class NCBannerServiceImpl implements BannerService { // 创建横幅资源权限 try { - List userDeptRoles = LoginUtil.getCurrentDeptRole(); if (userDeptRoles != null && !userDeptRoles.isEmpty()) { // 使用用户的第一个部门角色创建权限 resourcePermissionService.createResourcePermission( @@ -162,7 +162,8 @@ public class NCBannerServiceImpl implements BannerService { // 如果修改了标题,检查标题是否已被使用 if (StringUtils.hasText(banner.getTitle()) && !banner.getTitle().equals(existing.getTitle())) { - int count = bannerMapper.countByTitle(banner.getTitle(), banner.getBannerID()); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + int count = bannerMapper.countByTitle(banner.getTitle(), banner.getBannerID(), userDeptRoles); if (count > 0) { resultDomain.fail("横幅标题已存在"); return resultDomain; @@ -318,7 +319,8 @@ public class NCBannerServiceImpl implements BannerService { public ResultDomain getActiveBanners(Integer limit) { ResultDomain resultDomain = new ResultDomain<>(); try { - List list = bannerMapper.selectActiveBanners(); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List list = bannerMapper.selectActiveBanners(userDeptRoles); // 如果指定了limit,截取列表 if (limit != null && limit > 0 && list != null && list.size() > limit) { diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java index f5a538d..9784e46 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java @@ -146,7 +146,8 @@ public class NCResourceServiceImpl implements ResourceService { } // 检查标题是否已存在 - int count = resourceMapper.countByTitle(resourceVO.getResource().getTitle(), null); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + int count = resourceMapper.countByTitle(resourceVO.getResource().getTitle(), null, userDeptRoles); if (count > 0) { resultDomain.fail("资源标题已存在"); return resultDomain; @@ -205,7 +206,6 @@ public class NCResourceServiceImpl implements ResourceService { // 创建资源权限 try { - List userDeptRoles = LoginUtil.getCurrentDeptRole(); if (userDeptRoles != null && !userDeptRoles.isEmpty()) { // 使用用户的第一个部门角色创建权限 resourcePermissionService.createResourcePermission( @@ -259,7 +259,8 @@ public class NCResourceServiceImpl implements ResourceService { // 如果修改了标题,检查标题是否已被使用 if (StringUtils.hasText(resource.getTitle()) && !resource.getTitle().equals(existing.getTitle())) { - int count = resourceMapper.countByTitle(resource.getTitle(), resource.getResourceID()); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + int count = resourceMapper.countByTitle(resource.getTitle(), resource.getResourceID(), userDeptRoles); if (count > 0) { resultDomain.fail("资源标题已存在"); return resultDomain; @@ -762,7 +763,8 @@ public class NCResourceServiceImpl implements ResourceService { } // 先按关键词搜索 - List list = resourceMapper.searchByKeyword(keyword); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List list = resourceMapper.searchByKeyword(keyword, userDeptRoles); // 如果指定了分类ID,进行过滤 if (StringUtils.hasText(tagID) && list != null) { diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCTagServiceImpl.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCTagServiceImpl.java index 78cb744..2ffeb55 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCTagServiceImpl.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCTagServiceImpl.java @@ -58,7 +58,8 @@ public class NCTagServiceImpl implements TagService { } // 检查标签名称是否已存在(同类型下) - int count = tagMapper.countByNameAndType(tag.getName(), tag.getTagType(), null); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + int count = tagMapper.countByNameAndType(tag.getName(), tag.getTagType(), null, userDeptRoles); if (count > 0) { resultDomain.fail("该类型下标签名称已存在"); return resultDomain; @@ -83,7 +84,6 @@ public class NCTagServiceImpl implements TagService { // 创建标签资源权限 try { - List userDeptRoles = LoginUtil.getCurrentDeptRole(); if (userDeptRoles != null && !userDeptRoles.isEmpty()) { // 使用用户的第一个部门角色创建权限 resourcePermissionService.createResourcePermission( diff --git a/schoolNewsServ/news/src/main/resources/mapper/BannerMapper.xml b/schoolNewsServ/news/src/main/resources/mapper/BannerMapper.xml index 22234a9..4d3bbda 100644 --- a/schoolNewsServ/news/src/main/resources/mapper/BannerMapper.xml +++ b/schoolNewsServ/news/src/main/resources/mapper/BannerMapper.xml @@ -76,7 +76,6 @@ AND rp.deleted = 0 AND rp.can_read = 1 AND ( - -- 全局权限:所有用户可访问 (rp.dept_id IS NULL AND rp.role_id IS NULL) OR EXISTS ( @@ -88,12 +87,9 @@ ) user_roles LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0 WHERE - -- 部门级权限:当前部门或父部门(通过dept_path判断继承关系) (rp.role_id IS NULL AND rp.dept_id IS NOT NULL AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%')) - -- 角色级权限:跨部门的角色权限 OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id) - -- 精确权限:特定部门的特定角色 OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id) ) @@ -157,13 +153,13 @@ WHERE banner_id = #{bannerID} AND deleted = 0 - + @@ -175,22 +171,23 @@ ORDER BY order_num ASC, create_time DESC - + - + diff --git a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml index a11782d..375978b 100644 --- a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml +++ b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml @@ -130,38 +130,38 @@ - SELECT - - FROM tb_resource - WHERE status = 1 AND deleted = 0 - ORDER BY publish_time DESC, create_time DESC + SELECT DISTINCT r.* + FROM tb_resource r + + WHERE r.status = 1 AND r.deleted = 0 + ORDER BY r.publish_time DESC, r.create_time DESC LIMIT #{limit} @@ -181,23 +181,24 @@ @@ -205,14 +206,10 @@ INSERT INTO tb_resource ( id, resource_id, title, content, summary, cover_image, tag_id, author, source, - source_url, view_count, like_count, collect_count, status, is_recommend, - is_banner, publish_time, creator, updater, create_time, update_time, - delete_time, deleted + source_url, creator,create_time ) VALUES ( #{id}, #{resourceID}, #{title}, #{content}, #{summary}, #{coverImage}, #{tagID}, #{author}, #{source}, - #{sourceUrl}, #{viewCount}, #{likeCount}, #{collectCount}, #{status}, #{isRecommend}, - #{isBanner}, #{publishTime}, #{creator}, #{updater}, #{createTime}, #{updateTime}, - #{deleteTime}, #{deleted} + #{sourceUrl}, #{creator}, #{createTime} ) @@ -291,17 +288,13 @@ INSERT INTO tb_resource ( id, resource_id, title, content, summary, cover_image, tag_id, author, source, - source_url, view_count, like_count, collect_count, status, is_recommend, - is_banner, publish_time, creator, updater, create_time, update_time, - delete_time, deleted + source_url, creator,create_time ) VALUES ( #{item.id}, #{item.resourceID}, #{item.title}, #{item.content}, #{item.summary}, #{item.coverImage}, #{item.tagID}, #{item.author}, #{item.source}, #{item.sourceUrl}, - #{item.viewCount}, #{item.likeCount}, #{item.collectCount}, #{item.status}, - #{item.isRecommend}, #{item.isBanner}, #{item.publishTime}, #{item.creator}, - #{item.updater}, #{item.createTime}, #{item.updateTime}, #{item.deleteTime}, #{item.deleted} + #{item.creator}, #{item.createTime} ) diff --git a/schoolNewsServ/news/src/main/resources/mapper/TagMapper.xml b/schoolNewsServ/news/src/main/resources/mapper/TagMapper.xml index 712c4d8..d568bb5 100644 --- a/schoolNewsServ/news/src/main/resources/mapper/TagMapper.xml +++ b/schoolNewsServ/news/src/main/resources/mapper/TagMapper.xml @@ -103,21 +103,21 @@ WHERE tag_id = #{tagId} AND deleted = 0 - + - + @@ -144,16 +144,17 @@ - + diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/CourseMapper.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/CourseMapper.java index 7f12bcc..706e361 100644 --- a/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/CourseMapper.java +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/CourseMapper.java @@ -39,77 +39,85 @@ public interface CourseMapper extends BaseMapper { TbCourse selectByCourseId(@Param("courseId") String courseId); /** - * @description 根据课程名称查询课程 + * @description 根据课程名称查询课程(包含权限过滤) * @param name 课程名称 + * @param userDeptRoles 用户部门角色列表 * @return TbCourse 课程信息 * @author yslg * @since 2025-10-15 */ - TbCourse selectByName(@Param("name") String name); + TbCourse selectByName(@Param("name") String name, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据状态查询课程列表 + * @description 根据状态查询课程列表(包含权限过滤) * @param status 状态 + * @param userDeptRoles 用户部门角色列表 * @return List 课程列表 * @author yslg * @since 2025-10-15 */ - List selectByStatus(@Param("status") Integer status); + List selectByStatus(@Param("status") Integer status, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据类型查询课程列表 + * @description 根据类型查询课程列表(包含权限过滤) * @param type 类型 + * @param userDeptRoles 用户部门角色列表 * @return List 课程列表 * @author yslg * @since 2025-10-15 */ - List selectByType(@Param("type") Integer type); + List selectByType(@Param("type") Integer type, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据难度查询课程列表 + * @description 根据难度查询课程列表(包含权限过滤) * @param difficulty 难度 + * @param userDeptRoles 用户部门角色列表 * @return List 课程列表 * @author yslg * @since 2025-10-15 */ - List selectByDifficulty(@Param("difficulty") Integer difficulty); + List selectByDifficulty(@Param("difficulty") Integer difficulty, @Param("userDeptRoles") List userDeptRoles); /** - * @description 查询热门课程列表 + * @description 查询热门课程列表(包含权限过滤) * @param limit 限制数量 + * @param userDeptRoles 用户部门角色列表 * @return List 课程列表 * @author yslg * @since 2025-10-15 */ - List selectHotCourses(@Param("limit") Integer limit); + List selectHotCourses(@Param("limit") Integer limit, @Param("userDeptRoles") List userDeptRoles); /** - * @description 查询最新课程列表 + * @description 查询最新课程列表(包含权限过滤) * @param limit 限制数量 + * @param userDeptRoles 用户部门角色列表 * @return List 课程列表 * @author yslg * @since 2025-10-15 */ - List selectLatestCourses(@Param("limit") Integer limit); + List selectLatestCourses(@Param("limit") Integer limit, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据关键词搜索课程 + * @description 根据关键词搜索课程(包含权限过滤) * @param keyword 关键词 + * @param userDeptRoles 用户部门角色列表 * @return List 课程列表 * @author yslg * @since 2025-10-15 */ - List searchByKeyword(@Param("keyword") String keyword); + List searchByKeyword(@Param("keyword") String keyword, @Param("userDeptRoles") List userDeptRoles); /** - * @description 检查课程名称是否存在 + * @description 检查课程名称是否存在(包含权限过滤) * @param name 课程名称 * @param excludeId 排除的课程ID(用于更新时排除自身) + * @param userDeptRoles 用户部门角色列表 * @return int 存在的数量 * @author yslg * @since 2025-10-15 */ - int countByName(@Param("name") String name, @Param("excludeId") String excludeId); + int countByName(@Param("name") String name, @Param("excludeId") String excludeId, @Param("userDeptRoles") List userDeptRoles); /** * @description 插入课程 diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningTaskMapper.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningTaskMapper.java index 7b6fc7e..214f4aa 100644 --- a/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningTaskMapper.java +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningTaskMapper.java @@ -58,22 +58,24 @@ public interface LearningTaskMapper extends BaseMapper { TbLearningTask selectByName(@Param("name") String name); /** - * @description 根据状态查询学习任务列表 + * @description 根据状态查询学习任务列表(包含权限过滤) * @param status 状态 + * @param userDeptRoles 用户部门角色列表 * @return List 学习任务列表 * @author yslg * @since 2025-10-15 */ - List selectByStatus(@Param("status") Integer status); + List selectByStatus(@Param("status") Integer status, @Param("userDeptRoles") List userDeptRoles); /** - * @description 根据优先级查询学习任务列表 + * @description 根据优先级查询学习任务列表(包含权限过滤) * @param priority 优先级 + * @param userDeptRoles 用户部门角色列表 * @return List 学习任务列表 * @author yslg * @since 2025-10-15 */ - List selectByPriority(@Param("priority") Integer priority); + List selectByPriority(@Param("priority") Integer priority, @Param("userDeptRoles") List userDeptRoles); /** * @description 查询用户待完成任务列表 @@ -94,14 +96,15 @@ public interface LearningTaskMapper extends BaseMapper { List selectCompletedTasksByUserId(@Param("userId") String userId); /** - * @description 检查任务名称是否存在 + * @description 检查任务名称是否存在(包含权限过滤) * @param name 任务名称 * @param excludeId 排除的任务ID(用于更新时排除自身) + * @param userDeptRoles 用户部门角色列表 * @return int 存在的数量 * @author yslg * @since 2025-10-15 */ - int countByName(@Param("name") String name, @Param("excludeId") String excludeId); + int countByName(@Param("name") String name, @Param("excludeId") String excludeId, @Param("userDeptRoles") List userDeptRoles); /** * @description 插入学习任务 diff --git a/schoolNewsServ/study/src/main/resources/mapper/CourseMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/CourseMapper.xml index 568ae61..6132d8f 100644 --- a/schoolNewsServ/study/src/main/resources/mapper/CourseMapper.xml +++ b/schoolNewsServ/study/src/main/resources/mapper/CourseMapper.xml @@ -140,46 +140,46 @@ - SELECT - - FROM tb_course - WHERE status = 1 AND deleted = 0 - ORDER BY create_time DESC + SELECT DISTINCT c.* + FROM tb_course c + + WHERE c.status = 1 AND c.deleted = 0 + ORDER BY c.create_time DESC LIMIT #{limit} @@ -199,24 +199,25 @@ - + @@ -224,10 +225,10 @@ INSERT INTO tb_course ( id, course_id, name, cover_image, description, content, duration, - teacher, status, view_count, learn_count, order_num, creator, create_time + teacher, status, order_num, creator, create_time ) VALUES ( #{id}, #{courseID}, #{name}, #{coverImage}, #{description}, #{content}, #{duration}, - #{teacher}, #{status}, 0, 0, #{orderNum}, #{creator},#{createTime} + #{teacher}, #{status}, #{orderNum}, #{creator},#{createTime} ) @@ -291,14 +292,14 @@ INSERT INTO tb_course ( id, course_id, name, cover_image, description, content, duration, - teacher, status, view_count, learn_count, order_num, creator, - create_time, delete_time, deleted + teacher, status, order_num, creator, + create_time ) VALUES ( #{item.id}, #{item.courseID}, #{item.name}, #{item.coverImage}, #{item.description}, - #{item.content}, #{item.duration}, #{item.teacher}, #{item.status}, 0, - 0, #{item.orderNum}, #{item.creator}, #{item.createTime}, #{item.deleteTime}, #{item.deleted} + #{item.content}, #{item.duration}, #{item.teacher}, #{item.status}, + #{item.orderNum}, #{item.creator}, #{item.createTime} ) diff --git a/schoolNewsServ/study/src/main/resources/mapper/LearningTaskMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/LearningTaskMapper.xml index f3ae0b1..0e9a10d 100644 --- a/schoolNewsServ/study/src/main/resources/mapper/LearningTaskMapper.xml +++ b/schoolNewsServ/study/src/main/resources/mapper/LearningTaskMapper.xml @@ -124,28 +124,28 @@ @@ -153,7 +153,9 @@ SELECT FROM tb_learning_task - WHERE status = 0 AND deleted = 0 + LEFT JOIN tb_task_user tu ON t.task_id = tu.task_id + WHERE t.status = 0 AND t.deleted = 0 + AND tu.user_id = #{userId} ORDER BY create_time DESC @@ -162,17 +164,20 @@ SELECT FROM tb_learning_task - WHERE status = 1 AND deleted = 0 - ORDER BY create_time DESC + LEFT JOIN tb_task_user tu ON t.task_id = tu.task_id + WHERE t.status = 1 AND t.deleted = 0 + AND tu.user_id = #{userId} + ORDER BY t.create_time DESC diff --git a/schoolNewsServ/study/src/main/resources/mapper/TaskUserMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/TaskUserMapper.xml index bdeb87a..a9aab47 100644 --- a/schoolNewsServ/study/src/main/resources/mapper/TaskUserMapper.xml +++ b/schoolNewsServ/study/src/main/resources/mapper/TaskUserMapper.xml @@ -25,6 +25,9 @@ + + + @@ -86,21 +89,44 @@ diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/config/AsyncLogConfig.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/config/AsyncLogConfig.java new file mode 100644 index 0000000..587130a --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/config/AsyncLogConfig.java @@ -0,0 +1,58 @@ +package org.xyzh.system.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @description 异步日志配置 - 配置异步执行器 + * @filename AsyncLogConfig.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +@Configuration +@EnableAsync +public class AsyncLogConfig { + + /** + * @description 配置日志异步执行器 + * @return Executor + */ + @Bean(name = "logExecutor") + public Executor logExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + + // 核心线程数 + executor.setCorePoolSize(2); + + // 最大线程数 + executor.setMaxPoolSize(5); + + // 队列容量 + executor.setQueueCapacity(1000); + + // 线程名称前缀 + executor.setThreadNamePrefix("async-log-"); + + // 线程空闲时间(秒) + executor.setKeepAliveSeconds(60); + + // 拒绝策略:由调用线程处理(防止日志丢失) + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + + // 关闭时等待任务完成 + executor.setWaitForTasksToCompleteOnShutdown(true); + + // 等待时间(秒) + executor.setAwaitTerminationSeconds(60); + + executor.initialize(); + return executor; + } +} + diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/DeptController.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/DeptController.java index e724f82..188de85 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/DeptController.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/DeptController.java @@ -98,14 +98,14 @@ public class DeptController { } /** - * @description 查询部门绑定角色 + * @description 查询部门绑定角色(包含名称) * @param dept 部门信息 - * @return ResultDomain 角色信息 + * @return ResultDomain 部门角色信息 * @author yslg * @since 2025-10-06 */ @PostMapping("/role") - public ResultDomain getDeptByRole(@RequestBody TbSysDept dept) { + public ResultDomain getDeptByRole(@RequestBody TbSysDept dept) { return deptService.getDeptByRole(dept.getDeptID()); } diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/LogController.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/LogController.java new file mode 100644 index 0000000..fddadbe --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/LogController.java @@ -0,0 +1,58 @@ +package org.xyzh.system.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.xyzh.api.system.log.LoginLogService; +import org.xyzh.api.system.log.OperationLogService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.core.page.PageRequest; +import org.xyzh.common.dto.system.TbSysLoginLog; +import org.xyzh.common.dto.system.TbSysOperationLog; + +/** + * @description 日志管理控制器 + * @filename LogController.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +@RestController +@RequestMapping("/sys/log") +public class LogController { + + @Autowired + private LoginLogService loginLogService; + + @Autowired + private OperationLogService operationLogService; + + /** + * @description 分页查询登录日志 + * 根据条件分页查询登录日志,superadmin可查看所有日志,其他用户只能查看本部门及子部门的日志 + * @param loginLog 查询条件 + * @param pageNum 页码(从1开始) + * @param pageSize 每页大小 + * @return ResultDomain 登录日志分页列表 + */ + @PostMapping("/login/page") + public ResultDomain getLoginLogPage(@RequestBody PageRequest pageRequest) { + + return loginLogService.selectLoginLogPage(pageRequest.getFilter(), pageRequest.getPageParam()); + } + + /** + * @description 分页查询操作日志 + * 根据条件分页查询操作日志,superadmin可查看所有日志,其他用户只能查看本部门及子部门的日志 + * @param operationLog 查询条件 + * @param pageNum 页码(从1开始) + * @param pageSize 每页大小 + * @return ResultDomain 操作日志分页列表 + */ + @PostMapping("/operation/page") + public ResultDomain getOperationLogPage(@RequestBody PageRequest pageRequest) { + + return operationLogService.selectOperationLogPage(pageRequest.getFilter(), pageRequest.getPageParam()); + } +} + diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/MenuController.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/MenuController.java index 28f6981..349ec61 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/MenuController.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/MenuController.java @@ -14,6 +14,7 @@ import org.xyzh.api.system.menu.MenuService; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.menu.TbSysMenu; import org.xyzh.common.dto.menu.TbSysMenuPermission; +import org.xyzh.common.vo.PermissionVO; @RestController @RequestMapping("/menus") @@ -97,14 +98,14 @@ public class MenuController { /** - * @description 查询菜单权限 - * @param menuId 菜单ID - * @return ResultDomain 菜单权限信息 + * @description 查询菜单权限(包含模块名称等信息) + * @param menu 菜单信息 + * @return ResultDomain 权限信息 * @author yslg * @since 2025-10-06 */ @PostMapping("/permission") - public ResultDomain getMenuPermission(@RequestBody TbSysMenu menu) { + public ResultDomain getMenuPermission(@RequestBody TbSysMenu menu) { return menuService.getMenuPermission(menu.getMenuID()); } diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/RoleController.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/RoleController.java index 875c9ab..f866bf4 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/RoleController.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/RoleController.java @@ -14,6 +14,7 @@ import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.permission.TbSysPermission; import org.xyzh.common.dto.role.TbSysRole; import org.xyzh.common.dto.role.TbSysRolePermission; +import org.xyzh.common.vo.PermissionVO; import org.xyzh.common.vo.UserDeptRoleVO; /** @@ -37,8 +38,14 @@ public class RoleController { * @author yslg * @ since 2025-10-09 */ + /** + * @description 查询所有角色(包含权限过滤) + * @return ResultDomain 角色VO列表 + * @author yslg + * @since 2025-10-30 + */ @PostMapping("/all") - public ResultDomain all() { + public ResultDomain all() { return roleService.getAllRoles(); } @@ -55,14 +62,14 @@ public class RoleController { } /** - * @description 根据过滤条件获取角色列表 + * @description 根据过滤条件获取角色列表(包含权限过滤) * @param filter 过滤条件 - * @return ResultDomain 角色列表 + * @return ResultDomain 角色VO列表 * @author yslg * @since 2025-10-09 */ @PostMapping("/list") - public ResultDomain getRoleList(@RequestBody TbSysRole filter) { + public ResultDomain getRoleList(@RequestBody TbSysRole filter) { return roleService.getRoleList(filter); } diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/UserController.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/UserController.java index 2a70c27..4b7db32 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/UserController.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/UserController.java @@ -18,6 +18,7 @@ import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.common.dto.user.TbSysUserDeptRole; import org.xyzh.common.dto.user.TbSysUserInfo; import org.xyzh.common.vo.UserDeptRoleVO; +import org.xyzh.common.vo.UserVO; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -92,8 +93,8 @@ public class UserController { * @since 2025-10-09 */ @PostMapping("/list") - public ResultDomain getUserList(@RequestBody TbSysUser filter) { - return userService.getUserByFilter(filter); + public ResultDomain getUserList(@RequestBody TbSysUser filter) { + return userService.getUserVOByFilter(filter); } /** @@ -103,10 +104,10 @@ public class UserController { * @since 2025-10-09 */ @PostMapping("/page") - public ResultDomain getUserPage(@RequestBody PageRequest pageRequest) { + public ResultDomain getUserPage(@RequestBody PageRequest pageRequest) { TbSysUser filter = pageRequest.getFilter(); PageParam pageParam = pageRequest.getPageParam(); - return userService.getUserPage(filter, pageParam); + return userService.getUserVOPage(filter, pageParam); } /** diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/department/service/impl/SysDepartmentServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/department/service/impl/SysDepartmentServiceImpl.java index 75dd54c..5489bad 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/department/service/impl/SysDepartmentServiceImpl.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/department/service/impl/SysDepartmentServiceImpl.java @@ -370,11 +370,12 @@ public class SysDepartmentServiceImpl implements SysDepartmentService { } @Override - public ResultDomain getDeptByRole(String deptId) { - ResultDomain resultDomain = new ResultDomain<>(); + public ResultDomain getDeptByRole(String deptId) { + ResultDomain resultDomain = new ResultDomain<>(); try { logger.info("开始查询部门绑定角色:{}", deptId); - List roles = deptRoleMapper.selectDeptRole(deptId); + List roles = deptRoleMapper.selectDeptRole(deptId); + logger.info("查询部门绑定角色完成,共找到{}条记录", roles.size()); resultDomain.success("查询成功", roles); return resultDomain; } catch (Exception e) { diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/log/DatabaseAppender.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/DatabaseAppender.java new file mode 100644 index 0000000..19c2dc5 --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/DatabaseAppender.java @@ -0,0 +1,177 @@ +package org.xyzh.system.log; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.xyzh.common.utils.spring.SpringContextUtil; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +/** + * @description 数据库日志Appender - 异步写入数据库 + * @filename DatabaseAppender.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +@Plugin(name = "DatabaseAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true) +public class DatabaseAppender extends AbstractAppender { + + // 异步队列,存储待处理的日志事件上下文 + private static final BlockingQueue logQueue = new ArrayBlockingQueue<>(10000); + + // 日志处理工作线程 + private static LogEventProcessor processor; + + protected DatabaseAppender(String name, Filter filter, boolean ignoreExceptions) { + super(name, filter, null, ignoreExceptions, Property.EMPTY_ARRAY); + + // 启动日志处理线程 + if (processor == null) { + processor = new LogEventProcessor(); + Thread processorThread = new Thread(processor, "log-db-processor"); + processorThread.setDaemon(true); + processorThread.start(); + } + } + + @Override + public void append(LogEvent event) { + // 在主线程中提取HTTP上下文信息(异步线程中无法获取) + String userId = null; + String username = null; + String requestUrl = null; + String requestMethod = null; + String ipAddress = null; + String ipSource = null; + String browser = null; + String os = null; + + try { + // 尝试获取用户信息 + SpringContextUtil contextUtil = SpringContextUtil.getBean(SpringContextUtil.class); + if (contextUtil != null) { + Object loginUtil = SpringContextUtil.getBean("loginUtil"); + if (loginUtil != null) { + java.lang.reflect.Method getUserIdMethod = loginUtil.getClass().getMethod("getCurrentUserId"); + java.lang.reflect.Method getUsernameMethod = loginUtil.getClass().getMethod("getCurrentUsername"); + userId = (String) getUserIdMethod.invoke(null); + username = (String) getUsernameMethod.invoke(null); + } + } + } catch (Exception e) { + // 非登录状态或反射调用失败,忽略 + } + + try { + // 尝试获取请求信息 + Class servletUtilsClass = Class.forName("org.xyzh.common.utils.ServletUtils"); + requestUrl = (String) servletUtilsClass.getMethod("getRequestUrl").invoke(null); + requestMethod = (String) servletUtilsClass.getMethod("getHeader", String.class).invoke(null, "X-HTTP-Method-Override"); + ipAddress = (String) servletUtilsClass.getMethod("getClientIp").invoke(null); + ipSource = (String) servletUtilsClass.getMethod("getIpSource").invoke(null); + browser = (String) servletUtilsClass.getMethod("getBrowser").invoke(null); + os = (String) servletUtilsClass.getMethod("getOs").invoke(null); + } catch (Exception e) { + // 非HTTP请求上下文或类不存在,忽略 + } + + // 创建日志事件上下文(包含HTTP上下文信息) + LogEventContext context = new LogEventContext( + event.toImmutable(), userId, username, + requestUrl, requestMethod, ipAddress, ipSource, browser, os + ); + + // 将日志事件上下文添加到队列中,非阻塞方式 + if (!logQueue.offer(context)) { + // 队列满了,记录警告但不阻塞应用 + System.err.println("日志队列已满,丢弃日志: " + event.getMessage().getFormattedMessage()); + } + } + + @Override + public void stop() { + super.stop(); + if (processor != null) { + processor.shutdown(); + } + } + + /** + * @description 创建 DatabaseAppender 实例的工厂方法 + */ + @PluginFactory + public static DatabaseAppender createAppender( + @PluginAttribute("name") String name, + @PluginElement("Filter") Filter filter, + @PluginAttribute("ignoreExceptions") boolean ignoreExceptions) { + + if (name == null) { + LOGGER.error("未指定 DatabaseAppender 名称"); + return null; + } + + return new DatabaseAppender(name, filter, ignoreExceptions); + } + + /** + * @description 获取日志队列,供处理器使用 + */ + public static BlockingQueue getLogQueue() { + return logQueue; + } + + /** + * @description 日志事件处理器(内部类) + */ + static class LogEventProcessor implements Runnable { + private volatile boolean running = true; + + @Override + public void run() { + while (running) { + try { + // 从队列中获取日志事件上下文(阻塞等待) + LogEventContext context = logQueue.poll(1, TimeUnit.SECONDS); + if (context != null) { + processLogEvent(context); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + System.err.println("处理日志事件失败: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + /** + * @description 处理单个日志事件 + */ + private void processLogEvent(LogEventContext context) { + try { + // 通过 Spring 容器获取日志Service并保存 + LogEventHandler handler = SpringContextUtil.getBean(LogEventHandler.class); + if (handler != null) { + handler.handleLogEvent(context); + } + } catch (Exception e) { + System.err.println("保存日志到数据库失败: " + e.getMessage()); + } + } + + public void shutdown() { + running = false; + } + } +} + diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventContext.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventContext.java new file mode 100644 index 0000000..ffdc5ff --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventContext.java @@ -0,0 +1,75 @@ +package org.xyzh.system.log; + +import org.apache.logging.log4j.core.LogEvent; + +/** + * @description 日志事件上下文 - 包含日志事件和HTTP上下文信息 + * @filename LogEventContext.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +public class LogEventContext { + + private final LogEvent logEvent; + private final String userId; + private final String username; + private final String requestUrl; + private final String requestMethod; + private final String ipAddress; + private final String ipSource; + private final String browser; + private final String os; + + public LogEventContext(LogEvent logEvent, String userId, String username, + String requestUrl, String requestMethod, + String ipAddress, String ipSource, + String browser, String os) { + this.logEvent = logEvent; + this.userId = userId; + this.username = username; + this.requestUrl = requestUrl; + this.requestMethod = requestMethod; + this.ipAddress = ipAddress; + this.ipSource = ipSource; + this.browser = browser; + this.os = os; + } + + public LogEvent getLogEvent() { + return logEvent; + } + + public String getUserId() { + return userId; + } + + public String getUsername() { + return username; + } + + public String getRequestUrl() { + return requestUrl; + } + + public String getRequestMethod() { + return requestMethod; + } + + public String getIpAddress() { + return ipAddress; + } + + public String getIpSource() { + return ipSource; + } + + public String getBrowser() { + return browser; + } + + public String getOs() { + return os; + } +} + diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventHandler.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventHandler.java new file mode 100644 index 0000000..e87c0c2 --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/LogEventHandler.java @@ -0,0 +1,109 @@ +package org.xyzh.system.log; + +import org.apache.logging.log4j.core.LogEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.xyzh.common.dto.system.TbSysOperationLog; +import org.xyzh.common.utils.IDUtils; +import org.xyzh.system.mapper.SysOperationLogMapper; + +import java.util.Date; + +/** + * @description 日志事件处理器 - 将日志事件转换并保存到数据库 + * @filename LogEventHandler.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +@Component +public class LogEventHandler { + + @Autowired + private SysOperationLogMapper operationLogMapper; + + /** + * @description 处理日志事件(异步) + * @param context 日志事件上下文(包含HTTP上下文信息) + */ + @Async("logExecutor") + public void handleLogEvent(LogEventContext context) { + try { + // 处理 DEBUG 级别及以上的所有日志(由 ThresholdFilter 已过滤) + // 这里不需要再次判断级别,直接保存即可 + TbSysOperationLog operationLog = buildOperationLog(context); + operationLogMapper.insertOperationLog(operationLog); + } catch (Exception e) { + // 使用 System.err 而不是 logger,避免循环依赖 + System.err.println("保存日志到数据库失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * @description 构建操作日志对象 + */ + private TbSysOperationLog buildOperationLog(LogEventContext context) { + LogEvent event = context.getLogEvent(); + TbSysOperationLog log = new TbSysOperationLog(); + + log.setID(IDUtils.generateID()); + + // 使用从主线程提取的用户信息 + log.setUserID(context.getUserId()); + log.setUsername(context.getUsername()); + + // 日志基本信息 + log.setModule(event.getLoggerName()); + log.setOperation(event.getLevel().name()); + log.setMethod(event.getLoggerName()); + + // 使用从主线程提取的请求信息 + log.setRequestUrl(context.getRequestUrl()); + log.setRequestMethod(context.getRequestMethod()); + log.setIpAddress(context.getIpAddress()); + log.setIpSource(context.getIpSource()); + log.setBrowser(context.getBrowser()); + log.setOs(context.getOs()); + + // 日志消息和异常信息 + log.setResponseData(event.getMessage().getFormattedMessage()); + if (event.getThrown() != null) { + log.setErrorMessage(getStackTrace(event.getThrown())); + log.setStatus(0); // 失败 + } else { + log.setStatus(1); // 成功 + } + + log.setExecuteTime(0); // 日志无执行时间 + log.setCreateTime(new Date(event.getTimeMillis())); + + return log; + } + + /** + * @description 获取异常堆栈信息 + */ + private String getStackTrace(Throwable throwable) { + if (throwable == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + sb.append(throwable.getClass().getName()).append(": ").append(throwable.getMessage()).append("\n"); + + StackTraceElement[] elements = throwable.getStackTrace(); + int maxLines = Math.min(elements.length, 10); // 只保留前10行 + for (int i = 0; i < maxLines; i++) { + sb.append("\tat ").append(elements[i].toString()).append("\n"); + } + + if (elements.length > maxLines) { + sb.append("\t... ").append(elements.length - maxLines).append(" more\n"); + } + + return sb.toString(); + } +} + diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/log/README.md b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/README.md new file mode 100644 index 0000000..5b13fad --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/README.md @@ -0,0 +1,160 @@ +# 异步日志监听系统 + +## 功能说明 + +本系统实现了对 Logger 输出的监听,将ERROR级别及以上的日志异步写入数据库,用于日志审计和问题追踪。 + +## 核心组件 + +### 1. DatabaseAppender +**路径**: `system/src/main/java/org/xyzh/system/log/DatabaseAppender.java` + +自定义 Log4j2 Appender,负责: +- 监听所有日志输出 +- 过滤 ERROR 级别及以上的日志 +- 将日志事件放入异步队列(非阻塞,防止影响业务性能) +- 队列容量:10000条 + +### 2. LogEventHandler +**路径**: `system/src/main/java/org/xyzh/system/log/LogEventHandler.java` + +日志事件处理器,负责: +- 从异步队列获取日志事件 +- 转换为 `TbSysOperationLog` 对象 +- 异步批量写入数据库 +- 自动提取:用户信息、请求信息、IP、浏览器、OS等 + +### 3. SpringContextUtil +**路径**: `system/src/main/java/org/xyzh/system/log/SpringContextUtil.java` + +Spring上下文工具类,用于在非Spring管理的类(如Log4j2 Appender)中获取Spring Bean。 + +### 4. AsyncLogConfig +**路径**: `system/src/main/java/org/xyzh/system/config/AsyncLogConfig.java` + +异步执行器配置: +- 核心线程数:2 +- 最大线程数:5 +- 队列容量:1000 +- 拒绝策略:CallerRunsPolicy(防止日志丢失) + +## 配置方法 + +### 1. 在 log4j2-spring.xml 中添加 Database Appender + +```xml + + + + + + + + + + + + + + + + +``` + +### 2. 确保数据库表存在 + +表名:`tb_sys_operation_log` + +主要字段: +- `id` - 主键 +- `user_id` - 用户ID +- `username` - 用户名 +- `module` - 模块名(Logger名称) +- `operation` - 操作类型(日志级别) +- `method` - 方法名 +- `request_url` - 请求URL +- `request_method` - 请求方法 +- `request_params` - 请求参数 +- `response_data` - 日志消息 +- `ip_address` - IP地址 +- `ip_source` - IP来源 +- `browser` - 浏览器 +- `os` - 操作系统 +- `status` - 状态(0-失败,1-成功) +- `error_message` - 异常堆栈 +- `execute_time` - 执行时间 +- `create_time` - 创建时间 + +## 使用示例 + +### 记录错误日志 + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UserService { + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + + public void someMethod() { + try { + // 业务逻辑 + } catch (Exception e) { + // 这条ERROR日志会自动写入数据库 + logger.error("用户操作失败", e); + } + } +} +``` + +### 记录警告日志(不会写入数据库) + +```java +// WARN级别日志不会写入数据库,只写入文件 +logger.warn("这是一个警告信息"); +``` + +## 性能优化 + +1. **异步队列**:日志事件先放入内存队列,不阻塞业务线程 +2. **批量处理**:后台线程批量从队列取出并处理 +3. **线程池**:使用专用线程池处理日志写入 +4. **非阻塞**:队列满时丢弃日志而不阻塞 +5. **级别过滤**:只处理ERROR及以上级别,减少数据库压力 + +## 注意事项 + +1. **日志级别**:默认只记录ERROR级别及以上的日志到数据库 +2. **队列容量**:队列容量10000,超出会丢弃(打印警告到stderr) +3. **关闭等待**:应用关闭时会等待60秒完成日志写入 +4. **循环依赖**:LogEventHandler中不要使用logger.error(),会造成循环 +5. **性能影响**:每次ERROR日志会触发数据库写入,避免频繁ERROR日志 + +## 监控建议 + +1. 定期检查 `tb_sys_operation_log` 表大小 +2. 定期归档或删除旧日志(如30天前的) +3. 监控队列满的告警(stderr输出) +4. 监控数据库写入性能 + +## 扩展功能 + +### 自定义日志级别过滤 + +修改 `LogEventHandler.handleLogEvent()` 方法: + +```java +// 改为记录WARN及以上级别 +if (event.getLevel().isMoreSpecificThan(org.apache.logging.log4j.Level.WARN)) { + // ... +} +``` + +### 添加自定义字段 + +在 `buildOperationLog()` 方法中添加自定义字段提取逻辑。 + +### 批量写入优化 + +可以修改为收集多条日志后批量写入数据库,进一步提升性能。 + diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/LoginLogServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/LoginLogServiceImpl.java new file mode 100644 index 0000000..d734f88 --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/LoginLogServiceImpl.java @@ -0,0 +1,74 @@ +package org.xyzh.system.log.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.xyzh.api.system.log.LoginLogService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageDomain; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.dto.system.TbSysLoginLog; +import org.xyzh.common.vo.UserDeptRoleVO; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xyzh.system.mapper.SysLoginLogMapper; +import org.xyzh.system.utils.LoginUtil; + +@Service +public class LoginLogServiceImpl implements LoginLogService { + + private static final Logger logger = LoggerFactory.getLogger(LoginLogServiceImpl.class); + + @Autowired + private SysLoginLogMapper sysLoginLogMapper; + + @Override + public ResultDomain insertLoginLog(TbSysLoginLog loginLog) { + ResultDomain result = new ResultDomain<>(); + int rows = sysLoginLogMapper.insertLoginLog(loginLog); + if (rows > 0) { + result.success("插入登录日志成功", loginLog); + } else { + result.fail( "插入登录日志失败"); + } + logger.info("insertLoginLog: {}", loginLog); + return result; + } + + @Override + public ResultDomain selectLoginLogList(TbSysLoginLog loginLog) { + ResultDomain result = new ResultDomain<>(); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List list = sysLoginLogMapper.selectLoginLogList(loginLog, userDeptRoles); + if (list != null) { + result.success("查询登录日志成功", list); + } else { + result.fail("查询登录日志失败"); + } + return result; + } + + @Override + public ResultDomain selectLoginLogPage(TbSysLoginLog loginLog, PageParam pageParam) { + ResultDomain result = new ResultDomain<>(); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List list = sysLoginLogMapper.selectLoginLogPage(loginLog, pageParam, userDeptRoles); + PageDomain pageDomain = new PageDomain<>(); + int count = sysLoginLogMapper.countLoginLog(loginLog, userDeptRoles); + pageParam.setTotalElements(count); + pageParam.setTotalPages((int)Math.ceil(count / pageParam.getPageSize() )); + + pageDomain.setPageParam(pageParam); + pageDomain.setDataList(list); + if (list != null) { + result.success("查询登录日志成功", pageDomain); + } else { + result.fail("查询登录日志失败"); + } + return result; + + } + +} diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/SysOperationLogServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/SysOperationLogServiceImpl.java new file mode 100644 index 0000000..3df89be --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/log/service/impl/SysOperationLogServiceImpl.java @@ -0,0 +1,64 @@ +package org.xyzh.system.log.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.xyzh.api.system.log.OperationLogService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageDomain; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.dto.system.TbSysOperationLog; +import org.xyzh.common.vo.UserDeptRoleVO; +import org.xyzh.system.mapper.SysOperationLogMapper; +import org.xyzh.system.utils.LoginUtil; + +import java.util.List; + +/** + * @description 操作日志服务实现 + * @filename SysOperationLogServiceImpl.java + * @author yslg + * @copyright xyzh + * @since 2025-10-30 + */ +@Service +public class SysOperationLogServiceImpl implements OperationLogService { + + @Autowired + private SysOperationLogMapper operationLogMapper; + + @Override + public ResultDomain selectOperationLogPage(TbSysOperationLog operationLog, PageParam pageParam) { + ResultDomain result = new ResultDomain<>(); + + try { + // 获取当前用户的部门角色信息(用于权限过滤) + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + + // 分页查询操作日志 + List logs = operationLogMapper.selectOperationLogPage(operationLog, pageParam, userDeptRoles); + + // 统计总数(带权限过滤) + int count = operationLogMapper.countOperationLog(operationLog, userDeptRoles); + pageParam.setTotalElements(count); + pageParam.setTotalPages((int) Math.ceil((double) count / pageParam.getPageSize())); + + // 构建分页结果 + PageDomain pageDomain = new PageDomain<>(); + pageDomain.setPageParam(pageParam); + pageDomain.setDataList(logs); + + result.success("查询成功", pageDomain); + } catch (Exception e) { + result.fail("查询失败: " + e.getMessage()); + } + + return result; + } + + @Override + public int countOperationLog(TbSysOperationLog operationLog) { + // 获取当前用户的部门角色信息(用于权限过滤) + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + return operationLogMapper.countOperationLog(operationLog, userDeptRoles); + } +} diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/DeptRoleMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/DeptRoleMapper.java index b14064a..801adca 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/DeptRoleMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/DeptRoleMapper.java @@ -7,20 +7,19 @@ import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.xyzh.common.dto.dept.TbSysDeptRole; -import org.xyzh.common.dto.role.TbSysRole; import org.xyzh.common.vo.UserDeptRoleVO; @Mapper public interface DeptRoleMapper extends BaseMapper { /** - * @description 查询部门绑定角色 + * @description 查询部门绑定角色(包含名称) * @param deptId 部门ID - * @return List 角色列表 + * @return List 部门角色列表 * @author yslg * @since 2025-09-28 */ - List selectDeptRole(String deptId); + List selectDeptRole(String deptId); /** * @description 查询部门绑定角色列表(包含名称) diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/MenuPermissionMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/MenuPermissionMapper.java index 2e22b82..7f35151 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/MenuPermissionMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/MenuPermissionMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.xyzh.common.dto.menu.TbSysMenuPermission; +import org.xyzh.common.vo.PermissionVO; import java.util.List; @@ -49,13 +50,13 @@ public interface MenuPermissionMapper extends BaseMapper { @Param("permissionId") String permissionId); /** - * @description 根据菜单ID查询权限关联列表 + * @description 根据菜单ID查询权限列表(包含模块名称等信息) * @param menuId 菜单ID - * @return List 菜单权限关联列表 + * @return List 权限列表 * @author yslg * @since 2025-10-07 */ - List selectByMenuId(@Param("menuId") String menuId); + List selectByMenuId(@Param("menuId") String menuId); /** * @description 根据权限ID查询菜单关联列表 diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/PermissionMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/PermissionMapper.java index 240f95d..abecf1f 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/PermissionMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/PermissionMapper.java @@ -141,22 +141,22 @@ public interface PermissionMapper extends BaseMapper { PermissionVO selectPermissionVO(@Param("permission") PermissionVO permission); /** - * @description 查询权限绑定菜单 + * @description 查询权限绑定菜单(包含菜单名称描述) * @param permission 权限对象 - * @return List 权限绑定菜单列表 + * @return List 权限VO列表(包含菜单信息) * @author yslg * @since 2025-10-08 */ - List selectPermissionBindMenu(@Param("permission") PermissionVO permission); + List selectPermissionBindMenu(@Param("permission") PermissionVO permission); /** - * @description 查询权限绑定角色 + * @description 查询权限绑定角色(包含角色名称描述和创建人更新人) * @param permission 权限对象 - * @return List 权限绑定角色列表 + * @return List 权限VO列表(包含角色信息) * @author yslg * @since 2025-10-08 */ - List selectPermissionBindRole(@Param("permission") PermissionVO permission); + List selectPermissionBindRole(@Param("permission") PermissionVO permission); /** * @description 根据模块ID查询权限列表 diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/RoleMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/RoleMapper.java index fa23618..1cf1248 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/RoleMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/RoleMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.xyzh.common.dto.role.TbSysRole; +import org.xyzh.common.vo.PermissionVO; import org.xyzh.common.vo.UserDeptRoleVO; import java.util.List; @@ -24,16 +25,24 @@ public interface RoleMapper extends BaseMapper { * @author yslg * @since 2025-10-09 */ - List selectAllRoles(); + /** + * @description 查询所有角色(包含权限过滤和创建人更新人名称) + * @param userDeptRoles 用户部门角色列表(用于权限过滤) + * @return List 角色VO列表 + * @author yslg + * @since 2025-10-30 + */ + List selectAllRoles(@Param("userDeptRoles") List userDeptRoles); /** - * @description 根据过滤条件查询角色列表 + * @description 根据过滤条件查询角色列表(包含权限过滤) * @param filter 过滤条件 - * @return List 角色列表 + * @param userDeptRoles 用户部门角色列表(用于权限过滤) + * @return List 角色VO列表 * @author yslg * @since 2025-10-09 */ - List selectRole(TbSysRole filter); + List selectRole(@Param("filter") TbSysRole filter, @Param("userDeptRoles") List userDeptRoles); /** * @description 插入角色 @@ -71,35 +80,19 @@ public interface RoleMapper extends BaseMapper { */ List selectDeptRolesByUserId(@Param("userId") String userId); - /** - * @description 根据角色编码查询角色 - * @param roleCode 角色编码 - * @return TbSysRole 角色信息 - * @author yslg - * @since 2025-09-28 - */ - TbSysRole selectByRoleCode(@Param("roleCode") String roleCode); /** - * @description 检查角色名称是否存在 + * @description 检查角色名称是否存在(包含权限过滤) * @param roleName 角色名称 * @param excludeId 排除的角色ID + * @param userDeptRoles 用户部门角色列表(用于权限过滤) * @return int 存在数量 * @author yslg * @since 2025-09-28 */ - int countByRoleName(@Param("roleName") String roleName, @Param("excludeId") String excludeId); - - /** - * @description 检查角色编码是否存在 - * @param roleCode 角色编码 - * @param excludeId 排除的角色ID - * @return int 存在数量 - * @author yslg - * @since 2025-09-28 - */ - int countByRoleCode(@Param("roleCode") String roleCode, @Param("excludeId") String excludeId); + int countByRoleName(@Param("roleName") String roleName, @Param("excludeId") String excludeId, @Param("userDeptRoles") List userDeptRoles); + /** * @description 批量删除角色(逻辑删除) * @param roleIds 角色ID列表 @@ -111,7 +104,7 @@ public interface RoleMapper extends BaseMapper { int batchDeleteByIds(@Param("roleIds") List roleIds, @Param("updater") String updater); /** - * @description 检查角色是否存在 + * @description 检查角色是否存在(精确查询,不需要权限过滤) * @param roleIds 角色ID列表 * @return List 角色列表 * @author yslg diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysLoginLogMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysLoginLogMapper.java new file mode 100644 index 0000000..4578cb6 --- /dev/null +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysLoginLogMapper.java @@ -0,0 +1,23 @@ +package org.xyzh.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.dto.system.TbSysLoginLog; +import org.xyzh.common.vo.UserDeptRoleVO; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +@Mapper +public interface SysLoginLogMapper extends BaseMapper { + + int insertLoginLog(TbSysLoginLog loginLog); + + List selectLoginLogList(@Param("loginLog") TbSysLoginLog loginLog, @Param("userDeptRoles") List userDeptRoles); + + List selectLoginLogPage(@Param("loginLog") TbSysLoginLog loginLog, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List userDeptRoles); + + int countLoginLog(@Param("loginLog") TbSysLoginLog loginLog, @Param("userDeptRoles") List userDeptRoles); +} diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysOperationLogMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysOperationLogMapper.java index 15554b5..04b4eeb 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysOperationLogMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/SysOperationLogMapper.java @@ -2,7 +2,10 @@ package org.xyzh.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.common.core.page.PageParam; import org.xyzh.common.dto.system.TbSysOperationLog; +import org.xyzh.common.vo.UserDeptRoleVO; import java.util.List; @@ -17,11 +20,46 @@ import java.util.List; public interface SysOperationLogMapper extends BaseMapper { /** - * @description 查询操作日志列表 - * @param filter 过滤条件 + * @description 插入操作日志 + * @param operationLog 操作日志对象 + * @return int 影响行数 + * @author yslg + * @since 2025-10-30 + */ + int insertOperationLog(TbSysOperationLog operationLog); + + /** + * @description 查询操作日志列表(带权限过滤) + * @param operationLog 过滤条件 + * @param userDeptRoles 当前用户的部门角色列表 * @return List 操作日志列表 * @author yslg - * @since 2025-10-15 + * @since 2025-10-30 */ - List selectSysOperationLogs(TbSysOperationLog filter); + List selectOperationLogList(@Param("operationLog") TbSysOperationLog operationLog, + @Param("userDeptRoles") List userDeptRoles); + + /** + * @description 分页查询操作日志列表(带权限过滤) + * @param operationLog 过滤条件 + * @param pageParam 分页参数 + * @param userDeptRoles 当前用户的部门角色列表 + * @return List 操作日志列表 + * @author yslg + * @since 2025-10-30 + */ + List selectOperationLogPage(@Param("operationLog") TbSysOperationLog operationLog, + @Param("pageParam") PageParam pageParam, + @Param("userDeptRoles") List userDeptRoles); + + /** + * @description 统计操作日志数量(带权限过滤) + * @param operationLog 过滤条件 + * @param userDeptRoles 当前用户的部门角色列表 + * @return int 日志数量 + * @author yslg + * @since 2025-10-30 + */ + int countOperationLog(@Param("operationLog") TbSysOperationLog operationLog, + @Param("userDeptRoles") List userDeptRoles); } diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java index 3175757..100ac02 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java @@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Param; import org.xyzh.common.core.page.PageParam; import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.common.dto.user.TbSysUserInfo; +import org.xyzh.common.vo.UserDeptRoleVO; import org.xyzh.common.vo.UserVO; import java.util.List; @@ -20,6 +21,8 @@ import java.util.List; @Mapper public interface UserMapper extends BaseMapper { + TbSysUser selectLoginUser(@Param("filter") TbSysUser filter); + /** * @description 插入用户 * @param user 用户信息 @@ -75,36 +78,61 @@ public interface UserMapper extends BaseMapper { TbSysUser selectByPhone(@Param("phone") String phone); /** - * @description 根据过滤条件查询用户 + * @description 根据过滤条件查询用户(包含权限过滤) * @param filter 过滤条件 + * @param userDeptRoles 用户部门角色列表 * @return TbSysUser 用户信息 * @author yslg * @since 2025-09-28 */ - List selectByFilter(@Param("filter") TbSysUser filter); + List selectByFilter(@Param("filter") TbSysUser filter, @Param("userDeptRoles") List userDeptRoles); /** - * @description 查询用户列表(分页) + * @description 根据过滤条件查询用户VO列表(包含userinfo和deptrole信息,包含权限过滤) + * @param filter 过滤条件 + * @param userDeptRoles 用户部门角色列表 + * @return List 用户VO列表 + * @author yslg + * @since 2025-10-30 + */ + List selectUserVOByFilter(@Param("filter") TbSysUser filter, @Param("userDeptRoles") List userDeptRoles); + + /** + * @description 查询用户列表(包含权限过滤) * @param username 用户名(模糊查询) * @param email 邮箱(模糊查询) * @param status 用户状态 + * @param userDeptRoles 用户部门角色列表 * @return List 用户列表 * @author yslg * @since 2025-09-28 */ List selectUserList(@Param("username") String username, @Param("email") String email, - @Param("status") String status); + @Param("status") String status, + @Param("userDeptRoles") List userDeptRoles); /** - * @description 查询用户列表(分页) + * @description 查询用户列表(分页,包含权限过滤) * @param filter 过滤条件 * @param pageParam 分页参数 + * @param userDeptRoles 用户部门角色列表 * @return List 用户列表 * @author yslg * @since 2025-09-28 */ - List selectUserPage(@Param("filter") TbSysUser filter, @Param("pageParam") PageParam pageParam); + List selectUserPage(@Param("filter") TbSysUser filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List userDeptRoles); + + /** + * @description 查询用户VO列表(分页,包含userinfo和deptrole信息,包含权限过滤) + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @param userDeptRoles 用户部门角色列表 + * @return List 用户VO列表 + * @author yslg + * @since 2025-10-30 + */ + List selectUserVOPage(@Param("filter") TbSysUser filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List userDeptRoles); /** * @description 批量删除用户(逻辑删除) @@ -153,4 +181,7 @@ public interface UserMapper extends BaseMapper { * @since 2025-10-18 */ UserVO selectUserInfoTotal(@Param("userId") String userId); + + + int countDeptUser(@Param("deptId") String deptId); } diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/menu/service/impl/SysMenuServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/menu/service/impl/SysMenuServiceImpl.java index 38230f9..7b3ebe1 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/menu/service/impl/SysMenuServiceImpl.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/menu/service/impl/SysMenuServiceImpl.java @@ -9,6 +9,7 @@ import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.menu.TbSysMenu; import org.xyzh.common.dto.menu.TbSysMenuPermission; import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.vo.PermissionVO; import org.xyzh.system.mapper.MenuMapper; import org.xyzh.system.mapper.MenuPermissionMapper; import org.xyzh.system.menu.service.SysMenuService; @@ -463,16 +464,13 @@ public class SysMenuServiceImpl implements SysMenuService { } @Override - public ResultDomain getMenuPermission(String menuId) { - ResultDomain resultDomain = new ResultDomain<>(); + public ResultDomain getMenuPermission(String menuId) { + ResultDomain resultDomain = new ResultDomain<>(); try { logger.info("开始查询菜单权限:{}", menuId); - List menuPermissions = menuPermissionMapper.selectByMenuId(menuId); - if (menuPermissions.isEmpty()) { - resultDomain.fail("未找到菜单权限"); - return resultDomain; - } - resultDomain.success("查询菜单权限成功", menuPermissions); + List permissions = menuPermissionMapper.selectByMenuId(menuId); + logger.info("查询菜单权限完成,共找到{}个权限", permissions.size()); + resultDomain.success("查询菜单权限成功", permissions); return resultDomain; } catch (Exception e) { logger.error("查询菜单权限异常:{}", menuId, e); diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/permission/service/impl/SysPermissionServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/permission/service/impl/SysPermissionServiceImpl.java index 6782369..99d7315 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/permission/service/impl/SysPermissionServiceImpl.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/permission/service/impl/SysPermissionServiceImpl.java @@ -301,14 +301,14 @@ public class SysPermissionServiceImpl implements SysPermissionService { PermissionVO permissionVO = permissionMapper.selectPermissionVO(permission); if (permission.getBindType().equals("menu")) { - List bindMenus = permissionMapper.selectPermissionBindMenu(permission); - permissionVO.setMenus(bindMenus); - resultDomain.success("查询权限绑定列表成功", permissionVO); + List bindMenus = permissionMapper.selectPermissionBindMenu(permission); + logger.info("查询权限绑定菜单列表完成,共找到{}个菜单", bindMenus.size()); + resultDomain.success("查询权限绑定菜单列表成功", bindMenus); return resultDomain; } else { - List bindRoles = permissionMapper.selectPermissionBindRole(permission); - permissionVO.setRoles(bindRoles); - resultDomain.success("查询权限绑定角色列表成功", permissionVO); + List bindRoles = permissionMapper.selectPermissionBindRole(permission); + logger.info("查询权限绑定角色列表完成,共找到{}个角色", bindRoles.size()); + resultDomain.success("查询权限绑定角色列表成功", bindRoles); return resultDomain; } } diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/role/service/impl/SysRoleServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/role/service/impl/SysRoleServiceImpl.java index d266967..3d8bb2e 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/role/service/impl/SysRoleServiceImpl.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/role/service/impl/SysRoleServiceImpl.java @@ -8,12 +8,15 @@ import org.springframework.util.StringUtils; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.permission.TbSysPermission; import org.xyzh.common.dto.role.TbSysRole; +import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.vo.PermissionVO; import org.xyzh.common.vo.UserDeptRoleVO; import org.xyzh.system.mapper.RolePermissionMapper; import org.xyzh.system.mapper.RoleMapper; import org.xyzh.system.mapper.UserDeptRoleMapper; import org.xyzh.system.role.service.SysRoleService; +import org.xyzh.system.utils.LoginUtil; import java.util.Date; import java.util.List; @@ -40,13 +43,19 @@ public class SysRoleServiceImpl implements SysRoleService { private UserDeptRoleMapper userDeptRoleMapper; @Override - public ResultDomain getAllRoles() { - ResultDomain resultDomain = new ResultDomain<>(); + public ResultDomain getAllRoles() { + ResultDomain resultDomain = new ResultDomain<>(); try { - logger.info("开始查询所有角色"); + logger.info("开始查询所有角色(包含权限过滤)"); - List roles = roleMapper.selectAllRoles(); + // 获取当前用户的部门角色信息 + TbSysUser currentUser = LoginUtil.getCurrentUser(); + List userDeptRoles = roleMapper.selectDeptRolesByUserId(currentUser.getID()); + logger.info("当前用户拥有 {} 个部门角色", userDeptRoles.size()); + + // 查询有权限的角色列表 + List roles = roleMapper.selectAllRoles(userDeptRoles); logger.info("查询所有角色完成,共找到{}个角色", roles.size()); resultDomain.success("查询成功", roles); @@ -60,12 +69,18 @@ public class SysRoleServiceImpl implements SysRoleService { } @Override - public ResultDomain getRoleList(TbSysRole filter) { - ResultDomain resultDomain = new ResultDomain<>(); + public ResultDomain getRoleList(TbSysRole filter) { + ResultDomain resultDomain = new ResultDomain<>(); try { - logger.info("开始根据过滤条件查询角色列表:{}", filter); + logger.info("开始根据过滤条件查询角色列表(包含权限过滤):{}", filter); - List roles = roleMapper.selectRole(filter); + // 获取当前用户的部门角色信息 + TbSysUser currentUser = LoginUtil.getCurrentUser(); + List userDeptRoles = roleMapper.selectDeptRolesByUserId(currentUser.getID()); + logger.info("当前用户拥有 {} 个部门角色", userDeptRoles.size()); + + // 查询有权限的角色列表 + List roles = roleMapper.selectRole(filter, userDeptRoles); logger.info("查询角色列表完成,共找到{}个角色", roles.size()); resultDomain.success("查询成功", roles); @@ -233,18 +248,28 @@ public class SysRoleServiceImpl implements SysRoleService { resultDomain.fail("角色ID不能为空"); return resultDomain; } + // ByID 是精确查询,不需要权限过滤 TbSysRole filter = new TbSysRole(); filter.setRoleID(roleId); filter.setDeleted(false); - List roles = roleMapper.selectRole(filter); - TbSysRole role = roles.isEmpty() ? null : roles.get(0); + // 获取当前用户的部门角色信息 + TbSysUser currentUser = LoginUtil.getCurrentUser(); + List userDeptRoles = roleMapper.selectDeptRolesByUserId(currentUser.getID()); + List roleVOs = roleMapper.selectRole(filter, userDeptRoles); - if (role == null) { + if (roleVOs.isEmpty()) { logger.warn("未找到角色:{}", roleId); resultDomain.fail("未找到指定角色"); return resultDomain; } + // 将 PermissionVO 转换为 TbSysRole + PermissionVO roleVO = roleVOs.get(0); + TbSysRole role = new TbSysRole(); + role.setRoleID(roleVO.getRoleID()); + role.setName(roleVO.getRoleName()); + role.setDescription(roleVO.getRoleDescription()); + logger.info("根据ID查询角色完成:{}", roleId); resultDomain.success("查询成功", role); return resultDomain; @@ -291,7 +316,11 @@ public class SysRoleServiceImpl implements SysRoleService { return resultDomain; } - int count = roleMapper.countByRoleName(roleName, excludeId); + // 获取当前用户的部门角色信息(用于权限过滤) + TbSysUser currentUser = LoginUtil.getCurrentUser(); + List userDeptRoles = roleMapper.selectDeptRolesByUserId(currentUser.getID()); + + int count = roleMapper.countByRoleName(roleName, excludeId, userDeptRoles); boolean exists = count > 0; logger.info("角色名称存在性检查完成:{},存在:{}", roleName, exists); diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/user/service/impl/SysUserServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/user/service/impl/SysUserServiceImpl.java index ed6d121..997b534 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/user/service/impl/SysUserServiceImpl.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/user/service/impl/SysUserServiceImpl.java @@ -113,8 +113,9 @@ public class SysUserServiceImpl implements SysUserService { try { logger.info("开始查询所有用户"); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); TbSysUser filter = new TbSysUser(); - List users = userMapper.selectByFilter(filter); + List users = userMapper.selectByFilter(filter, userDeptRoles); logger.info("查询所有用户完成,共找到{}个用户", users.size()); resultDomain.success("查询成功", users); @@ -138,11 +139,12 @@ public class SysUserServiceImpl implements SysUserService { return resultDomain; } + List userDeptRoles = LoginUtil.getCurrentDeptRole(); TbSysUser filter = new TbSysUser(); filter.setID(userId); filter.setDeleted(false); - List users = userMapper.selectByFilter(filter); + List users = userMapper.selectByFilter(filter, userDeptRoles); if (users.isEmpty()) { logger.warn("未找到用户:{}", userId); @@ -213,7 +215,8 @@ public class SysUserServiceImpl implements SysUserService { // return resultDomain; // } - List users = userMapper.selectByFilter(filter); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List users = userMapper.selectByFilter(filter, userDeptRoles); if (users.isEmpty()) { logger.warn("未找到符合条件的用户:{}", filter); @@ -232,13 +235,75 @@ public class SysUserServiceImpl implements SysUserService { } } + @Override + public ResultDomain getUserVOByFilter(TbSysUser filter) { + ResultDomain resultDomain = new ResultDomain<>(); + try { + logger.info("开始根据过滤条件查询用户VO:{}", filter); + + if (filter == null) { + resultDomain.fail("过滤条件不能为空"); + return resultDomain; + } + + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List userVOs = userMapper.selectUserVOByFilter(filter, userDeptRoles); + + if (userVOs.isEmpty()) { + logger.warn("未找到符合条件的用户:{}", filter); + resultDomain.fail("未找到指定用户"); + return resultDomain; + } + + logger.info("成功查询到 {} 个用户", userVOs.size()); + resultDomain.success("查询成功", userVOs); + return resultDomain; + } catch (Exception e) { + logger.error("根据过滤条件查询用户VO失败:{}", filter, e); + resultDomain.fail("查询用户失败:" + e.getMessage()); + return resultDomain; + } + } + + @Override + public ResultDomain getLoginUser(TbSysUser filter) { + ResultDomain resultDomain = new ResultDomain<>(); + TbSysUser users = userMapper.selectLoginUser(filter); + resultDomain.success("查询成功", users); + return resultDomain; + + } + @Override public ResultDomain getUserPage(TbSysUser filter, PageParam pageParam) { ResultDomain resultDomain = new ResultDomain<>(); - List users = userMapper.selectUserPage(filter, pageParam); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List users = userMapper.selectUserPage(filter, pageParam, userDeptRoles); PageDomain pageDomain = new PageDomain<>(); + + int count = userMapper.countDeptUser(userDeptRoles.get(0).getDeptID()); pageDomain.setDataList(users); pageDomain.setPageParam(pageParam); + pageParam.setTotalElements(count); + pageParam.setTotalPages((int) Math.ceil(count / pageParam.getPageSize())); + pageDomain.setPageParam(pageParam); + resultDomain.success("查询成功", pageDomain); + return resultDomain; + } + + @Override + public ResultDomain getUserVOPage(TbSysUser filter, PageParam pageParam) { + ResultDomain resultDomain = new ResultDomain<>(); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List userVOs = userMapper.selectUserVOPage(filter, pageParam, userDeptRoles); + PageDomain pageDomain = new PageDomain<>(); + + int count = userMapper.countDeptUser(userDeptRoles.get(0).getDeptID()); + pageDomain.setDataList(userVOs); + pageDomain.setPageParam(pageParam); + pageParam.setTotalElements(count); + pageParam.setTotalPages((int) Math.ceil((double)count / pageParam.getPageSize())); + pageDomain.setPageParam(pageParam); resultDomain.success("查询成功", pageDomain); return resultDomain; } @@ -503,6 +568,7 @@ public class SysUserServiceImpl implements SysUserService { return resultDomain; } + List userDeptRoles = LoginUtil.getCurrentDeptRole(); TbSysUser filter = new TbSysUser(); if (StringUtils.hasText(excludeId)) { filter.setID(excludeId); @@ -510,7 +576,7 @@ public class SysUserServiceImpl implements SysUserService { filter.setUsername(username); filter.setDeleted(false); - long count = userMapper.selectByFilter(filter).size(); + long count = userMapper.selectByFilter(filter, userDeptRoles).size(); boolean exists = count > 0; logger.info("用户名存在性检查完成:{},存在:{}", username, exists); @@ -535,6 +601,7 @@ public class SysUserServiceImpl implements SysUserService { return resultDomain; } + List userDeptRoles = LoginUtil.getCurrentDeptRole(); TbSysUser filter = new TbSysUser(); filter.setEmail(email); filter.setDeleted(false); @@ -543,7 +610,7 @@ public class SysUserServiceImpl implements SysUserService { filter.setID(excludeId); } - long count = userMapper.selectByFilter(filter).size(); + long count = userMapper.selectByFilter(filter, userDeptRoles).size(); boolean exists = count > 0; logger.info("邮箱存在性检查完成:{},存在:{}", email, exists); @@ -563,7 +630,8 @@ public class SysUserServiceImpl implements SysUserService { try { logger.info("开始搜索用户,用户名:{},邮箱:{},状态:{}", username, email, status); - List users = userMapper.selectUserList(username, email, status); + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + List users = userMapper.selectUserList(username, email, status, userDeptRoles); logger.info("搜索用户完成,共找到{}个用户", users.size()); resultDomain.success("搜索成功", users); diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/utils/LoginUtil.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/utils/LoginUtil.java index 4f9024c..43597b3 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/utils/LoginUtil.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/utils/LoginUtil.java @@ -11,7 +11,7 @@ import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.common.dto.user.TbSysUserInfo; import org.xyzh.common.redis.service.RedisService; import org.xyzh.common.utils.NonUtils; -import org.xyzh.common.utils.ServletUtil; +import org.xyzh.common.utils.ServletUtils; import org.xyzh.common.vo.UserDeptRoleVO; /** @@ -48,7 +48,7 @@ public class LoginUtil { public static LoginDomain getCurrentLoginDomain() { try { // 从当前请求获取token - String token = ServletUtil.getToken(); + String token = ServletUtils.getToken(); if (NonUtils.isEmpty(token)) { return null; } @@ -238,6 +238,6 @@ public class LoginUtil { * @since 2025-10-07 */ public static String getCurrentToken() { - return ServletUtil.getToken(); + return ServletUtils.getToken(); } } diff --git a/schoolNewsServ/system/src/main/resources/mapper/DeptRoleMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/DeptRoleMapper.xml index 32db2c7..7f666b0 100644 --- a/schoolNewsServ/system/src/main/resources/mapper/DeptRoleMapper.xml +++ b/schoolNewsServ/system/src/main/resources/mapper/DeptRoleMapper.xml @@ -14,7 +14,26 @@ - + + + + + + + + + + + + + + + + + + + + id, dept_id, role_id, creator, updater, @@ -29,25 +48,24 @@ - + - SELECT - - FROM tb_sys_dept_role - - ORDER BY create_time DESC + dr.dept_id, + d.name AS dept_name, + d.description AS dept_description, + dr.role_id, + r.name AS role_name, + r.description AS role_description + FROM tb_sys_dept_role dr + LEFT JOIN tb_sys_dept d ON dr.dept_id = d.dept_id AND d.deleted = 0 + LEFT JOIN tb_sys_role r ON dr.role_id = r.role_id AND r.deleted = 0 + WHERE dr.deleted = 0 + AND dr.dept_id = #{deptId} + ORDER BY dr.create_time DESC - - - - - - - - - - - SELECT - - FROM tb_sys_menu_permission - WHERE deleted = 0 - AND menu_id = #{menuId} - ORDER BY create_time ASC + mp.id, + p.module_id, + m.name AS module_name, + m.description AS module_description, + mp.permission_id, + p.name AS permission_name, + p.code AS permission_code, + p.description AS permission_description, + mp.creator, + mp.updater, + mp.create_time, + mp.update_time, + mp.delete_time, + mp.deleted + FROM tb_sys_menu_permission mp + LEFT JOIN tb_sys_permission p ON mp.permission_id = p.permission_id AND p.deleted = 0 + LEFT JOIN tb_sys_module m ON p.module_id = m.module_id AND m.deleted = 0 + WHERE mp.deleted = 0 + AND mp.menu_id = #{menuId} + ORDER BY mp.create_time ASC diff --git a/schoolNewsServ/system/src/main/resources/mapper/PermissionMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/PermissionMapper.xml index 1d19de4..23b6440 100644 --- a/schoolNewsServ/system/src/main/resources/mapper/PermissionMapper.xml +++ b/schoolNewsServ/system/src/main/resources/mapper/PermissionMapper.xml @@ -20,6 +20,9 @@ + + + @@ -47,6 +50,26 @@ + + + + + + + + + + + + + + + + + + + + id, permission_id, name, code, description, module_id, creator, updater, @@ -226,13 +249,15 @@ SELECT - tsp.id, tsp.permission_id, tsp.name, tsp.code, tsp.description, + tsp.id, tsp.module_id, tsp.permission_id, tsp.name, tsp.code, tsp.description, tsp.creator, tsp.updater, tsu.username as creator_name, tuu.username as updater_name, + m.name as module_name, m.description as module_description, tsp.create_time, tsp.update_time, tsp.delete_time, tsp.deleted FROM tb_sys_permission tsp INNER JOIN tb_sys_user tsu ON tsp.creator = tsu.id LEFT JOIN tb_sys_user tuu ON tsp.updater = tuu.id + LEFT JOIN tb_sys_module m ON tsp.module_id = m.module_id AND m.deleted = 0 WHERE tsp.deleted = 0 AND tsp.name LIKE CONCAT('%', #{permission.name}, '%') @@ -269,9 +296,12 @@ - SELECT - tsm.id, tsm.menu_id, tsm.name + tsm.id, + tsm.menu_id, + tsm.name AS menu_name, + tsm.url AS menu_url FROM tb_sys_menu tsm INNER JOIN tb_sys_menu_permission tsmp ON tsmp.menu_id = tsm.menu_id WHERE tsm.deleted = 0 @@ -281,11 +311,20 @@ - SELECT - tsr.id, tsr.role_id, tsr.name, tsr.description + tsr.id, + tsr.role_id, + tsr.name AS role_name, + tsr.description AS role_description, + tsr.creator, + cu.username AS creator_name, + tsr.updater, + uu.username AS updater_name FROM tb_sys_role tsr INNER JOIN tb_sys_role_permission tsrp ON tsrp.role_id = tsr.role_id + LEFT JOIN tb_sys_user cu ON tsr.creator = cu.id AND cu.deleted = 0 + LEFT JOIN tb_sys_user uu ON tsr.updater = uu.id AND uu.deleted = 0 WHERE tsr.deleted = 0 AND tsrp.deleted = 0 AND tsrp.permission_id = #{permission.permissionID} diff --git a/schoolNewsServ/system/src/main/resources/mapper/RoleMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/RoleMapper.xml index db00491..e0be83a 100644 --- a/schoolNewsServ/system/src/main/resources/mapper/RoleMapper.xml +++ b/schoolNewsServ/system/src/main/resources/mapper/RoleMapper.xml @@ -47,23 +47,97 @@ - + + + INNER JOIN tb_resource_permission rp ON r.role_id = rp.resource_id + AND rp.resource_type = 5 + AND rp.deleted = 0 + AND rp.can_read = 1 + AND ( + -- 全局权限:所有用户可访问 + (rp.dept_id IS NULL AND rp.role_id IS NULL) + + OR EXISTS ( + SELECT 1 + FROM ( + + SELECT #{udr.deptID} AS dept_id, #{udr.deptPath} AS dept_path, #{udr.roleID} AS role_id + + ) user_roles + LEFT JOIN tb_sys_dept perm_dept ON perm_dept.dept_id = rp.dept_id AND perm_dept.deleted = 0 + WHERE + -- 部门级权限:当前部门或父部门(通过dept_path判断继承关系) + (rp.role_id IS NULL AND rp.dept_id IS NOT NULL + AND user_roles.dept_path LIKE CONCAT(perm_dept.dept_path, '%')) + -- 角色级权限:跨部门的角色权限 + OR (rp.dept_id IS NULL AND rp.role_id = user_roles.role_id) + -- 精确权限:特定部门的特定角色 + OR (rp.dept_id = user_roles.dept_id AND rp.role_id = user_roles.role_id) + ) + + ) + - + SELECT DISTINCT + r.id, + r.role_id, + r.name AS role_name, + r.description AS role_description, + r.creator, + cu.username AS creator_name, + r.updater, + uu.username AS updater_name, + r.create_time, + r.update_time + FROM tb_sys_role r + + LEFT JOIN tb_sys_user cu ON r.creator = cu.id AND cu.deleted = 0 + LEFT JOIN tb_sys_user uu ON r.updater = uu.id AND uu.deleted = 0 + WHERE r.deleted = 0 + ORDER BY r.role_id, r.create_time ASC - - + SELECT DISTINCT + r.id, + r.role_id, + r.name AS role_name, + r.description AS role_description, + r.creator, + cu.username AS creator_name, + r.updater, + uu.username AS updater_name, + r.create_time, + r.update_time + FROM tb_sys_role r + + LEFT JOIN tb_sys_user cu ON r.creator = cu.id AND cu.deleted = 0 + LEFT JOIN tb_sys_user uu ON r.updater = uu.id AND uu.deleted = 0 + WHERE r.deleted = 0 + + AND r.role_id = #{filter.roleID} + + + AND r.name LIKE CONCAT('%', #{filter.name}, '%') + + ORDER BY r.role_id, r.create_time ASC @@ -108,35 +182,15 @@ ORDER BY dr.create_time ASC - - - - + - - - @@ -155,9 +209,8 @@ - - - SELECT FROM tb_sys_role diff --git a/schoolNewsServ/system/src/main/resources/mapper/SysLoginLogMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/SysLoginLogMapper.xml new file mode 100644 index 0000000..adba7a4 --- /dev/null +++ b/schoolNewsServ/system/src/main/resources/mapper/SysLoginLogMapper.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + INNER JOIN tb_sys_user u ON ll.user_id = u.id AND u.deleted = 0 + INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0 + INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0 + WHERE ( + -- superadmin 可以查看所有登录日志 + EXISTS ( + SELECT 1 FROM ( + + SELECT #{currentRole.deptID} AS dept_id, #{currentRole.roleID} AS role_id + + ) admin_check + WHERE admin_check.dept_id = 'root_department' + AND admin_check.role_id = 'superadmin' + ) + -- 普通用户按部门路径过滤(本部门及子部门) + OR EXISTS ( + SELECT 1 FROM ( + + SELECT #{currentRole.deptPath} AS user_dept_path + + ) user_roles + WHERE d.dept_path LIKE CONCAT(user_roles.user_dept_path, '%') + ) + ) + + + + INSERT INTO tb_sys_login_log (id, user_id, username, ip_address, ip_source, browser, os, password, login_time, status, error_count, message, create_time) + VALUES (#{id}, #{userID}, #{username}, #{ipAddress}, #{ipSource}, #{browser}, #{os}, #{password}, #{loginTime, jdbcType=TIMESTAMP}, #{status}, #{errorCount}, #{message}, now()); + + + + + + + + + + + diff --git a/schoolNewsServ/system/src/main/resources/mapper/SysOperationLogMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/SysOperationLogMapper.xml index 5634d83..5606553 100644 --- a/schoolNewsServ/system/src/main/resources/mapper/SysOperationLogMapper.xml +++ b/schoolNewsServ/system/src/main/resources/mapper/SysOperationLogMapper.xml @@ -31,37 +31,99 @@ browser, os, status, error_message, execute_time, create_time - - - - - AND user_id = #{userID} - - - AND username LIKE CONCAT('%', #{username}, '%') - - - AND module = #{module} - - - AND operation = #{operation} - - - AND status = #{status} - - - AND ip_address = #{ipAddress} - - + + + INNER JOIN tb_sys_user u ON ol.user_id = u.id AND u.deleted = 0 + INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0 + INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0 + WHERE ( + -- superadmin 可以查看所有操作日志 + EXISTS ( + SELECT 1 FROM ( + + SELECT #{currentRole.deptID} AS dept_id, #{currentRole.roleID} AS role_id + + ) admin_check + WHERE admin_check.dept_id = 'root_department' + AND admin_check.role_id = 'superadmin' + ) + -- 普通用户按部门路径过滤(本部门及子部门) + OR EXISTS ( + SELECT 1 FROM ( + + SELECT #{currentRole.deptPath} AS user_dept_path + + ) user_roles + WHERE d.dept_path LIKE CONCAT(user_roles.user_dept_path, '%') + ) + ) - - + SELECT DISTINCT ol.* + FROM tb_sys_operation_log ol + + + ORDER BY ol.create_time DESC + + + + + + + diff --git a/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml index 7c2d6fc..a2a1058 100644 --- a/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml +++ b/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml @@ -33,11 +33,20 @@ + + + + + + + + + @@ -108,6 +117,31 @@ + + + INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0 + INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0 + WHERE ( + EXISTS ( + SELECT 1 FROM ( + + SELECT #{currentRole.deptID} AS dept_id, #{currentRole.roleID} AS role_id + + ) admin_check + WHERE admin_check.dept_id = 'root_department' + AND admin_check.role_id = 'superadmin' + ) + OR EXISTS ( + SELECT 1 FROM ( + + SELECT #{currentRole.deptPath} AS user_dept_path + + ) user_roles + WHERE d.dept_path LIKE CONCAT(user_roles.user_dept_path, '%') + ) + ) + + - + - + + + + - + + + + @@ -352,4 +559,26 @@ WHERE tsui.user_id = #{userId} AND tsui.deleted = 0 + + + + + + + + diff --git a/schoolNewsWeb/src/apis/study/learning-task.ts b/schoolNewsWeb/src/apis/study/learning-task.ts index 36def26..b0dbadd 100644 --- a/schoolNewsWeb/src/apis/study/learning-task.ts +++ b/schoolNewsWeb/src/apis/study/learning-task.ts @@ -42,6 +42,11 @@ export const learningTaskApi = { return response.data; }, + async getTaskUsers(taskID: string): Promise> { + const response = await api.get(`${this.learningTaskPrefix}/${taskID}/users`); + return response.data; + }, + /** * 获取任务分页列表 * @param pageParam 分页参数 diff --git a/schoolNewsWeb/src/apis/system/dept.ts b/schoolNewsWeb/src/apis/system/dept.ts index a107648..9d25674 100644 --- a/schoolNewsWeb/src/apis/system/dept.ts +++ b/schoolNewsWeb/src/apis/system/dept.ts @@ -5,7 +5,7 @@ */ import { api } from '@/apis/index'; -import type { SysDept, SysRole, DeptRoleVO, SysDeptRole, ResultDomain } from '@/types'; +import type { SysDept, SysRole, SysDeptRole, ResultDomain, UserDeptRoleVO } from '@/types'; /** * 部门API服务 @@ -89,8 +89,8 @@ export const deptApi = { * @author yslg * @ since 2025-10-06 */ - async getDeptByRole(dept: SysDept): Promise> { - const response = await api.post('/depts/role', dept); + async getDeptByRole(dept: SysDept): Promise> { + const response = await api.post('/depts/role', dept); return response.data; }, @@ -113,7 +113,7 @@ export const deptApi = { * @author yslg * @ since 2025-10-06 */ - async bindDeptRole(deptRole: DeptRoleVO): Promise> { + async bindDeptRole(deptRole: UserDeptRoleVO): Promise> { const response = await api.post('/depts/bind/role', deptRole); return response.data; }, @@ -125,7 +125,7 @@ export const deptApi = { * @author yslg * @ since 2025-10-06 */ - async unbindDeptRole(deptRole: DeptRoleVO): Promise> { + async unbindDeptRole(deptRole: UserDeptRoleVO): Promise> { const response = await api.post('/depts/unbind/role', deptRole); return response.data; } diff --git a/schoolNewsWeb/src/apis/system/index.ts b/schoolNewsWeb/src/apis/system/index.ts index 3dadd75..62b8d46 100644 --- a/schoolNewsWeb/src/apis/system/index.ts +++ b/schoolNewsWeb/src/apis/system/index.ts @@ -13,4 +13,5 @@ export { permissionApi } from './permission'; export { authApi } from './auth'; export { fileApi } from './file'; export { moduleApi } from './module'; +export { logApi } from './log'; diff --git a/schoolNewsWeb/src/apis/system/log.ts b/schoolNewsWeb/src/apis/system/log.ts new file mode 100644 index 0000000..d3097a8 --- /dev/null +++ b/schoolNewsWeb/src/apis/system/log.ts @@ -0,0 +1,33 @@ +/** + * @description 系统日志API + * @author yslg + * @since 2025-10-30 + */ + +import { api } from '../index'; +import type { LoginLog, OperationLog } from '@/types/log'; +import type { PageParam, ResultDomain } from '@/types'; + +/** + * 日志API接口 + */ +export const logApi = { + baseUrl: '/sys/log', + + async getLoginLogPage(pageParam: PageParam, filter: LoginLog): Promise> { + const response = await api.post(`${this.baseUrl}/login/page`, { + pageParam, + filter, + }); + return response.data; + }, + + async getOperationLogPage(pageParam: PageParam, filter: OperationLog): Promise> { + const response = await api.post(`${this.baseUrl}/operation/page`, { + pageParam, + filter, + }); + return response.data; + } +}; + diff --git a/schoolNewsWeb/src/apis/system/user.ts b/schoolNewsWeb/src/apis/system/user.ts index 63802d8..4b31f6d 100644 --- a/schoolNewsWeb/src/apis/system/user.ts +++ b/schoolNewsWeb/src/apis/system/user.ts @@ -43,8 +43,8 @@ export const userApi = { * @param filter 过滤条件 * @returns Promise> */ - async getUserList(filter: SysUser): Promise> { - const response = await api.post('/users/list', filter); + async getUserList(filter: SysUser): Promise> { + const response = await api.post('/users/list', filter); return response.data; }, diff --git a/schoolNewsWeb/src/components/base/GenericSelector.vue b/schoolNewsWeb/src/components/base/GenericSelector.vue index 06be997..a068ff3 100644 --- a/schoolNewsWeb/src/components/base/GenericSelector.vue +++ b/schoolNewsWeb/src/components/base/GenericSelector.vue @@ -625,12 +625,6 @@ function toggleAvailable(itemId: string) { // 树形模式下的级联选择 const node = findNodeInTree(itemId, treeData.value); if (node) { - // 如果只允许选择叶子节点,检查是否为叶子节点 - if (props.onlyLeafSelectable && !isLeafNode(node)) { - // 非叶子节点,不允许选择 - return; - } - const childrenIds = getAllChildrenIds(node); if (index > -1) { @@ -701,6 +695,11 @@ function moveSelectedToTarget() { itemsToMove = flatItems.filter(item => selectedAvailable.value.includes(getNodeId(item)) ); + + // 如果只允许选择叶子节点,过滤掉非叶子节点 + if (props.onlyLeafSelectable) { + itemsToMove = itemsToMove.filter(item => isLeafNode(item)); + } } else { // 列表模式:从可选列表中查找 itemsToMove = availableList.value.filter(item => @@ -712,8 +711,10 @@ function moveSelectedToTarget() { if (props.useTree && treeData.value.length > 0) { // 树形模式:需要重新构建树(移除已选项) + // 只移除叶子节点 + const idsToRemove = new Set(itemsToMove.map(item => getNodeId(item))); availableList.value = availableList.value.filter(item => - !selectedAvailable.value.includes(getItemId(item)) + !idsToRemove.has(getItemId(item)) ); // 重新转换为树形结构 if (props.treeTransform) { @@ -782,10 +783,27 @@ function moveToTarget(itemId: string) { function moveAllToTarget() { if (props.useTree && treeData.value.length > 0) { // 树形模式:扁平化所有节点 - const flatItems = flattenTree(treeData.value); + let flatItems = flattenTree(treeData.value); + + // 如果只允许选择叶子节点,过滤掉非叶子节点 + if (props.onlyLeafSelectable) { + flatItems = flatItems.filter(item => isLeafNode(item)); + } + targetList.value.push(...flatItems); - availableList.value = []; - treeData.value = []; + + // 从availableList中移除已移动的项 + const idsToRemove = new Set(flatItems.map(item => getNodeId(item))); + availableList.value = availableList.value.filter(item => + !idsToRemove.has(getItemId(item)) + ); + + // 重新构建树(如果还有非叶子节点) + if (props.onlyLeafSelectable && props.treeTransform) { + treeData.value = props.treeTransform(availableList.value); + } else { + treeData.value = []; + } } else { // 列表模式 targetList.value.push(...availableList.value); diff --git a/schoolNewsWeb/src/components/base/TreeNode.vue b/schoolNewsWeb/src/components/base/TreeNode.vue index 861f4e5..059bc05 100644 --- a/schoolNewsWeb/src/components/base/TreeNode.vue +++ b/schoolNewsWeb/src/components/base/TreeNode.vue @@ -3,10 +3,7 @@
@@ -25,7 +22,6 @@ @@ -107,36 +103,28 @@ const hasChildren = computed(() => children.value && children.value.length > 0); const expanded = computed(() => props.expandedKeys.has(nodeId.value)); -// 判断复选框是否应该被禁用 -const isCheckboxDisabled = computed(() => { - // 如果只允许选择叶子节点,且当前节点有子节点,则禁用复选框 - return props.onlyLeafSelectable && hasChildren.value; -}); - function getChildId(child: any): string { const idProp = props.treeProps?.id || 'id'; return String(child[idProp] || ''); } function handleClick() { - // 点击节点主体时切换选中状态(如果复选框未禁用) - if (!isCheckboxDisabled.value) { - emit('toggle-select', nodeId.value); - } + // 点击节点主体时切换选中状态 + emit('toggle-select', nodeId.value); } function handleCheckboxClick() { - // 复选框点击事件(如果未禁用) - if (!isCheckboxDisabled.value) { - emit('toggle-select', nodeId.value); - } + // 复选框点击事件 + emit('toggle-select', nodeId.value); } function handleDblClick() { - // 双击时触发(如果复选框未禁用) - if (!isCheckboxDisabled.value) { - emit('dblclick', nodeId.value); + // 双击时触发 + // 如果只允许选择叶子节点,且当前是非叶子节点,则不触发双击 + if (props.onlyLeafSelectable && hasChildren.value) { + return; } + emit('dblclick', nodeId.value); } @@ -191,11 +179,6 @@ function handleDblClick() { input[type="checkbox"] { margin: 0 8px; cursor: pointer; - - &:disabled { - cursor: not-allowed; - opacity: 0.4; - } } .node-label { @@ -203,14 +186,6 @@ function handleDblClick() { font-size: 14px; color: #606266; } - - &.disabled { - cursor: not-allowed; - - .node-label { - color: #999; - } - } } &-children { diff --git a/schoolNewsWeb/src/types/index.ts b/schoolNewsWeb/src/types/index.ts index 74aa202..f090f9c 100644 --- a/schoolNewsWeb/src/types/index.ts +++ b/schoolNewsWeb/src/types/index.ts @@ -48,6 +48,9 @@ export * from './usercenter'; // 定时任务相关 export * from './crontab'; +// 日志相关 +export * from './log'; + // 枚举类型 export * from './enums'; export * from './enums/achievement-enums'; diff --git a/schoolNewsWeb/src/types/log/index.ts b/schoolNewsWeb/src/types/log/index.ts new file mode 100644 index 0000000..43c61fe --- /dev/null +++ b/schoolNewsWeb/src/types/log/index.ts @@ -0,0 +1,84 @@ +/** + * @description 系统日志类型定义 + * @author yslg + * @since 2025-10-30 + */ + +import { BaseDTO } from '../base'; + +/** + * 登录日志 + */ +export interface LoginLog extends BaseDTO { + /** 用户名 */ + username?: string; + /** 用户ID */ + userId?: string; + /** 登录IP */ + ipAddress?: string; + /** 登录地点 */ + location?: string; + /** 浏览器 */ + browser?: string; + /** 操作系统 */ + os?: string; + /** 登录状态: success-成功, failed-失败 */ + status?: number; + /** 登录信息 */ + message?: string; + /** 登录时间 */ + loginTime?: string; + /** 部门ID */ + deptId?: string; + /** 部门名称 */ + deptName?: string; + /** 开始时间 (查询条件) */ + startTime?: string; + /** 结束时间 (查询条件) */ + endTime?: string; +} + +/** + * 操作日志 + */ +export interface OperationLog extends BaseDTO { + /** 操作人用户名 */ + username?: string; + /** 操作人ID */ + userId?: string; + /** 操作模块 */ + module?: string; + /** 操作类型: create-新增, update-修改, delete-删除, read-查询 */ + operation?: 'create' | 'update' | 'delete' | 'read' | string; + /** 操作描述 */ + description?: string; + /** 请求方法 */ + method?: string; + /** 请求路径 */ + requestUrl?: string; + /** 请求参数 */ + requestParams?: string; + /** 响应结果 */ + responseData?: string; + /** IP地址 */ + ipAddress?: string; + /** 操作地点 */ + location?: string; + /** 操作耗时(毫秒) */ + duration?: number; + /** 操作状态: success-成功, failed-失败 */ + status?: 'success' | 'failed' | string; + /** 错误信息 */ + errorMessage?: string; + /** 操作时间 */ + operationTime?: string; + /** 部门ID */ + deptId?: string; + /** 部门名称 */ + deptName?: string; + /** 开始时间 (查询条件) */ + startTime?: string; + /** 结束时间 (查询条件) */ + endTime?: string; +} + diff --git a/schoolNewsWeb/src/types/permission/index.ts b/schoolNewsWeb/src/types/permission/index.ts index 69e60e1..11b06cf 100644 --- a/schoolNewsWeb/src/types/permission/index.ts +++ b/schoolNewsWeb/src/types/permission/index.ts @@ -18,6 +18,15 @@ export interface SysPermission extends BaseDTO { /** 权限ID */ permissionID?: string; moduleID?: string; + moduleName?: string; + moduleCode?: string; + moduleDescription?: string; + menuID?: string; + menuName?: string; + menuUrl?: string; + roleID?: string; + roleName?: string; + roleDescription?: string; /** 权限名称 */ name?: string; /** 权限描述 */ diff --git a/schoolNewsWeb/src/types/study/index.ts b/schoolNewsWeb/src/types/study/index.ts index bf4704c..5f206d1 100644 --- a/schoolNewsWeb/src/types/study/index.ts +++ b/schoolNewsWeb/src/types/study/index.ts @@ -317,6 +317,9 @@ export interface TaskItemVO extends LearningTask { userID?: string; /** 用户名 */ username?: string; + deptID?: string; + deptName?: string; + parentDeptID?: string; /** 是否必修 */ required?: boolean; /** 排序号 */ diff --git a/schoolNewsWeb/src/types/user/index.ts b/schoolNewsWeb/src/types/user/index.ts index f406c0c..a2bd9ae 100644 --- a/schoolNewsWeb/src/types/user/index.ts +++ b/schoolNewsWeb/src/types/user/index.ts @@ -61,16 +61,19 @@ export interface UserVO extends BaseDTO { wechatID?: string; /** 用户状态 0-正常 1-禁用 */ status?: number; - /** 真实姓名 */ - realName?: string; + familyName?: string; + /** 名 */ + givenName?: string; /** 昵称 */ - nickname?: string; + fullName?: string; /** 头像URL */ avatar?: string; /** 性别 0-未知 1-男 2-女 */ gender?: number; /** 学习等级 */ level?: number; + deptID?: string; + parentDeptID?: string; /** 部门名称 */ deptName?: string; /** 角色名称 */ diff --git a/schoolNewsWeb/src/views/admin/manage/logs/LoginLogsView.vue b/schoolNewsWeb/src/views/admin/manage/logs/LoginLogsView.vue index 129205b..fbeb6e0 100644 --- a/schoolNewsWeb/src/views/admin/manage/logs/LoginLogsView.vue +++ b/schoolNewsWeb/src/views/admin/manage/logs/LoginLogsView.vue @@ -1,110 +1,312 @@ @@ -114,15 +316,310 @@ function handleCurrentChange(val: number) { padding: 20px; } -.filter-bar { +.search-bar { + background: #fff; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); display: flex; gap: 16px; + flex-wrap: wrap; + align-items: flex-end; +} + +.search-group { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 180px; +} + +.search-label { + font-size: 14px; + font-weight: 500; + color: #606266; +} + +.search-input, +.search-select { + padding: 8px 12px; + border: 1px solid #dcdfe6; + border-radius: 4px; + font-size: 14px; + color: #606266; + transition: border-color 0.3s; + + &:focus { + outline: none; + border-color: #409eff; + } +} + +.search-select { + cursor: pointer; +} + +.search-actions { + display: flex; + gap: 12px; + margin-left: auto; +} + +.btn-search, +.btn-reset { + padding: 8px 20px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + border: none; +} + +.btn-search { + background: #409eff; + color: #fff; + + &:hover { + background: #66b1ff; + } +} + +.btn-reset { + background: #fff; + color: #606266; + border: 1px solid #dcdfe6; + + &:hover { + color: #409eff; + border-color: #409eff; + } +} + +.toolbar { margin-bottom: 20px; +} + +.btn-danger { + padding: 8px 20px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + border: none; + background: #f56c6c; + color: #fff; + + &:hover { + background: #f78989; + } +} + +.btn-primary { + padding: 8px 20px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + border: none; + background: #409eff; + color: #fff; + + &:hover { + background: #66b1ff; + } +} + +.log-table-wrapper { + background: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.log-table { + width: 100%; + border-collapse: collapse; + + th { + background: #f5f7fa; + color: #606266; + font-weight: 600; + font-size: 14px; + padding: 12px 16px; + text-align: left; + border-bottom: 2px solid #e0e0e0; + } + + td { + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; + color: #606266; + } + + .table-row { + transition: background-color 0.3s; + + &:hover { + background: #f5f7fa; + } + + &:last-child td { + border-bottom: none; + } + } +} + +.log-message { + max-width: 300px; +} + +.message-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-tag { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + + &.status-success { + background: #e1f3d8; + color: #67c23a; + } + + &.status-failed { + background: #fde2e2; + color: #f56c6c; + } +} + +.loading-cell, +.empty-cell { + text-align: center; + padding: 60px 20px !important; + color: #909399; +} + +.loading-spinner { + display: inline-block; + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #409eff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 12px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.empty-icon { + font-size: 48px; + margin-bottom: 12px; +} + +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding: 20px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + flex-wrap: wrap; + gap: 16px; +} + +.pagination-info { + font-size: 14px; + color: #606266; +} + +.pagination-controls { + display: flex; + gap: 8px; align-items: center; } -.el-table { - margin-bottom: 20px; +.page-btn, +.page-number { + padding: 6px 12px; + border: 1px solid #dcdfe6; + background: #fff; + color: #606266; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + + &:hover:not(:disabled) { + color: #409eff; + border-color: #409eff; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &.active { + background: #409eff; + color: #fff; + border-color: #409eff; + } +} + +.page-numbers { + display: flex; + gap: 4px; +} + +.pagination-jump { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #606266; + + .jump-input { + width: 60px; + padding: 6px 8px; + border: 1px solid #dcdfe6; + border-radius: 4px; + font-size: 14px; + text-align: center; + + &:focus { + outline: none; + border-color: #409eff; + } + } + + .jump-btn { + padding: 6px 12px; + border: 1px solid #dcdfe6; + background: #fff; + color: #606266; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: #409eff; + border-color: #409eff; + } + } } diff --git a/schoolNewsWeb/src/views/admin/manage/logs/OperationLogsView.vue b/schoolNewsWeb/src/views/admin/manage/logs/OperationLogsView.vue index 6341e57..bf87e94 100644 --- a/schoolNewsWeb/src/views/admin/manage/logs/OperationLogsView.vue +++ b/schoolNewsWeb/src/views/admin/manage/logs/OperationLogsView.vue @@ -1,135 +1,481 @@ @@ -139,15 +485,498 @@ function handleCurrentChange(val: number) { padding: 20px; } -.filter-bar { +.search-bar { + background: #fff; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); display: flex; gap: 16px; + flex-wrap: wrap; + align-items: flex-end; +} + +.search-group { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 150px; +} + +.search-label { + font-size: 14px; + font-weight: 500; + color: #606266; +} + +.search-input, +.search-select { + padding: 8px 12px; + border: 1px solid #dcdfe6; + border-radius: 4px; + font-size: 14px; + color: #606266; + transition: border-color 0.3s; + + &:focus { + outline: none; + border-color: #409eff; + } +} + +.search-select { + cursor: pointer; +} + +.search-actions { + display: flex; + gap: 12px; + margin-left: auto; +} + +.btn-search, +.btn-reset { + padding: 8px 20px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + border: none; +} + +.btn-search { + background: #409eff; + color: #fff; + + &:hover { + background: #66b1ff; + } +} + +.btn-reset { + background: #fff; + color: #606266; + border: 1px solid #dcdfe6; + + &:hover { + color: #409eff; + border-color: #409eff; + } +} + +.toolbar { margin-bottom: 20px; +} + +.btn-danger { + padding: 8px 20px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + border: none; + background: #f56c6c; + color: #fff; + + &:hover { + background: #f78989; + } +} + +.btn-primary { + padding: 8px 20px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + border: none; + background: #409eff; + color: #fff; + + &:hover { + background: #66b1ff; + } +} + +.log-table-wrapper { + background: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.log-table { + width: 100%; + border-collapse: collapse; + + th { + background: #f5f7fa; + color: #606266; + font-weight: 600; + font-size: 14px; + padding: 12px 16px; + text-align: left; + border-bottom: 2px solid #e0e0e0; + } + + td { + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; + color: #606266; + } + + .table-row { + transition: background-color 0.3s; + + &:hover { + background: #f5f7fa; + } + + &:last-child td { + border-bottom: none; + } + } +} + +.log-desc { + max-width: 300px; +} + +.desc-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.align-right { + text-align: right; +} + +.status-tag { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + + &.status-success { + background: #e1f3d8; + color: #67c23a; + } + + &.status-failed { + background: #fde2e2; + color: #f56c6c; + } + + &.operation-create { + background: #e1f3d8; + color: #67c23a; + } + + &.operation-update { + background: #fdf6ec; + color: #e6a23c; + } + + &.operation-delete { + background: #fde2e2; + color: #f56c6c; + } + + &.operation-read { + background: #ecf5ff; + color: #409eff; + } +} + +.action-cell { + display: flex; + justify-content: center; align-items: center; } -.el-table { - margin-bottom: 20px; +.btn-link { + border: none; + padding: 6px 12px; + font-size: 13px; + cursor: pointer; + transition: all 0.3s; + border-radius: 4px; + min-width: 64px; + text-align: center; + color: #fff; + + &:hover { + opacity: 0.8; + } + + &.btn-primary { + background: #409eff; + } +} + +.loading-cell, +.empty-cell { + text-align: center; + padding: 60px 20px !important; + color: #909399; +} + +.loading-spinner { + display: inline-block; + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #409eff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 12px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.empty-icon { + font-size: 48px; + margin-bottom: 12px; +} + +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding: 20px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + flex-wrap: wrap; + gap: 16px; +} + +.pagination-info { + font-size: 14px; + color: #606266; +} + +.pagination-controls { + display: flex; + gap: 8px; + align-items: center; +} + +.page-btn, +.page-number { + padding: 6px 12px; + border: 1px solid #dcdfe6; + background: #fff; + color: #606266; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + + &:hover:not(:disabled) { + color: #409eff; + border-color: #409eff; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &.active { + background: #409eff; + color: #fff; + border-color: #409eff; + } +} + +.page-numbers { + display: flex; + gap: 4px; +} + +.pagination-jump { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #606266; + + .jump-input { + width: 60px; + padding: 6px 8px; + border: 1px solid #dcdfe6; + border-radius: 4px; + font-size: 14px; + text-align: center; + + &:focus { + outline: none; + border-color: #409eff; + } + } + + .jump-btn { + padding: 6px 12px; + border: 1px solid #dcdfe6; + background: #fff; + color: #606266; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: #409eff; + border-color: #409eff; + } + } +} + +// 模态框样式 +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; + padding: 20px; +} + +.modal-content { + background: #fff; + border-radius: 8px; + width: 100%; + max-width: 800px; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #e0e0e0; +} + +.modal-title { + font-size: 18px; + font-weight: 600; + color: #303133; + margin: 0; +} + +.modal-close { + width: 32px; + height: 32px; + border: none; + background: transparent; + font-size: 24px; + color: #909399; + cursor: pointer; + transition: all 0.3s; + border-radius: 4px; + + &:hover { + background: #f5f7fa; + color: #303133; + } +} + +.modal-body { + padding: 20px; + overflow-y: auto; + flex: 1; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 20px; + border-top: 1px solid #e0e0e0; +} + +.log-detail { + display: flex; + flex-direction: column; + gap: 24px; +} + +.detail-section { + background: #fafafa; + border-radius: 6px; + padding: 16px; +} + +.detail-title { + font-size: 16px; + font-weight: 600; + color: #303133; + margin: 0 0 16px 0; + padding-bottom: 12px; + border-bottom: 2px solid #e0e0e0; +} + +.detail-content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.detail-row { + display: flex; + font-size: 14px; +} + +.detail-label { + color: #909399; + min-width: 100px; + font-weight: 500; +} + +.detail-value { + color: #606266; + flex: 1; + word-break: break-all; +} + +.code-block { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 12px; + font-size: 13px; + font-family: 'Courier New', monospace; + color: #303133; + overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; +} + +.error-message { + background: #fef0f0; + border: 1px solid #fde2e2; + border-radius: 4px; + padding: 12px; + font-size: 14px; + color: #f56c6c; } - diff --git a/schoolNewsWeb/src/views/admin/manage/logs/SystemLogsView.vue b/schoolNewsWeb/src/views/admin/manage/logs/SystemLogsView.vue index ffec0e1..03dad57 100644 --- a/schoolNewsWeb/src/views/admin/manage/logs/SystemLogsView.vue +++ b/schoolNewsWeb/src/views/admin/manage/logs/SystemLogsView.vue @@ -1,46 +1,968 @@ - diff --git a/schoolNewsWeb/src/views/admin/manage/system/DeptManageView.vue b/schoolNewsWeb/src/views/admin/manage/system/DeptManageView.vue index c2584cb..673ca2b 100644 --- a/schoolNewsWeb/src/views/admin/manage/system/DeptManageView.vue +++ b/schoolNewsWeb/src/views/admin/manage/system/DeptManageView.vue @@ -424,7 +424,19 @@ async function fetchDeptRoles() { auth: true }; } - return await deptApi.getDeptByRole(currentDept.value); + + const result = await deptApi.getDeptByRole(currentDept.value); + + // 转换数据格式,将 UserDeptRoleVO 转换为与 SysRole 一致的格式 + if (result.success && result.dataList) { + result.dataList = result.dataList.map(item => ({ + roleID: item.roleID, + name: item.roleName, // roleName -> name + description: item.roleDescription // roleDescription -> description + })); + } + + return result; } // 查看绑定角色 diff --git a/schoolNewsWeb/src/views/admin/manage/system/MenuManageView.vue b/schoolNewsWeb/src/views/admin/manage/system/MenuManageView.vue index 5277e8e..adbeff6 100644 --- a/schoolNewsWeb/src/views/admin/manage/system/MenuManageView.vue +++ b/schoolNewsWeb/src/views/admin/manage/system/MenuManageView.vue @@ -166,7 +166,12 @@ right-title="已选权限" :fetch-available-api="fetchAllPermissions" :fetch-selected-api="fetchMenuPermissions" + :filter-selected="filterPermissions" :item-config="{ id: 'permissionID', label: 'name', sublabel: 'code' }" + :use-tree="true" + :tree-transform="transformPermissionsToTree" + :tree-props="{ children: 'children', label: 'displayName', id: 'permissionID' }" + :only-leaf-selectable="true" unit-name="个" search-placeholder="搜索权限名称或编码..." @confirm="handlePermissionConfirm" @@ -505,11 +510,80 @@ async function fetchAllPermissions() { // 获取菜单已绑定权限的接口 async function fetchMenuPermissions() { if (!currentMenu.value?.menuID) { - return { success: true, dataList: [] }; + return { + success: true, + dataList: [], + code: 200, + message: '', + login: true, + auth: true + }; } return await menuApi.getMenuPermission(currentMenu.value.menuID); } +// 过滤已选项 +function filterPermissions(available: any[], selected: any[]) { + const selectedIds = new Set(selected.map(item => item.permissionID)); + return available.filter(item => !selectedIds.has(item.permissionID)); +} + +// 将权限按模块ID构建树形结构 +function transformPermissionsToTree(flatData: any[]) { + if (!flatData || flatData.length === 0) { + return []; + } + + // 按模块分组 + const moduleMap = new Map(); + const tree: any[] = []; + + flatData.forEach(item => { + const moduleID = item.moduleID; + + if (!moduleID) { + // 没有模块ID的权限作为根节点 + tree.push({ + ...item, + displayName: item.name, + isModule: false + }); + return; + } + + if (!moduleMap.has(moduleID)) { + // 创建模块节点 + moduleMap.set(moduleID, { + permissionID: moduleID, // 使用moduleID作为ID,确保唯一性 + displayName: item.moduleName || moduleID, + moduleID: moduleID, + moduleName: item.moduleName, + moduleCode: item.moduleCode, + moduleDescription: item.moduleDescription, + children: [], + isModule: true // 标记这是模块节点 + }); + } + + // 添加权限到模块的children中 + const moduleNode = moduleMap.get(moduleID); + if (moduleNode) { + moduleNode.children.push({ + ...item, + displayName: item.name, + isModule: false // 标记这是权限节点 + }); + } + }); + + // 将所有模块添加到树中 + moduleMap.forEach(moduleNode => { + tree.push(moduleNode); + }); + + return tree; +} + // 查看绑定权限 async function handleBindPermission(row: SysMenu) { currentMenu.value = row; @@ -535,8 +609,11 @@ async function handlePermissionConfirm(items: SysPermission[]) { const currentBoundResult = await menuApi.getMenuPermission(currentMenu.value.menuID); const currentBoundIds = (currentBoundResult.dataList || []).map(p => p.permissionID).filter((id): id is string => !!id); - // 新选择的权限ID - const newSelectedIds = items.map(p => p.permissionID).filter((id): id is string => !!id); + // 新选择的权限ID(过滤掉模块节点,只保留权限节点) + const newSelectedIds = items + .filter((item: any) => !item.isModule) // 只保留权限节点,过滤模块节点 + .map(p => p.permissionID) + .filter((id): id is string => !!id); // 找出需要绑定的权限(新增的) const permissionsToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id)); @@ -546,7 +623,8 @@ async function handlePermissionConfirm(items: SysPermission[]) { // 构建需要绑定的权限对象数组 if (permissionsToBind.length > 0) { - const permissionsToBindObjects = items.filter(p => p.permissionID && permissionsToBind.includes(p.permissionID)); + const permissionsToBindObjects = items + .filter((item: any) => !item.isModule && item.permissionID && permissionsToBind.includes(item.permissionID)); const bindMenu = { ...currentMenu.value, @@ -569,6 +647,8 @@ async function handlePermissionConfirm(items: SysPermission[]) { } ElMessage.success('权限绑定保存成功'); + bindPermissionDialogVisible.value = false; + resetBindList(); // 刷新菜单列表 await loadMenuList(); diff --git a/schoolNewsWeb/src/views/admin/manage/system/ModulePermissionManageView.vue b/schoolNewsWeb/src/views/admin/manage/system/ModulePermissionManageView.vue index 46bb1e3..2e7701c 100644 --- a/schoolNewsWeb/src/views/admin/manage/system/ModulePermissionManageView.vue +++ b/schoolNewsWeb/src/views/admin/manage/system/ModulePermissionManageView.vue @@ -286,7 +286,12 @@ right-title="已选菜单" :fetch-available-api="fetchAllMenus" :fetch-selected-api="fetchPermissionMenus" + :filter-selected="filterMenus" :item-config="{ id: 'menuID', label: 'name', sublabel: 'url' }" + :use-tree="true" + :tree-transform="transformMenusToTree" + :tree-props="{ children: 'children', label: 'name', id: 'menuID' }" + :only-leaf-selectable="true" unit-name="个" search-placeholder="搜索菜单名称..." @confirm="handleMenuConfirm" @@ -301,6 +306,7 @@ right-title="已选角色" :fetch-available-api="fetchAllRoles" :fetch-selected-api="fetchPermissionRoles" + :filter-selected="filterRoles" :item-config="{ id: 'roleID', label: 'name', sublabel: 'description' }" unit-name="个" search-placeholder="搜索角色名称..." @@ -622,7 +628,11 @@ async function fetchPermissionMenus() { success: result.success, login: result.login ?? true, auth: result.auth ?? true, - dataList: result.data?.menus || [] + dataList: result.dataList?.map(item => ({ + ...item, + name: item.menuName, + url: item.menuUrl + })) || [] }; } @@ -651,10 +661,115 @@ async function fetchPermissionRoles() { success: result.success, login: result.login ?? true, auth: result.auth ?? true, - dataList: result.data?.roles || [] + dataList: result.dataList?.map(item => ({ + ...item, + name: item.roleName, + description: item.roleDescription + })) || [] }; } +// 过滤已选菜单项 +function filterMenus(available: any[], selected: any[]) { + const selectedIds = new Set(selected.map(item => item.menuID)); + return available.filter(item => !selectedIds.has(item.menuID)); +} + +// 过滤已选角色项 +function filterRoles(available: any[], selected: any[]) { + const selectedIds = new Set(selected.map(item => item.roleID)); + return available.filter(item => !selectedIds.has(item.roleID)); +} + +// 将菜单扁平数据转换为树形结构(根据parentID构建) +function transformMenusToTree(flatData: any[]): any[] { + if (!flatData || flatData.length === 0) { + return []; + } + + const tree: any[] = []; + const map = new Map(); + const maxDepth = flatData.length; // 最多遍历len层 + + // 初始化所有节点 + flatData.forEach(item => { + if (item.menuID) { + map.set(item.menuID, { ...item, children: [] }); + } + }); + + // 循环构建树结构,最多遍历maxDepth次 + for (let depth = 0; depth < maxDepth; depth++) { + let hasChanges = false; + + flatData.forEach(item => { + if (!item.menuID) return; + + const node = map.get(item.menuID); + if (!node) return; + + // 如果节点已经在树中,跳过 + if (isMenuNodeInTree(node, tree)) { + return; + } + + if (!item.parentID || item.parentID === '0' || item.parentID === '') { + // 根节点 + if (!isMenuNodeInTree(node, tree)) { + tree.push(node); + hasChanges = true; + } + } else { + // 查找父节点 + const parent = map.get(item.parentID); + if (parent && isMenuNodeInTree(parent, tree)) { + if (!parent.children) { + parent.children = []; + } + if (!parent.children.includes(node)) { + parent.children.push(node); + hasChanges = true; + } + } + } + }); + + // 如果没有变化,说明树构建完成 + if (!hasChanges) { + break; + } + } + + // 清理空的children数组 + function cleanEmptyChildren(nodes: any[]) { + nodes.forEach(node => { + if (node.children && node.children.length === 0) { + delete node.children; + } else if (node.children && node.children.length > 0) { + cleanEmptyChildren(node.children); + } + }); + } + + cleanEmptyChildren(tree); + return tree; +} + +// 检查菜单节点是否已经在树中 +function isMenuNodeInTree(node: any, tree: any[]): boolean { + for (const treeNode of tree) { + if (treeNode.menuID === node.menuID) { + return true; + } + if (treeNode.children && treeNode.children.length > 0) { + if (isMenuNodeInTree(node, treeNode.children)) { + return true; + } + } + } + return false; +} + // 绑定菜单 async function handleBindMenu(permission: SysPermission) { currentPermission.value = permission; @@ -708,6 +823,8 @@ async function handleMenuConfirm(items: SysMenu[]) { } ElMessage.success('菜单绑定保存成功'); + bindMenuDialogVisible.value = false; + resetBindList(); } catch (error) { console.error('保存菜单绑定失败:', error); ElMessage.error('保存菜单绑定失败'); @@ -757,6 +874,8 @@ async function handleRoleConfirm(items: SysRole[]) { } ElMessage.success('角色绑定保存成功'); + bindRoleDialogVisible.value = false; + resetBindList(); } catch (error) { console.error('保存角色绑定失败:', error); ElMessage.error('保存角色绑定失败'); diff --git a/schoolNewsWeb/src/views/admin/manage/system/RoleManageView.vue b/schoolNewsWeb/src/views/admin/manage/system/RoleManageView.vue index fb5862c..86248b6 100644 --- a/schoolNewsWeb/src/views/admin/manage/system/RoleManageView.vue +++ b/schoolNewsWeb/src/views/admin/manage/system/RoleManageView.vue @@ -102,9 +102,14 @@ :title="`绑定权限 - ${currentRole?.name || ''}`" left-title="可选权限" right-title="已选权限" - :available-items="availablePermissions" - :initial-target-items="initialBoundPermissions" + :fetch-available-api="fetchAllPermissions" + :fetch-selected-api="fetchRolePermissions" + :filter-selected="filterPermissions" :item-config="{ id: 'permissionID', label: 'name', sublabel: 'code' }" + :use-tree="true" + :tree-transform="transformPermissionsToTree" + :tree-props="{ children: 'children', label: 'displayName', id: 'permissionID' }" + :only-leaf-selectable="true" unit-name="个" search-placeholder="搜索权限名称或编码..." @confirm="handlePermissionConfirm" @@ -124,7 +129,7 @@ import { GenericSelector } from '@/components/base'; defineOptions({ name: 'RoleManageView' }); -import { ref, onMounted, reactive, computed } from 'vue'; +import { ref, onMounted, reactive } from 'vue'; import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'; import { Plus } from '@element-plus/icons-vue'; @@ -134,9 +139,10 @@ const loading = ref(false); const submitting = ref(false); // 权限绑定相关数据 -const permissionList = ref([]); +// 移除:改为由 GenericSelector 通过 API 加载 +// const permissionList = ref([]); +// const initialBoundPermissions = ref([]); const currentRole = ref(null); -const initialBoundPermissions = ref([]); // 对话框状态 const dialogVisible = ref(false); @@ -251,43 +257,105 @@ function resetForm() { }); } -// 计算可选权限(过滤掉已绑定的) -const availablePermissions = computed(() => { - const boundIds = new Set(initialBoundPermissions.value.map(p => p.permissionID)); - return permissionList.value.filter(p => !boundIds.has(p.permissionID)); -}); +// 获取所有权限的API函数 +async function fetchAllPermissions() { + const permission: SysPermission = { + permissionID: undefined, + name: undefined, + code: undefined, + description: undefined, + }; + return await permissionApi.getPermissionList(permission); +} + +// 获取角色已绑定权限的API函数 +async function fetchRolePermissions() { + if (!currentRole.value || !currentRole.value.roleID) { + return { + success: true, + dataList: [], + code: 200, + message: '', + login: true, + auth: true + }; + } + return await roleApi.getRolePermission({ + roleID: currentRole.value.roleID + }); +} + +// 过滤已选项 +function filterPermissions(available: any[], selected: any[]) { + const selectedIds = new Set(selected.map(item => item.permissionID)); + return available.filter(item => !selectedIds.has(item.permissionID)); +} + +// 将权限按模块ID构建树形结构 +function transformPermissionsToTree(flatData: any[]) { + if (!flatData || flatData.length === 0) { + return []; + } + + // 按模块分组 + const moduleMap = new Map(); + const tree: any[] = []; + + flatData.forEach(item => { + const moduleID = item.moduleID; + + if (!moduleID) { + // 没有模块ID的权限作为根节点 + tree.push({ + ...item, + displayName: item.name, + isModule: false + }); + return; + } + + if (!moduleMap.has(moduleID)) { + // 创建模块节点 + moduleMap.set(moduleID, { + permissionID: moduleID, // 使用moduleID作为ID,确保唯一性 + displayName: item.moduleName || moduleID, + moduleID: moduleID, + moduleName: item.moduleName, + moduleCode: item.moduleCode, + moduleDescription: item.moduleDescription, + children: [], + isModule: true // 标记这是模块节点 + }); + } + + // 添加权限到模块的children中 + const moduleNode = moduleMap.get(moduleID); + if (moduleNode) { + moduleNode.children.push({ + ...item, + displayName: item.name, + isModule: false // 标记这是权限节点 + }); + } + }); + + // 将所有模块添加到树中 + moduleMap.forEach(moduleNode => { + tree.push(moduleNode); + }); + + return tree; +} // 查看绑定权限 async function handleBindPermission(row: SysRole) { currentRole.value = row; - - try { - // 获取所有权限 - let permission: SysPermission = { - permissionID: undefined, - name: undefined, - code: undefined, - description: undefined, - }; - const permissionResult = await permissionApi.getPermissionList(permission); - permissionList.value = permissionResult.dataList || []; - - // 获取已绑定的权限 - const bindingResult = await roleApi.getRolePermission({ - roleID: row.roleID - }); - initialBoundPermissions.value = bindingResult.dataList || []; - - bindPermissionDialogVisible.value = true; - } catch (error) { - console.error('获取权限绑定信息失败:', error); - ElMessage.error('获取权限绑定信息失败'); - } + bindPermissionDialogVisible.value = true; + // 不再需要预加载数据,由 GenericSelector 在打开时自动调用 API 加载 } // 重置绑定列表 function resetBindList() { - initialBoundPermissions.value = []; currentRole.value = null; } @@ -301,11 +369,20 @@ async function handlePermissionConfirm(items: SysPermission[]) { try { submitting.value = true; - // 获取当前已绑定的权限ID - const currentBoundIds = initialBoundPermissions.value.map(p => p.permissionID).filter((id): id is string => !!id); + // 重新获取当前已绑定的权限 + const bindingResult = await roleApi.getRolePermission({ + roleID: currentRole.value.roleID + }); + const currentBoundPermissions = bindingResult.dataList || []; - // 新选择的权限ID - const newSelectedIds = items.map(p => p.permissionID).filter((id): id is string => !!id); + // 获取当前已绑定的权限ID + const currentBoundIds = currentBoundPermissions.map(p => p.permissionID).filter((id): id is string => !!id); + + // 新选择的权限ID(过滤掉模块节点,只保留权限节点) + const newSelectedIds = items + .filter((item: any) => !item.isModule) // 只保留权限节点,过滤模块节点 + .map(p => p.permissionID) + .filter((id): id is string => !!id); // 找出需要绑定的权限(新增的) const permissionsToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id)); @@ -316,7 +393,7 @@ async function handlePermissionConfirm(items: SysPermission[]) { // 构建需要绑定的权限对象数组 if (permissionsToBind.length > 0) { const permissionsToBindObjects = permissionsToBind.map(permissionID => { - const permission = permissionList.value.find(p => p.permissionID === permissionID); + const permission = items.find(p => p.permissionID === permissionID); return permission || { permissionID: permissionID }; }); @@ -331,7 +408,7 @@ async function handlePermissionConfirm(items: SysPermission[]) { // 构建需要解绑的权限对象数组 if (permissionsToUnbind.length > 0) { const permissionsToUnbindObjects = permissionsToUnbind.map(permissionID => { - const permission = permissionList.value.find(p => p.permissionID === permissionID); + const permission = currentBoundPermissions.find(p => p.permissionID === permissionID); return permission || { permissionID: permissionID }; }); @@ -344,6 +421,8 @@ async function handlePermissionConfirm(items: SysPermission[]) { } ElMessage.success('权限绑定保存成功'); + bindPermissionDialogVisible.value = false; + resetBindList(); // 刷新角色列表 await loadRoleList(); diff --git a/schoolNewsWeb/src/views/public/task/LearningTaskList.vue b/schoolNewsWeb/src/views/public/task/LearningTaskList.vue index 919982c..987bd0b 100644 --- a/schoolNewsWeb/src/views/public/task/LearningTaskList.vue +++ b/schoolNewsWeb/src/views/public/task/LearningTaskList.vue @@ -319,16 +319,17 @@ :title="selectorMode === 'add' ? '添加人员' : '删除人员'" :left-title="selectorMode === 'add' ? '可添加人员' : '当前人员'" :right-title="selectorMode === 'add' ? '待添加人员' : '待删除人员'" - :available-items="selectorMode === 'remove' ? availableUsers : []" - :initial-target-items="[]" + :fetch-available-api="selectorMode === 'add' ? fetchAllUsers : fetchCurrentUsers" + :fetch-selected-api="selectorMode === 'add' ? fetchTaskUsers : undefined" + :filter-selected="filterUsers" :loading="saving" :item-config="{ id: 'id', label: 'username', sublabel: 'deptName' }" + :use-tree="true" + :tree-transform="transformUsersToTree" + :tree-props="{ children: 'children', label: 'displayName', id: 'id' }" + :only-leaf-selectable="true" unit-name="人" search-placeholder="搜索人员..." - :use-pagination="selectorMode === 'add'" - :fetch-api="selectorMode === 'add' ? userApi.getUserPage : undefined" - :filter-params="userFilterParams" - :page-size="20" @confirm="handleUserSelectConfirm" @cancel="closeSelectorModal" /> @@ -388,9 +389,7 @@ const deleting = ref(false); const managingTask = ref(null); const selectorMode = ref<'add' | 'remove'>('add'); const currentUsers = ref([]); -const availableUsers = ref([]); const saving = ref(false); -const userFilterParams = ref({}); // 计算显示的页码 const displayPages = computed(() => { @@ -523,7 +522,9 @@ function handleEdit(task: LearningTask) { emit('edit', task); } -function handleStatistics(task: LearningTask) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function handleStatistics(_task: LearningTask) { + // TODO: 实现统计功能 return; } @@ -565,30 +566,171 @@ function closeUserList() { currentUsers.value = []; } +// 获取所有用户的API函数 +async function fetchAllUsers() { + const filter: any = { username: undefined }; + return await userApi.getUserList(filter); +} + +// 获取当前任务用户的API函数(用于删除模式) +async function fetchCurrentUsers() { + // 删除模式时,左侧显示当前任务的用户列表 + return { + success: true, + dataList: currentUsers.value, + code: 200, + message: '', + login: true, + auth: true + }; +} + +// 获取任务已分配用户的API函数 +async function fetchTaskUsers() { + if (!managingTask.value || !managingTask.value.taskID) { + return { + success: true, + dataList: [], + code: 200, + message: '', + login: true, + auth: true + }; + } + + // 从任务详情中获取用户列表 + const result = await learningTaskApi.getTaskUsers(managingTask.value.taskID); + if (result.success && result.dataList) { + // 将返回的用户列表转换为UserVO格式 + const users = (result.dataList || []).map((item: any) => ({ + id: item.userID, + username: item.username, + deptID: item.deptID, + deptName: item.deptName, + parentDeptID: item.parentDeptID + })); + return { + success: true, + dataList: users, + code: 200, + message: '', + login: true, + auth: true + }; + } + + return { + success: false, + dataList: [], + code: result.code || 500, + message: result.message || '获取任务用户失败', + login: true, + auth: true + }; +} + +// 过滤已选用户 +function filterUsers(available: any[], selected: any[]) { + const selectedIds = new Set(selected.map(item => item.id)); + return available.filter(item => !selectedIds.has(item.id)); +} + +// 将用户扁平数据转换为树形结构(按部门分组) +function transformUsersToTree(flatData: any[]): any[] { + if (!flatData || flatData.length === 0) { + return []; + } + + // 按部门分组 + const deptMap = new Map(); + const tree: any[] = []; + + flatData.forEach(item => { + const deptID = item.deptID || 'unknown'; + const deptName = item.deptName || '未分配部门'; + + if (!deptMap.has(deptID)) { + // 创建部门节点 + deptMap.set(deptID, { + id: `dept_${deptID}`, // 使用特殊前缀避免与用户ID冲突 + displayName: deptName, + deptID: deptID, + deptName: deptName, + parentDeptID: item.parentDeptID, + children: [], + isDept: true // 标记这是部门节点 + }); + } + + // 添加用户到部门的children中 + const deptNode = deptMap.get(deptID); + if (deptNode) { + deptNode.children.push({ + ...item, + displayName: item.username || item.id, + isDept: false // 标记这是用户节点 + }); + } + }); + + // 构建部门层级关系 + const deptNodes = Array.from(deptMap.values()); + const deptTreeMap = new Map(); + + // 初始化所有部门节点 + deptNodes.forEach(dept => { + deptTreeMap.set(dept.deptID, { ...dept }); + }); + + // 构建部门树 + deptNodes.forEach(dept => { + const node = deptTreeMap.get(dept.deptID); + if (!node) return; + + if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') { + // 根部门 + tree.push(node); + } else { + // 子部门 + const parent = deptTreeMap.get(dept.parentDeptID); + if (parent) { + // 将用户节点暂存 + const users = node.children || []; + node.children = []; + + // 添加部门到父部门 + if (!parent.children) { + parent.children = []; + } + parent.children.push(node); + + // 将用户添加回来(放在部门子节点之后) + node.children = users; + } else { + // 找不到父节点,作为根节点 + tree.push(node); + } + } + }); + + return tree; +} + // 显示添加人员选择器 function showAddUserSelector() { selectorMode.value = 'add'; - - // 设置过滤参数(可以过滤掉已分配的用户,但在组件内部会自动过滤) - userFilterParams.value = {}; - showUserSelector.value = true; } // 显示删除人员选择器 function showRemoveUserSelector() { selectorMode.value = 'remove'; - - // 左侧显示当前用户 - availableUsers.value = [...currentUsers.value]; - showUserSelector.value = true; } // 关闭选择器弹窗 function closeSelectorModal() { showUserSelector.value = false; - availableUsers.value = []; } // 处理用户选择器确认事件