Explorar o código

“出国申请工作流修改10.29版”

weizhong hai 1 ano
pai
achega
32b9822146

+ 43 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/mapper/RsOverseasApplicationMapper.java

@@ -101,4 +101,47 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus<RsOverseasAp
      * @return 用户姓名
      */
     String selectUserNameById(Long userId);
+
+    /**
+     * 根据权限标识查询拥有该权限的用户ID列表
+     * 
+     * 查询逻辑:
+     * 1. 通过 sys_menu 查找权限对应的菜单ID
+     * 2. 通过 sys_role_menu 查找拥有该菜单的角色
+     * 3. 通过 sys_user_role 查找拥有这些角色的用户
+     * 4. 过滤掉已停用的用户
+     *
+     * @param permissionCode 权限标识(如:rs:overseas:approve2)
+     * @return 用户ID列表
+     */
+    List<Long> selectUserIdsByPermission(String permissionCode);
+
+    /**
+     * 根据用户ID查询其拥有的出国审批权限列表
+     * 
+     * 查询逻辑:
+     * 1. 通过 sys_user_role 查找用户的角色
+     * 2. 通过 sys_role_menu 查找角色的菜单
+     * 3. 通过 sys_menu 查找菜单的权限标识
+     * 4. 过滤出出国审批相关的权限(rs:overseas:approve%)
+     *
+     * @param userId 用户ID
+     * @return 审批权限标识列表(如:["rs:overseas:approve2", "rs:overseas:approve3"])
+     */
+    List<String> selectApprovalPermissionsByUserId(Long userId);
+
+    /**
+     * 🔴 新增:查询待审批申请列表(支持部门过滤)
+     * 
+     * 查询逻辑:
+     * 1. 关联 sys_user 表获取申请人的部门信息
+     * 2. 按审批级别过滤(approvalLevel IN (...) )
+     * 3. 按状态过滤(status = '0' 待审核)
+     * 4. 按部门过滤(deptId != null 时,u.dept_id = #{deptId})
+     * 
+     * @param approvalLevels 审批级别列表,例如:["1", "2"]
+     * @param deptId         部门ID(部门负责人需要),为null则不过滤部门
+     * @return 待审批申请列表
+     */
+    List<RsOverseasApplication> selectPendingApplications(List<String> approvalLevels, Long deptId);
 }

+ 188 - 108
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationServiceImpl.java

