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

This commit is contained in:
weizhong committed 2025-10-29 12:48:19 +08:00
1 parent f907b9cd40
commit 32b9822146
7 files changed
+593 -169

No files matched your search

@@ -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);
}
@@ -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");
// 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 (isDeptHead) {
// 🔴 部门负责人:只能看到本部门员工提交的申请
SysUserVo currentUser = userService.selectUserById(currentUserId);
if (currentUser != null && currentUser.getDeptId() != null) {
query.setApplicantDeptId(currentUser.getDeptId());
log.info("🔒 [部门权限过滤] 用户: {}, 部门ID: {}, 只能查看本部门申请",
currentUser.getUserName(), currentUser.getDeptId());
}
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));
log.info("🔍 查询用户待审批申请 - 用户ID: {}, 用户名: {}", currentUserId, currentUserName);
// 转换为VO对象
return applications.stream()
.map(this::convertToVo)
.collect(Collectors.toList());
// 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 类中
@@ -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);
return;
}
// 人事管理员可以看到所有申请(仅当不是部门负责人时)
// 人事管理员可以看到所有申请
if (StpUtil.hasRole("rs-admin")) {
log.info("用户 {} 具有rs-admin角色,不设置过滤条件", currentUsername);
return;
}
// 其他角色只能看到分配给自己的审批
// 🔧 检查部门负责人权限(但不能与其他审批权限冲突)
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);
}
@@ -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>
@@ -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
View File
@@ -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
View File
@@ -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();
};
// 重置搜索