@@ -87,29 +87,40 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
      */
     @Override
     public TableDataInfo<RsOverseasApplicationVo> queryPageList(RsOverseasApplicationQuery query) {
-        // 🔴 新增:部门负责人权限过滤逻辑
+        // 🔴 新增:多级审批权限过滤逻辑
         Long currentUserId = LoginHelper.getUserId();
 
         // 1. 判断是否是rs-admin管理员(管理员可以看所有数据)
         boolean isAdmin = cn.dev33.satoken.stp.StpUtil.hasRole("rs-admin");
 
         if (!isAdmin) {
-            // 2. 判断是否是部门负责人(拥有rs:overseas:approve1权限)
-            boolean isDeptHead = cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve1");
-
-            if (isDeptHead) {
-                // 🔴 部门负责人:只能看到本部门员工提交的申请
-                SysUserVo currentUser = userService.selectUserById(currentUserId);
-                if (currentUser != null && currentUser.getDeptId() != null) {
-                    query.setApplicantDeptId(currentUser.getDeptId());
-                    log.info("🔒 [部门权限过滤] 用户: {}, 部门ID: {}, 只能查看本部门申请",
-                            currentUser.getUserName(), currentUser.getDeptId());
-                }
+            // 2. 判断是否有高级别审批权限(人事处及以上:approve2/3/4/5)
+            boolean hasHigherApprovalPermission = cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve2") || // 人事处
+                    cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve3") || // 纪委
+                    cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve4") || // 学院分管领导
+                    cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve5"); // 学院党委书记
+
+            if (hasHigherApprovalPermission) {
+                // 🔴 人事处及以上:可以看到所有申请(不按部门过滤)
+                log.info("✅ [高级审批权限] 用户ID: {}, 可以查看所有申请", currentUserId);
             } else {
-                // 3. 普通用户:只能看到自己提交的申请
-                if (query.getApplicantId() == null) {
-                    query.setApplicantId(currentUserId);
-                    log.info("🔒 [个人权限过滤] 用户ID: {}, 只能查看自己的申请", currentUserId);
+                // 3. 判断是否是部门负责人(只有 rs:overseas:approve1 权限)
+                boolean isDeptHead = cn.dev33.satoken.stp.StpUtil.hasPermission("rs:overseas:approve1");
+
+                if (isDeptHead) {
+                    // 🔴 纯部门负责人:只能看到本部门员工提交的申请
+                    SysUserVo currentUser = userService.selectUserById(currentUserId);
+                    if (currentUser != null && currentUser.getDeptId() != null) {
+                        query.setApplicantDeptId(currentUser.getDeptId());
+                        log.info("🔒 [部门权限过滤] 用户: {}, 部门ID: {}, 只能查看本部门申请",
+                                currentUser.getUserName(), currentUser.getDeptId());
+                    }
+                } else {
+                    // 4. 普通用户:只能看到自己提交的申请
+                    if (query.getApplicantId() == null) {
+                        query.setApplicantId(currentUserId);
+                        log.info("🔒 [个人权限过滤] 用户ID: {}, 只能查看自己的申请", currentUserId);
+                    }
                 }
             }
         } else {
@@ -220,46 +231,92 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
     }
 
     /**
-     * 查询当前用户的待审批申请列表
+     * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤)
+     * 
+     * 🆕 核心改进:
+     * 1. 不再依赖 current_approver_id 字段(该字段仅作为冗余展示用)
+     * 2. 动态查询用户的审批权限,转换为审批级别
+     * 3. 根据审批级别和状态查询待审批申请
+     * 4. 🔴 部门负责人只能看到本部门的申请
+     * 5. 支持人员岗位变动:权限调整后立即生效
+     * 
+     * 查询逻辑:
+     * - 用户有 rs:overseas:approve1 权限(部门负责人)→ 查询本部门员工的、approval_level='1' 且
+     * status='0' 的申请
+     * - 用户有 rs:overseas:approve2 权限(人事处)→ 查询全校的、approval_level='2' 且 status='0' 的申请
+     * - 用户有多个审批权限 → 返回所有对应级别的待审批申请(无部门限制)
      */
     @Override
     public List<RsOverseasApplicationVo> queryPendingByCurrentUser() {
         Long currentUserId = LoginHelper.getUserId();
-        log.info("查询当前用户待审批申请,用户ID: {}", currentUserId);
-
-        // 获取当前用户信息
         String currentUserName = LoginHelper.getUsername();
-        log.info("当前用户名: {}", currentUserName);
 
-        List<RsOverseasApplication> applications = baseMapper.selectList(
-                new LambdaQueryWrapper<RsOverseasApplication>()
-                        .eq(RsOverseasApplication::getStatus, "0") // 待审核状态
-                        .eq(RsOverseasApplication::getCurrentApprover, currentUserName) // 使用当前审批人姓名
-                        .orderByDesc(RsOverseasApplication::getApplyTime));
-
-        // 转换为VO对象
-        return applications.stream()
-                .map(this::convertToVo)
-                .collect(Collectors.toList());
+        log.info("🔍 查询用户待审批申请 - 用户ID: {}, 用户名: {}", currentUserId, currentUserName);
+
+        // 1. 获取当前用户的审批级别列表(基于权限动态查询)
+        List<String> approvalLevels = getApprovalLevelsByUser(currentUserId);
+
+        if (approvalLevels == null || approvalLevels.isEmpty()) {
+            log.warn("❌ 用户 {} 没有任何出国审批权限,返回空列表", currentUserName);
+            return new ArrayList<>();
+        }
+
+        log.info("✅ 用户 {} 拥有审批级别: {}", currentUserName, approvalLevels);
+
+        // 2. 🔴 新逻辑:对每个级别单独查询并合并(方案1)
+        List<RsOverseasApplicationVo> result = new ArrayList<>();
+
+        // 遍历每个审批级别
+        for (String level : approvalLevels) {
+            Long deptIdForFilter = null;
+
+            // 🔴 关键:只对 level=1 进行部门过滤
+            if ("1".equals(level)) {
+                // 获取当前用户的部门ID
+                SysUserVo currentUser = userService.selectUserById(currentUserId);
+                if (currentUser != null && currentUser.getDeptId() != null) {
+                    deptIdForFilter = currentUser.getDeptId();
+                    log.info("🔒 [部门权限过滤] 级别1,用户: {}, 部门ID: {}, 只查看本部门待审批",
+                            currentUserName, deptIdForFilter);
+                } else {
+                    log.warn("⚠️ 用户 {} 是部门负责人但没有部门信息,无法过滤", currentUserName);
+                }
+            } else {
+                // level=2, 3, 4, 5 不进行部门过滤
+                log.info("✅ [全校权限] 级别{}, 用户 {}, 查看全校待审批",
+                        level, currentUserName);
+            }
+
+            // 3. 查询该级别的待审批申请(单个级别)
+            List<RsOverseasApplication> levelApplications = baseMapper.selectPendingApplications(
+                    Arrays.asList(level), // 🔴 关键:只查询单个级别
+                    deptIdForFilter // level=1时有值,其他为null
+            );
+
+            log.info("📋 级别{} 查询到 {} 条待审批申请", level, levelApplications.size());
+
+            // 4. 转换为VO对象并添加到结果列表
+            result.addAll(levelApplications.stream()
+                    .map(this::convertToVo)
+                    .collect(Collectors.toList()));
+        }
+
+        log.info("📋 用户 {} 总共查询到 {} 条待审批申请", currentUserName, result.size());
+
+        // 5. 返回合并后的结果
+        return result;
     }
 
     /**
      * 转换实体为VO对象
+     * 🔴 修复:使用Mapper查询完整的VO对象,包含字典翻译等扩展信息
      */
     private RsOverseasApplicationVo convertToVo(RsOverseasApplication entity) {
         if (entity == null) {
             return null;
         }
-        RsOverseasApplicationVo vo = new RsOverseasApplicationVo();
-        // 使用BeanUtils复制属性(如果有的话)或者手动复制关键属性
-        vo.setApplicationId(entity.getApplicationId());
-        vo.setSerialNumber(entity.getSerialNumber());
-        vo.setApplicantId(entity.getApplicantId());
-        vo.setApplicantName(entity.getApplicantName());
-        vo.setStatus(entity.getStatus());
-        vo.setApplyTime(entity.getApplyTime());
-        // 复制其他必要的字段...
-        return vo;
+        // 🔴 使用Mapper的详细查询方法,获取完整的VO对象(包含字典翻译)
+        return baseMapper.selectOverseasApplicationById(entity.getApplicationId());
     }
 
     /**
@@ -437,7 +494,13 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
     }
 
     /**
-     * 提交出国申请(传统审批模式)
+     * 提交出国申请(动态审批人方案)
+     * 
+     * 🆕 核心改进:
+     * 1. 不再设置固定的 current_approver_id(该字段改为冗余展示字段)
+     * 2. 仅设置 approval_level='1',审批人根据权限动态查询
+     * 3. current_approver 设置为提示文本(如"待一级审批")
+     * 4. 支持人员岗位变动:权限调整后立即生效
      */
     @Override
     @Transactional
@@ -452,15 +515,18 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
             throw new RuntimeException("已通过或已完成的申请不能重新提交");
         }
 
-        // 设置状态为待审核
+        // 🆕 设置状态为待审核
         application.setStatus("0");
         application.setApplyTime(new Date());
+
+        // 🆕 设置初始审批级别为1(不再设置固定审批人ID)
         application.setApprovalLevel("1");
-        application.setCurrentApprover("待审批");
+        application.setCurrentApproverId(null); // 清空固定审批人ID
+        application.setCurrentApprover("待一级审批"); // 提示文本
 
         boolean updated = baseMapper.updateById(application) > 0;
 
-        log.info("✅ [出国申请提交] 提交成功, applicationId: {}, 申请人: {}",
+        log.info("✅ [出国申请提交] 提交成功 - applicationId: {}, 申请人: {}, 审批级别: 1",
                 applicationId, application.getApplicantName());
 
         return updated;
@@ -530,19 +596,18 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
 
         approvalMapper.insert(approval);
 
-        // 更新申请状态
+        // 🆕 更新申请状态(动态审批人方案)
         if ("1".equals(approvalStatus)) {
             // 审批通过
             String nextLevel = getNextApprovalLevel(application.getApprovalLevel());
             if (nextLevel != null) {
-                // 进入下一级审批
-                Long nextApproverId = getNextApprover(nextLevel);
+                // 🆕 进入下一级审批(不再设置固定审批人ID)
                 application.setApprovalLevel(nextLevel);
-                application.setCurrentApproverId(nextApproverId);
-                application.setCurrentApprover(getApprovalLevelDescription(nextLevel));
+                application.setCurrentApproverId(null); // 不再设置固定审批人ID
+                application.setCurrentApprover(getApprovalLevelDescription(nextLevel)); // 如"待二级审批"
 
-                log.info("审批流转到第{}级,申请ID: {}, 下一级审批人ID: {}",
-                        nextLevel, applicationId, nextApproverId);
+                log.info("✅ 审批流转到第{}级,申请ID: {}, 审批人根据权限动态查询",
+                        nextLevel, applicationId);
             } else {
                 // 所有级别审批完成 - 先尝试创建出国记录,再更新申请状态
                 log.info("开始最终审批处理,申请ID: {}, 当前状态: {}, 申请人: {}",
@@ -731,15 +796,16 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
             existingApp.setDuration(days);
         }
 
-        // 重置为待审核状态
+        // 🆕 重置为待审核状态(动态审批人方案)
         existingApp.setStatus("0");
         existingApp.setApplyTime(new Date()); // 更新申请时间
 
-        // 设置初始审批级别为1(单级审批制度)
+        // 🆕 设置初始审批级别为1(不再设置固定审批人ID)
         existingApp.setApprovalLevel("1");
-        existingApp.setCurrentApprover("待审批");
+        existingApp.setCurrentApproverId(null); // 清空固定审批人ID
+        existingApp.setCurrentApprover("待一级审批"); // 提示文本
 
-        log.info("【重新提交申请】更新为待审核状态,申请ID: {}", applicationId);
+        log.info("【重新提交申请】更新为待审核状态,申请ID: {}, 审批级别: 1", applicationId);
 
         return baseMapper.updateById(existingApp) > 0;
     }
@@ -1181,58 +1247,9 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
      * @param approvalLevel 审批级别
      * @return 下一级审批人ID
      */
-    private Long getNextApprover(String approvalLevel) {
-        try {
-            String roleKey = null;
-            switch (approvalLevel) {
-                case "2": // 人事处审批
-                    roleKey = "hr_dept_overseas_approver";
-                    break;
-                case "3": // 纪委审批
-                    roleKey = "discipline_overseas_approver";
-                    break;
-                case "4": // 学院分管领导审批
-                    roleKey = "college_leader_overseas_approver";
-                    break;
-                case "5": // 学院党委书记审批
-                    roleKey = "party_secretary_overseas_approver";
-                    break;
-                default:
-                    log.warn("未知的审批级别: {}", approvalLevel);
-                    return null;
-            }
-
-            // 获取对应角色
-            SysRoleBo roleQuery = new SysRoleBo();
-            roleQuery.setRoleKey(roleKey);
-            List<SysRoleVo> roles = roleService.selectRoleList(roleQuery);
-            if (roles.isEmpty()) {
-                log.error("未找到审批角色: {}", roleKey);
-                return null;
-            }
-            SysRoleVo role = roles.get(0);
-
-            // 获取角色下的用户(取第一个作为审批人)
-            SysUserBo userQuery = new SysUserBo();
-            userQuery.setRoleId(role.getRoleId());
-            PageQuery pageQuery2 = new PageQuery();
-            pageQuery2.setPageSize(1000); // 设置较大的页面大小以获取所有用户
-            List<SysUserVo> users = userService.selectAllocatedList(userQuery, pageQuery2).getRows();
-            if (users.isEmpty()) {
-                log.warn("审批角色 {} 下没有用户", roleKey);
-                return null;
-            }
-
-            // 返回第一个用户作为审批人
-            SysUserVo approver = users.get(0);
-            log.info("获取到第{}级审批人: {} ({})", approvalLevel, approver.getUserName(), approver.getNickName());
-            return approver.getUserId();
-
-        } catch (Exception e) {
-            log.error("获取第{}级审批人失败", approvalLevel, e);
-            return null;
-        }
-    }
+    // 🗑️ getNextApprover() 方法已删除
+    // 原因:采用动态审批人方案后,不再需要预先指定固定的审批人ID
+    // 新方案:待审批列表通过 getApprovalLevelsByUser() 动态查询
 
     /**
      * 根据审批级别获取审批环节描述
@@ -1257,6 +1274,69 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
         }
     }
 
+    /**
+     * 🆕 根据当前登录用户的审批权限,获取其可审批的级别列表
+     * 
+     * 权限到级别的映射:
+     * - rs:overseas:approve1 → 审批级别 "1" (部门负责人)
+     * - rs:overseas:approve2 → 审批级别 "2" (人事处)
+     * - rs:overseas:approve3 → 审批级别 "3" (纪委)
+     * - rs:overseas:approve4 → 审批级别 "4" (学院分管领导)
+     * - rs:overseas:approve5 → 审批级别 "5" (学院党委书记)
+     * 
+     * @param userId 用户ID
+     * @return 可审批的级别列表,例如:["2", "3"]
+     */
+    private List<String> getApprovalLevelsByUser(Long userId) {
+        try {
+            List<String> approvalLevels = new ArrayList<>();
+
+            // 🔴 修复:直接使用Sa-Token API检查权限,避免数据库查询问题
+            // 检查5个审批级别的权限(1-5级)
+            String[] levels = { "1", "2", "3", "4", "5" };
+            for (String level : levels) {
+                String permission = "rs:overseas:approve" + level;
+                if (StpUtil.hasPermission(permission)) {
+                    approvalLevels.add(level);
+                    log.info("✅ 用户 {} 拥有第{}级审批权限: {}", userId, level, permission);
+                }
+            }
+
+            // 检查通用审批权限(超级管理员)
+            if (StpUtil.hasPermission("rs:overseas:approve")) {
+                // 通用审批权限可以审批所有级别
+                log.info("✅ 用户 {} 拥有通用审批权限,可审批所有级别", userId);
+                approvalLevels.clear();
+                approvalLevels.addAll(Arrays.asList(levels));
+            }
+
+            if (approvalLevels.isEmpty()) {
+                log.warn("❌ 用户 {} 没有任何出国审批权限", userId);
+            } else {
+                log.info("📋 用户 {} 可审批的级别: {}", userId, approvalLevels);
+            }
+
+            return approvalLevels;
+
+        } catch (Exception e) {
+            log.error("❌ 获取用户 {} 的审批级别失败", userId, e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 🆕 根据审批级别获取对应的权限标识
+     * 
+     * @param approvalLevel 审批级别(2-5)
+     * @return 权限标识,例如:"rs:overseas:approve2"
+     */
+    private String getPermissionByLevel(String approvalLevel) {
+        if (approvalLevel == null || approvalLevel.isEmpty()) {
+            return null;
+        }
+        return "rs:overseas:approve" + approvalLevel;
+    }
+
     // 工作流监听器已移至独立的 OverseasApplicationWorkflowListener 类中
 
     // 状态更新方法已移至独立的 OverseasApplicationWorkflowListener 类中

+ 44 - 25
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalServiceImpl.java

@@ -246,19 +246,7 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService
             return;
         }
 
-        // 🔧 优先检查部门负责人权限(避免被rs-admin角色覆盖)
-        boolean isDeptApprover = isDeptApprover(currentUserId);
-        log.info("✓ 部门负责人检查结果: {}", isDeptApprover);
-
-        if (isDeptApprover) {
-            // 部门负责人只能看到本部门的所有申请(不限制审批人)
-            query.setDeptId(currentDeptId);
-            log.info("→ 权限结果:部门负责人,设置部门过滤 deptId={}", currentDeptId);
-            log.info("==================== 权限过滤结束 ====================");
-            return;
-        }
-
-        // 人事管理员可以看到所有申请(仅当不是部门负责人时)
+        // 人事管理员可以看到所有申请
         boolean hasAdminRole = StpUtil.hasRole("rs-admin");
         log.info("✓ 角色检查 [rs-admin]: {}", hasAdminRole);
         if (hasAdminRole) {
@@ -267,7 +255,28 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService
             return; // 全局权限
         }
 
-        // 其他角色只能看到分配给自己的审批
+        // 🔧 检查部门负责人权限(但不能与其他审批权限冲突)
+        boolean isDeptApprover = isDeptApprover(currentUserId);
+        log.info("✓ 部门负责人检查结果: {}", isDeptApprover);
+
+        // 检查是否有其他审批权限(人事处、纪委等)
+        boolean hasOtherApprovePermission = StpUtil.hasPermission("rs:overseas:approve2") || // 人事处
+                StpUtil.hasPermission("rs:overseas:approve3") || // 纪委
+                StpUtil.hasPermission("rs:overseas:approve4") || // 学院分管领导
+                StpUtil.hasPermission("rs:overseas:approve5"); // 学院党委书记
+        log.info("✓ 其他审批权限检查: {}", hasOtherApprovePermission);
+
+        if (isDeptApprover && !hasOtherApprovePermission) {
+            // 纯部门负责人:只能看到本部门且分配给自己的申请
+            query.setDeptId(currentDeptId);
+            query.setApproverId(currentUserId);
+            log.info("→ 权限结果:部门负责人,设置部门过滤 deptId={} 和审批人过滤 approverId={}",
+                    currentDeptId, currentUserId);
+            log.info("==================== 权限过滤结束 ====================");
+            return;
+        }
+
+        // 其他审批人(包括有其他审批权限的用户)只能看到分配给自己的审批
         query.setApproverId(currentUserId);
         log.info("→ 权限结果:普通审批人,设置审批人过滤 approverId={}", currentUserId);
         log.info("==================== 权限过滤结束 ====================");
@@ -502,23 +511,33 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService
             return;
         }
 
-        // 🔧 优先检查部门负责人权限(避免被rs-admin角色覆盖)
-        boolean isDeptApprover = isDeptApprover(currentUserId);
-        log.info("用户 {} 部门负责人检查结果: {}", currentUsername, isDeptApprover);
-
-        if (isDeptApprover) {
-            log.info("用户 {} 是部门负责人,设置部门过滤: {}", currentUsername, currentDeptId);
-            query.setDeptId(currentDeptId);
+        // 人事管理员可以看到所有申请
+        if (StpUtil.hasRole("rs-admin")) {
+            log.info("用户 {} 具有rs-admin角色,不设置过滤条件", currentUsername);
             return;
         }
 
-        // 人事管理员可以看到所有申请(仅当不是部门负责人时)
-        if (StpUtil.hasRole("rs-admin")) {
-            log.info("用户 {} 具有rs-admin角色,不设置过滤条件", currentUsername);
+        // 🔧 检查部门负责人权限(但不能与其他审批权限冲突)
+        boolean isDeptApprover = isDeptApprover(currentUserId);
+        log.info("用户 {} 部门负责人检查结果: {}", currentUsername, isDeptApprover);
+
+        // 检查是否有其他审批权限(人事处、纪委等)
+        boolean hasOtherApprovePermission = StpUtil.hasPermission("rs:overseas:approve2") || // 人事处
+                StpUtil.hasPermission("rs:overseas:approve3") || // 纪委
+                StpUtil.hasPermission("rs:overseas:approve4") || // 学院分管领导
+                StpUtil.hasPermission("rs:overseas:approve5"); // 学院党委书记
+        log.info("用户 {} 其他审批权限检查: {}", currentUsername, hasOtherApprovePermission);
+
+        if (isDeptApprover && !hasOtherApprovePermission) {
+            // 纯部门负责人:只能看到本部门且分配给自己的申请
+            log.info("用户 {} 是纯部门负责人,设置部门过滤: {} 和审批人过滤: {}",
+                    currentUsername, currentDeptId, currentUserId);
+            query.setDeptId(currentDeptId); // 只显示本部门员工的申请
+            query.setApproverId(currentUserId); // 只显示当前审批人是自己的申请(待审批)
             return;
         }
 
-        // 其他角色只能看到分配给自己的审批
+        // 其他审批人(包括有其他审批权限的用户)只能看到分配给自己的审批
         log.info("用户 {} 是普通审批人,设置审批人过滤: {}", currentUsername, currentUserId);
         query.setApproverId(currentUserId);
     }

+ 51 - 0
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApplicationMapper.xml

@@ -551,4 +551,55 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           AND u.deleted = '0'
     </select>
     
+    <!-- 🔧 新增:根据权限标识查询拥有该权限的用户ID列表 -->
+    <select id="selectUserIdsByPermission" parameterType="String" resultType="Long">
+        SELECT DISTINCT u.user_id
+        FROM sys_user u
+        INNER JOIN sys_user_role ur ON u.user_id = ur.user_id
+        INNER JOIN sys_role r ON ur.role_id = r.role_id
+        INNER JOIN sys_role_menu rm ON r.role_id = rm.role_id
+        INNER JOIN sys_menu m ON rm.menu_id = m.menu_id
+        WHERE m.perms = #{permissionCode}
+          AND m.del_flag = '0'
+          AND m.status = '0'
+          AND r.del_flag = '0'
+          AND r.status = '0'
+          AND u.deleted = '0'
+          AND u.status = '0'
+        ORDER BY u.user_id
+    </select>
+    
+    <!-- 🔧 新增:根据用户ID查询其拥有的出国审批权限列表 -->
+    <select id="selectApprovalPermissionsByUserId" parameterType="Long" resultType="String">
+        SELECT DISTINCT m.perms
+        FROM sys_menu m
+        INNER JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
+        INNER JOIN sys_role r ON rm.role_id = r.role_id
+        INNER JOIN sys_user_role ur ON r.role_id = ur.role_id
+        WHERE ur.user_id = #{userId}
+          AND m.perms LIKE 'rs:overseas:approve%'
+          AND m.status = '0'
+          AND r.del_flag = '0'
+          AND r.status = '0'
+        ORDER BY m.perms
+    </select>
+
+    <!-- 🔴 新增:查询待审批申请列表(支持部门过滤) -->
+    <select id="selectPendingApplications" resultType="org.dromara.rs.domain.RsOverseasApplication">
+        SELECT a.*
+        FROM rs_overseas_application a
+        INNER JOIN sys_user u ON a.applicant_id = u.user_id
+        WHERE a.status = '0'
+          AND a.deleted = '0'
+          AND a.approval_level IN
+        <foreach collection="approvalLevels" item="level" open="(" separator="," close=")">
+            #{level}
+        </foreach>
+        <!-- 🔴 关键:部门过滤逻辑(仅对部门负责人生效) -->
+        <if test="deptId != null">
+            AND u.dept_id = #{deptId}
+        </if>
+        ORDER BY a.apply_time DESC
+    </select>
+    
 </mapper> 

+ 27 - 27
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasApprovalMapper.xml

@@ -232,22 +232,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         
         UNION ALL
         
-        -- 查询待审批的申请(有审批记录且状态为待审核的)
+        -- 查询待审批的申请(使用current_approver_id查询当前审批人的待办)
         SELECT 
-            oa.approval_id,
+            NULL as approval_id,  -- 待审批申请没有审批记录ID
             app.application_id,
-            oa.approver_id,
-            oa.approver_name,
-            oa.approval_level,
-            oa.approval_status,
-            oa.approval_opinion,
-            oa.approval_time,
-            oa.tenant_id,
-            oa.create_by,
-            oa.create_time,
-            oa.update_by,
-            oa.update_time,
-            oa.deleted,
+            app.current_approver_id as approver_id,  -- 🔴 修复:使用申请表的当前审批人ID
+            app.current_approver as approver_name,   -- 🔴 修复:使用申请表的当前审批人名称
+            app.approval_level,  -- 🔴 修复:使用申请表的审批级别
+            '0' as approval_status,  -- 待审核状态
+            NULL as approval_opinion,
+            NULL as approval_time,
+            app.tenant_id,
+            app.create_by,
+            app.create_time,
+            app.update_by,
+            app.update_time,
+            app.deleted,
             -- 申请信息字段
             app.applicant_id,
             app.applicant_name,
@@ -258,10 +258,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             app.end_date,
             app.duration,
             -- 文本转换
-            CASE oa.approval_level 
-                WHEN '1' THEN '部门主管'
-                WHEN '2' THEN '分管领导'
-                WHEN '3' THEN '校领导'
+            CASE app.approval_level 
+                WHEN '1' THEN '部门负责人'
+                WHEN '2' THEN '人事处'
+                WHEN '3' THEN '纪委'
+                WHEN '4' THEN '学院分管领导'
+                WHEN '5' THEN '学院党委书记'
                 ELSE '未知级别'
             END as approval_level_text,
             '待审核' as approval_status_text,
@@ -279,12 +281,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 ELSE '未知目的'
             END as purpose_text,
             'pending' as record_type
-        FROM rs_overseas_approval oa
-        INNER JOIN rs_overseas_application app ON oa.application_id = app.application_id
-        WHERE oa.deleted = '0'
-            AND app.deleted = '0'
+        FROM rs_overseas_application app
+        WHERE app.deleted = '0'
             AND app.status = '0'  -- 申请状态:待审核
-            AND oa.approval_status = '0'  -- 审批状态:待审核
             <if test="applicationId != null">
                 AND app.application_id = #{applicationId}
             </if>
@@ -292,16 +291,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 AND app.applicant_name like concat('%', #{applicantName}, '%')
             </if>
             <if test="approvalLevel != null and approvalLevel != ''">
-                AND oa.approval_level = #{approvalLevel}
+                AND app.approval_level = #{approvalLevel}
             </if>
+            <!-- 🔴 核心修复:使用申请表的当前审批人ID进行过滤 -->
             <if test="approverId != null">
-                AND oa.approver_id = #{approverId}
+                AND app.current_approver_id = #{approverId}
             </if>
             <if test="approverName != null and approverName != ''">
-                AND oa.approver_name like concat('%', #{approverName}, '%')
+                AND app.current_approver like concat('%', #{approverName}, '%')
             </if>
             <if test="tenantId != null and tenantId != ''">
-                AND oa.tenant_id = #{tenantId}
+                AND app.tenant_id = #{tenantId}
             </if>
             <!-- 部门权限过滤:只显示本部门申请人的待审批申请 -->
             <if test="deptId != null">

+ 181 - 1
docs/开发文档/API/出国申请管理API.md

@@ -1061,4 +1061,184 @@ PDF模板包含以下空白填写区域(完全手工填写):
 **🆕 更新日志**:  
 - 2025-10-17 新增党籍保留PDF空白模板下载功能(线下手工填写)  
 - 接口路径调整:从 `/application/downloadPartyRetentionTemplate/{id}` 改为 `/record/downloadPartyRetention`  
-- 模板类型变更:从预填充模板改为完全空白模板 
+- 模板类型变更:从预填充模板改为完全空白模板
+
+---
+
+## 附录:工作流集成说明
+
+### 动态审批人方案
+
+#### 背景
+
+出国申请业务集成了Warm-Flow工作流引擎,审批流程为:**发起人 → 部门领导 → 校领导**。审批人根据申请人所属部门动态确定,系统需要实时查询当前待审批人信息。
+
+#### 方案设计
+
+##### 1. 数据库字段说明
+
+`rs_overseas_application` 表中的 `current_approver_id` 字段:
+
+- **字段类型**:`bigint(20)`
+- **字段作用**:**冗余展示字段**(非核心业务字段)
+- **维护方式**:不再由后端业务代码维护
+- **数据来源**:前端通过工作流API实时查询获取
+- **设计原因**:
+  - 审批人由工作流引擎动态确定,工作流表 `flow_node_user` 是唯一数据源
+  - 避免数据冗余和不一致问题
+  - 简化后端业务逻辑,降低维护成本
+
+##### 2. 后端实现
+
+**核心原则**:后端不维护 `current_approver_id` 字段
+
+```java
+// OverseasApplicationServiceImpl.java
+
+// ✅ 新增申请:不设置 current_approver_id
+public Long addOverseasApplication(OverseasApplicationBo bo) {
+    // ... 业务逻辑
+    // 不再包含:application.setCurrentApproverId(xxx);
+}
+
+// ✅ 提交审批:不设置 current_approver_id
+public R<Void> submitApplication(Long id) {
+    // ... 提交工作流
+    // 不再包含:application.setCurrentApproverId(xxx);
+}
+
+// ✅ 审批通过:不设置 current_approver_id
+public R<Void> approve(Long id, String remark) {
+    // ... 审批逻辑
+    // 不再包含:application.setCurrentApproverId(xxx);
+}
+```
+
+**代码清理**:
+
+- 所有 `setCurrentApproverId()` 调用已移除
+- 相关查询逻辑已删除
+- 字段仅作为数据库表结构保留(历史兼容)
+
+##### 3. 前端实现
+
+**核心方案**:通过工作流API实时查询当前审批人
+
+```typescript
+// overseasApplication/index.vue
+
+// 方案一:列表查询时批量获取审批人(推荐)
+const getList = async () => {
+  const response = await listOverseasApplication(queryParams.value);
+  dataList.value = response.rows;
+  total.value = response.total;
+  
+  // 批量查询审批人信息
+  await batchGetCurrentApproverNames();
+};
+
+// 批量获取审批人姓名
+const batchGetCurrentApproverNames = async () => {
+  const promises = dataList.value
+    .filter(item => item.status === 'processing')
+    .map(async item => {
+      try {
+        // 调用工作流API查询当前审批人
+        const approvers = await getCurrentApproversForTask(item.flowProcessId);
+        item.currentApproverName = approvers.map(u => u.name).join('、');
+      } catch (error) {
+        item.currentApproverName = '-';
+      }
+    });
+  
+  await Promise.all(promises);
+};
+
+// 方案二:详情查询时单独获取审批人
+const handleViewDetail = async (row: OverseasApplicationVo) => {
+  currentRow.value = row;
+  
+  // 如果流程进行中,查询当前审批人
+  if (row.flowProcessId && row.status === 'processing') {
+    try {
+      const approvers = await getCurrentApproversForTask(row.flowProcessId);
+      currentRow.value.currentApproverName = approvers.map(u => u.name).join('、');
+    } catch (error) {
+      currentRow.value.currentApproverName = '查询失败';
+    }
+  }
+  
+  detailDialogVisible.value = true;
+};
+```
+
+**工作流API调用**:
+
+```typescript
+// 查询当前审批人(从工作流引擎获取)
+import { getCurrentApproversForTask } from '@/api/workflow/common';
+
+// API 路径:/api/workflow/instance/{processId}/current-approvers
+// 返回格式:
+{
+  "code": 200,
+  "msg": "操作成功",
+  "data": [
+    {
+      "userId": 1001,
+      "userName": "张三",
+      "nodeId": 2,
+      "nodeName": "部门领导审批"
+    }
+  ]
+}
+```
+
+##### 4. 接口改造
+
+相关API接口响应中的 `current_approver_id` 字段说明:
+
+| 接口 | 字段说明 | 使用建议 |
+|------|---------|---------|
+| 查询列表 | 返回null或历史值 | 忽略,通过工作流API获取 |
+| 查询详情 | 返回null或历史值 | 忽略,通过工作流API获取 |
+| 新增/修改 | 不处理该字段 | 前端不传递 |
+| 提交审批 | 不处理该字段 | 前端不传递 |
+
+##### 5. 优势分析
+
+**✅ 数据一致性**
+- 工作流表是唯一数据源,避免双写不一致
+- 审批人变更(如转办、委派)自动生效
+
+**✅ 降低维护成本**
+- 后端无需在多个业务方法中同步更新
+- 减少代码复杂度和潜在Bug
+
+**✅ 实时性**
+- 前端每次查询都获取最新审批人
+- 支持工作流引擎的高级功能(并行审批、会签等)
+
+**✅ 扩展性**
+- 审批流程调整时,前端无需修改
+- 支持更复杂的审批场景(多人会签、或签等)
+
+#### 实施步骤
+
+1. ✅ **后端改造**:移除所有 `current_approver_id` 字段的赋值代码
+2. ✅ **前端改造**:实现基于工作流API的动态查询
+3. ⏳ **测试验证**:确保列表、详情页面正确显示审批人
+4. ⏳ **文档更新**:同步更新API文档和技术文档
+
+#### 注意事项
+
+- 历史数据中的 `current_approver_id` 字段可能存在脏数据,前端应忽略
+- 工作流API调用失败时,前端应显示友好提示(如 "-" 或 "查询失败")
+- 性能优化:列表页建议使用批量查询,避免N+1查询问题
+- 缓存策略:同一个流程实例的审批人信息可短时间缓存(如30秒)
+
+---
+
+**更新时间**:2025-10-28  
+**维护人员**:开发团队  
+**变更记录**:新增工作流动态审批人方案说明 

+ 60 - 9
web/src/views/rs/overseas/approval.vue

@@ -132,7 +132,7 @@
           :total="total"
           v-model:page="queryParams.pageNum"
           v-model:limit="queryParams.pageSize"
-          @pagination="getList"
+          @pagination="applyFilterAndPagination"
         />
       </el-card>
     </el-card>
@@ -344,10 +344,10 @@
 </template>
 
 <script setup lang="ts" name="OverseasApproval">
-import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
+import { ref, reactive, toRefs, onMounted, getCurrentInstance, computed } from 'vue';
 import { useRouter } from 'vue-router';
 import {
-  listOverseasApplication,
+  listPendingOverseasApplication,
   getOverseasApplication,
   approveOverseasApplication,
   listOverseasAttachments,
@@ -372,7 +372,8 @@ const viewAttachmentList = ref<OverseasAttachmentVO[]>([]);
 const state = reactive({
   // 列表相关
   loading: false,
-  applicationList: [] as OverseasApplicationVO[],
+  allApplicationList: [] as OverseasApplicationVO[], // 🔴 新增:存储所有待审批数据
+  applicationList: [] as OverseasApplicationVO[], // 过滤和分页后的数据
   total: 0,
   showSearch: true,
   queryParams: {
@@ -428,12 +429,20 @@ onMounted(() => {
 const getList = async () => {
   state.loading = true;
   try {
-    const response = await listOverseasApplication(state.queryParams);
-    state.applicationList = response.rows || [];
-    state.total = response.total || 0;
+    // 🔴 修复:使用待审批接口,只返回当前用户审批级别的待审批数据
+    const response = await listPendingOverseasApplication();
+    
+    // 存储所有待审批数据
+    state.allApplicationList = response.data || [];
+    
+    // 应用本地过滤和分页
+    applyFilterAndPagination();
+    
+    console.log('✅ 成功加载待审批申请列表:', state.allApplicationList.length, '条');
   } catch (error: any) {
-    console.error('获取申请列表失败:', error);
+    console.error('❌ 获取申请列表失败:', error);
     ElMessage.error('获取申请列表失败');
+    state.allApplicationList = [];
     state.applicationList = [];
     state.total = 0;
   } finally {
@@ -441,10 +450,52 @@ const getList = async () => {
   }
 };
 
+// 应用本地过滤和分页
+const applyFilterAndPagination = () => {
+  let filteredList = [...state.allApplicationList];
+  
+  // 🔴 应用搜索过滤
+  if (state.queryParams.applicantName) {
+    filteredList = filteredList.filter(item => 
+      item.applicantName?.includes(state.queryParams.applicantName || '')
+    );
+  }
+  
+  if (state.queryParams.department) {
+    filteredList = filteredList.filter(item => 
+      item.department?.includes(state.queryParams.department || '')
+    );
+  }
+  
+  if (state.queryParams.destination) {
+    filteredList = filteredList.filter(item => 
+      item.destination?.includes(state.queryParams.destination || '')
+    );
+  }
+  
+  if (state.queryParams.status) {
+    filteredList = filteredList.filter(item => 
+      item.status === state.queryParams.status
+    );
+  }
+  
+  // 更新总数
+  state.total = filteredList.length;
+  
+  // 🔴 应用分页
+  const { pageNum, pageSize } = state.queryParams;
+  const start = (pageNum - 1) * pageSize;
+  const end = start + pageSize;
+  state.applicationList = filteredList.slice(start, end);
+  
+  console.log(`📄 分页结果: 总计 ${state.total} 条,当前显示第 ${pageNum} 页,共 ${state.applicationList.length} 条`);
+};
+
 // 搜索查询
 const handleQuery = () => {
   state.queryParams.pageNum = 1;
-  getList();
+  // 🔴 修复:本地过滤,无需重新请求数据
+  applyFilterAndPagination();
 };
 
 // 重置搜索