“1028审批权限修改v0.1”

This commit is contained in:
weizhong committed 2025-10-28 14:08:52 +08:00
1 parent 851ebe86e4
commit 76ee97a90c
21 files changed
+1733 -1049

No files matched your search

@@ -26,6 +26,8 @@ import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.satoken.utils.LoginHelper;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import lombok.extern.slf4j.Slf4j;
import org.dromara.rs.domain.bo.RsOverseasApplicationBo;
import org.dromara.rs.domain.bo.GenerateRegistrationFormBo;
import org.dromara.rs.domain.vo.RsOverseasApplicationVo;
@@ -53,6 +55,7 @@ import org.dromara.system.domain.vo.SysOssVo;
* @author weizzzz
* @date 2025-07-28
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/rs/overseas/application")
@@ -93,47 +96,27 @@ public class RsOverseasApplicationController extends BaseController {
}
/**
* 查询待审批的申请列表(有审批权限的用户可以看到所有待审批申请)
* 查询当前用户的待审批申请列表(五级审批模式)
*/
@SaCheckPermission("rs:overseas:approve")
@SaCheckPermission("rs:overseasapproval:list")
@GetMapping("/pending/list")
public TableDataInfo<RsOverseasApplicationVo> pendingList(RsOverseasApplicationQuery query) {
// 不限制currentApprover,让有审批权限的用户看到所有待审批申请
// String currentUser = LoginHelper.getUsername();
// query.setCurrentApprover(currentUser);
query.setStatus("0"); // 只查询待审核状态
query.setTenantId(LoginHelper.getTenantId()); // 设置租户ID
return applicationService.queryPageList(query);
public R<List<RsOverseasApplicationVo>> pendingList() {
List<RsOverseasApplicationVo> list = applicationService.queryPendingByCurrentUser();
return R.ok(list);
}
/**
* 临时测试API - 查询待审批的申请列表(调试用)
*/
@SaCheckPermission("rs:overseas:approve")
@GetMapping("/pending/debug")
public R<Object> pendingDebug(RsOverseasApplicationQuery query) {
// 获取当前用户信息
String currentUser = LoginHelper.getUsername();
Long userId = LoginHelper.getUserId();
String tenantId = LoginHelper.getTenantId();
// 设置查询条件
query.setStatus("0"); // 只查询待审核状态
query.setTenantId(tenantId); // 设置租户ID
// 执行查询
TableDataInfo<RsOverseasApplicationVo> result = applicationService.queryPageList(query);
// 返回调试信息
Map<String, Object> debugInfo = new HashMap<>();
debugInfo.put("currentUser", currentUser);
debugInfo.put("userId", userId);
debugInfo.put("tenantId", tenantId);
debugInfo.put("queryParams", query);
debugInfo.put("result", result);
return R.ok(debugInfo);
}
// ========================================
// 五级审批模式说明
//
// 当前实现:自定义五级审批流程
// 1. 部门负责人审批(根据申请人部门动态分配)
// 2. 人事处审批(角色:hr_dept_overseas_approver)
// 3. 纪委审批(角色:discipline_overseas_approver)
// 4. 学院分管领导审批(角色:college_leader_overseas_approver)
// 5. 学院党委书记审批(角色:party_secretary_overseas_approver)
//
// 审批权限控制:每个审批人只能看到分配给自己的待审批申请
// ========================================
/**
* 根据状态查询申请列表
@@ -173,6 +156,23 @@ public class RsOverseasApplicationController extends BaseController {
return R.ok(applicationService.queryById(applicationId));
}
/**
* 获取申请人的部门负责人ID
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/dept-approver/{applicationId}")
public R<Long> getDepartmentApproverId(@PathVariable Long applicationId) {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null) {
return R.fail("申请不存在");
}
Long deptLeaderId = applicationService.getDepartmentApproverId(application.getApplicantId());
if (deptLeaderId == null) {
return R.fail("未找到部门负责人,请联系管理员配置");
}
return R.ok(deptLeaderId);
}
/**
* 新增出国申请
*/
@@ -191,7 +191,7 @@ public class RsOverseasApplicationController extends BaseController {
// 自动设置申请人信息
bo.setApplicantId(LoginHelper.getUserId());
bo.setApplicantName(LoginHelper.getUsername());
// ✅ 不覆盖用户填写的姓名,保留用户在表单中填写的申请证件姓名
// 自动设置部门信息
bo.setDepartment(LoginHelper.getDeptName());
@@ -306,17 +306,31 @@ public class RsOverseasApplicationController extends BaseController {
}
/**
* 审批出国申请
* 审批出国申请(支持标准工作流模式)
* 🔴 支持五级审批权限:满足任一级别权限即可访问接口,具体级别在Service层验证
*/
@SaCheckPermission("rs:overseas:approve")
@SaCheckPermission(value = {
"rs:overseas:approve", // 通用审批权限(超级管理员/高级审批者)
"rs:overseas:approve1", // 第1级:部门负责人
"rs:overseas:approve2", // 第2级:人事处
"rs:overseas:approve3", // 第3级:纪委
"rs:overseas:approve4", // 第4级:学院分管领导
"rs:overseas:approve5" // 第5级:学院党委书记
}, mode = SaMode.OR) // 🔴 关键:OR模式,满足任一权限即可通过
@Log(title = "审批出国申请", businessType = BusinessType.UPDATE)
@PostMapping("/approve")
public R<Void> approve(@RequestBody ApprovalRequest request) {
Long approverId = LoginHelper.getUserId();
String approverName = LoginHelper.getUsername();
// 🔴 标准工作流模式:支持任务ID参数
// 如果传入了taskId,将通过工作流引擎进行审批
// 如果没有taskId,则使用原有的业务审批逻辑(向后兼容)
return toAjax(applicationService.approveApplication(
request.getApplicationId(), approverId, approverName,
request.getApprovalStatus(), request.getApprovalOpinion()));
request.getApprovalStatus(), request.getApprovalOpinion(),
request.getTaskId()));
}
/**
@@ -356,12 +370,13 @@ public class RsOverseasApplicationController extends BaseController {
}
/**
* 审批请求对象
* 审批请求对象(支持标准工作流模式)
*/
public static class ApprovalRequest {
private Long applicationId;
private String approvalStatus;
private String approvalOpinion;
private String taskId; // 🔴 新增:工作流任务ID,支持标准工作流模式
public Long getApplicationId() {
return applicationId;
@@ -386,6 +401,14 @@ public class RsOverseasApplicationController extends BaseController {
public void setApprovalOpinion(String approvalOpinion) {
this.approvalOpinion = approvalOpinion;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
}
// ==================== PDF生成相关接口 ====================
@@ -419,6 +442,32 @@ public class RsOverseasApplicationController extends BaseController {
registrationFormService.downloadPdf(documentId, response);
}
// ==================== 动态审批人获取API ====================
/**
* 根据申请人ID获取部门审批人ID
*
* @param applicantId 申请人ID
*/
@SaCheckPermission("rs:overseas:list")
@GetMapping("/getDepartmentApproverByApplicant/{applicantId}")
public R<Long> getDepartmentApproverByApplicant(@PathVariable Long applicantId) {
Long departmentApproverId = applicationService.getDepartmentApproverId(applicantId);
return R.ok(departmentApproverId);
}
/**
* 根据申请人ID获取人事处审批人列表
*
* @param applicantId 申请人ID
*/
@SaCheckPermission("rs:overseas:list")
@GetMapping("/getHrApprovers/{applicantId}")
public R<List<Long>> getHrApprovers(@PathVariable Long applicantId) {
List<Long> hrApprovers = applicationService.getHrApprovers(applicantId);
return R.ok(hrApprovers);
}
// ==================== 盖章文件上传接口 ====================
/**
@@ -52,13 +52,22 @@ public class RsOverseasApprovalMonitorController extends BaseController {
* @param query 查询条件
* @return 审批中的申请列表
*/
// 🔴 标准化改造:删除自定义待审批查询接口
// 原因:出国申请工作流应使用标准工作流模式
// 用户应通过工作流API pageByTaskWait() 获取待办任务
// 而不是调用自定义的业务API
//
// 标准工作流模式:
// 1. 用户访问标准待办页面:/workflow/task/taskWaiting.vue
// 2. 调用工作流API:pageByTaskWait() 获取待办任务
// 3. 点击"办理"跳转:/rs/overseas/application/edit?taskId=xxx
// 4. 在业务表单页面进行审批操作
@SaCheckPermission("rs:overseas:list")
@GetMapping("/pending/list")
public TableDataInfo<RsOverseasApprovalMonitorVo> pendingList(RsOverseasApprovalMonitorQuery query) {
log.info("查询审批中的申请列表,查询条件:{}", query);
// 只查询待审核状态(status = 0)
query.setStatus("0");
return monitorService.queryPendingList(query);
public R<String> pendingList(RsOverseasApprovalMonitorQuery query) {
// 提示用户使用标准工作流API
return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务");
}
/**
@@ -62,13 +62,17 @@ public class RsOverseasItineraryChangeController extends BaseController {
}
/**
* 查询待审批的行程变更申请列表
* 🔴 标准化改造:删除自定义待审批查询接口
*
* 原因:出国行程变更工作流应使用标准工作流模式
* 用户应通过工作流API pageByTaskWait() 获取待办任务
* 而不是调用自定义的业务API
*/
@SaCheckPermission("rs:itineraryChange:approve")
@GetMapping("/pending/list")
public TableDataInfo<RsOverseasItineraryChangeVo> pendingList(RsOverseasItineraryChangeQuery query) {
query.setTenantId(LoginHelper.getTenantId());
return changeService.queryPendingList(query);
public R<String> pendingList(RsOverseasItineraryChangeQuery query) {
// 提示用户使用标准工作流API
return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务");
}
/**
@@ -181,4 +185,3 @@ public class RsOverseasItineraryChangeController extends BaseController {
return R.ok(changeService.queryByRecordId(recordId));
}
}
@@ -181,8 +181,13 @@ public class RsTrainingApplicationController extends BaseController {
this.userName = StringUtils.isNotBlank(userName) ? userName : "用户" + userId;
}
public Long getUserId() { return userId; }
public String getUserName() { return userName; }
public Long getUserId() {
return userId;
}
public String getUserName() {
return userName;
}
}
/**
@@ -250,25 +255,17 @@ public class RsTrainingApplicationController extends BaseController {
}
/**
* 查询待审批的培训申请列表(供审批人使用)
* 🔴 标准化改造:删除自定义待审批查询接口
*
* 原因:培训申请工作流应使用标准工作流模式
* 用户应通过工作流API pageByTaskWait() 获取待办任务
* 而不是调用自定义的业务API
*/
@SaCheckPermission("rs:trainingapproval:process")
@GetMapping("/pending/list")
public TableDataInfo<RsTrainingApplication> pendingList(RsTrainingApplication application, PageQuery pageQuery) {
// 设置租户ID
if (application.getTenantId() == null || application.getTenantId().isEmpty()) {
application.setTenantId(LoginHelper.getTenantId());
}
// 只查询待审核状态的申请(排除已通过状态)
application.setExcludeStatus("1"); // 排除已通过状态
log.info("查询待审批培训申请列表 - 用户: {}, 角色: {}, 查询条件: {}",
LoginHelper.getUsername(),
StpUtil.getRoleList(),
application);
return trainingApplicationService.selectPageRsTrainingApplicationList(application, pageQuery);
public R<String> pendingList(RsTrainingApplication application, PageQuery pageQuery) {
// 提示用户使用标准工作流API
return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务");
}
/**
@@ -276,7 +273,8 @@ public class RsTrainingApplicationController extends BaseController {
*/
@SaCheckPermission("rs:trainingapplication:query")
@GetMapping("/my")
public TableDataInfo<RsTrainingApplication> listMyApplications(RsTrainingApplication application, PageQuery pageQuery) {
public TableDataInfo<RsTrainingApplication> listMyApplications(RsTrainingApplication application,
PageQuery pageQuery) {
return trainingApplicationService.selectPageMyApplications(StpUtil.getLoginIdAsLong(), application, pageQuery);
}
@@ -285,7 +283,8 @@ public class RsTrainingApplicationController extends BaseController {
*/
@SaCheckPermission("rs:trainingapplication:list")
@GetMapping("/list/{courseId}")
public TableDataInfo<RsTrainingApplication> listByCourse(@PathVariable("courseId") Long courseId, RsTrainingApplication application, PageQuery pageQuery) {
public TableDataInfo<RsTrainingApplication> listByCourse(@PathVariable("courseId") Long courseId,
RsTrainingApplication application, PageQuery pageQuery) {
return trainingApplicationService.selectPageApplicationsByCourse(courseId, application, pageQuery);
}
@@ -260,7 +260,7 @@ public class RsOverseasApplication extends BaseEntity {
private String status;
/**
* 审批状态(工作流状态):draft-草稿,waiting-待审批,finish-已完成,cancel-已取消,back-已退回
* 审批状态(工作流状态):cancel-已撤销,draft-草稿,waiting-待审核,finish-已完成,invalid-已作废,back-已退回,termination-已终止
*/
@ExcelProperty(value = "审批状态")
private String approvalStatus;
@@ -272,7 +272,13 @@ public class RsOverseasApplication extends BaseEntity {
private String currentApprover;
/**
* 审批级别:1-部门主管,2-分管领导,3-校领导
* 当前审批人ID
*/
@ExcelProperty(value = "当前审批人ID")
private Long currentApproverId;
/**
* 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
*/
@ExcelProperty(value = "审批级别")
private String approvalLevel;
@@ -7,14 +7,12 @@ import org.dromara.common.mybatis.core.domain.BaseEntity;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -42,9 +40,8 @@ public class RsOverseasApplicationBo extends BaseEntity {
private String serialNumber;
/**
* 申请人ID
* 申请人ID(系统自动设置,关联当前登录用户)
*/
@NotNull(message = "申请人ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long applicantId;
/**
@@ -92,7 +89,7 @@ public class RsOverseasApplicationBo extends BaseEntity {
/**
* 所属部门
*/
@NotBlank(message = "所属部门不能为空", groups = { AddGroup.class, EditGroup.class })
@NotBlank(message = "所属部门不能为空", groups = { EditGroup.class })
private String department;
/**
@@ -252,7 +249,6 @@ public class RsOverseasApplicationBo extends BaseEntity {
/**
* 应急联系人及电话
*/
@NotBlank(message = "应急联系人及电话不能为空")
private String emergencyContact;
/**
@@ -276,7 +272,12 @@ public class RsOverseasApplicationBo extends BaseEntity {
private String currentApprover;
/**
* 审批级别
* 当前审批人ID
*/
private Long currentApproverId;
/**
* 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
*/
private String approvalLevel;
@@ -285,6 +286,11 @@ public class RsOverseasApplicationBo extends BaseEntity {
*/
private String status;
/**
* 审批状态(工作流状态):cancel-已撤销,draft-草稿,waiting-待审核,finish-已完成,invalid-已作废,back-已退回,termination-已终止
*/
private String approvalStatus;
/**
* 登记备案表盖章文件路径
*/
@@ -86,4 +86,10 @@ public class RsOverseasApplicationQuery extends PageQuery {
* 租户ID
*/
private String tenantId;
/**
* 🔴 申请人部门ID(用于部门负责人权限过滤)
* 部门负责人只能看到本部门员工提交的申请
*/
private Long applicantDeptId;
}
@@ -72,14 +72,14 @@ public class RsApplicationVo extends RsApplication {
* 获取应聘者手机号
*/
public String getApplicantPhone() {
return this.getContactPhone();
return this.applicantPhone != null ? this.applicantPhone : super.getContactPhone();
}
/**
* 设置应聘者手机号
*/
public void setApplicantPhone(String applicantPhone) {
this.setContactPhone(applicantPhone);
super.setContactPhone(applicantPhone);
this.applicantPhone = applicantPhone;
}
@@ -87,14 +87,14 @@ public class RsApplicationVo extends RsApplication {
* 获取应聘者邮箱
*/
public String getApplicantEmail() {
return this.getContactEmail();
return this.applicantEmail != null ? this.applicantEmail : super.getContactEmail();
}
/**
* 设置应聘者邮箱
*/
public void setApplicantEmail(String applicantEmail) {
this.setContactEmail(applicantEmail);
super.setContactEmail(applicantEmail);
this.applicantEmail = applicantEmail;
}
}
@@ -241,7 +241,7 @@ public class RsOverseasApplicationVo implements Serializable {
private String statusText;
/**
* 审批状态(工作流状态):draft-草稿,waiting-待审批,finish-已完成,cancel-已取消,back-已退回
* 审批状态(工作流状态):cancel-已撤销,draft-草稿,waiting-待审核,finish-已完成,invalid-已作废,back-已退回,termination-已终止
*/
private String approvalStatus;
@@ -261,7 +261,12 @@ public class RsOverseasApplicationVo implements Serializable {
private String currentApprover;
/**
* 审批级别
* 当前审批人ID
*/
private Long currentApproverId;
/**
* 审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
*/
private String approvalLevel;
@@ -41,6 +41,13 @@ public interface IRsOverseasApplicationService {
*/
List<RsOverseasApplicationVo> queryList(RsOverseasApplicationQuery query);
/**
* 查询当前用户的待审批申请列表
*
* @return 待审批申请集合
*/
List<RsOverseasApplicationVo> queryPendingByCurrentUser();
/**
* 查询出国申请详细信息
*
@@ -127,6 +134,20 @@ public interface IRsOverseasApplicationService {
Boolean approveApplication(Long applicationId, Long approverId, String approverName,
String approvalStatus, String approvalOpinion);
/**
* 审批出国申请(支持标准工作流模式)
*
* @param applicationId 申请ID
* @param approverId 审批人ID
* @param approverName 审批人姓名
* @param approvalStatus 审批结果:1-通过,2-拒绝
* @param approvalOpinion 审批意见
* @param taskId 工作流任务ID(可选,用于标准工作流模式)
* @return 是否成功
*/
Boolean approveApplication(Long applicationId, Long approverId, String approverName,
String approvalStatus, String approvalOpinion, String taskId);
/**
* 校验删除数据
*
@@ -181,4 +202,22 @@ public interface IRsOverseasApplicationService {
* @return 校验结果Map,包含 isValid(是否通过校验)和 message(提示信息)
*/
java.util.Map<String, Object> validateRegistrationPrerequisite(Long applicantId);
// ==================== 动态审批人获取方法 ====================
/**
* 根据申请人ID获取部门审批人ID
*
* @param applicantId 申请人ID
* @return 部门审批人ID
*/
Long getDepartmentApproverId(Long applicantId);
/**
* 根据申请人ID获取人事处审批人列表
*
* @param applicantId 申请人ID
* @return 人事处审批人ID列表
*/
List<Long> getHrApprovers(Long applicantId);
}
@@ -1,33 +1,29 @@
package org.dromara.rs.service.impl;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Calendar;
import java.util.stream.Collectors;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.domain.dto.StartProcessDTO;
import org.dromara.common.core.domain.event.ProcessEvent;
import org.dromara.common.core.domain.event.ProcessDeleteEvent;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.service.WorkflowService;
import org.dromara.common.core.utils.DateUtils;
// 基础功能导入
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.workflow.common.ConditionalOnEnable;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
// EventListener 导入已移除
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.dromara.rs.mapper.RsOverseasApplicationMapper;
import org.dromara.rs.mapper.RsOverseasApprovalMapper;
import org.dromara.rs.mapper.RsOverseasRegistrationMapper;
@@ -42,17 +38,26 @@ import org.dromara.rs.service.IRsOverseasRecordService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.dromara.rs.domain.vo.RsOverseasRecordVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.domain.vo.SysDeptVo;
import org.dromara.system.domain.vo.SysRoleVo;
import org.dromara.system.domain.bo.SysDeptBo;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.domain.bo.SysRoleBo;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.SysDept;
import org.dromara.system.domain.SysRole;
import org.dromara.common.mybatis.core.page.PageQuery;
/**
* 出国申请Service业务层处理
* 集成Warm-Flow工作流支持5级审批流程
*
* @author weizzzz
* @date 2025-07-28
*/
@ConditionalOnEnable
@Service
public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationService {
public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasApplicationMapper, RsOverseasApplication>
implements IRsOverseasApplicationService {
private static final Logger log = LoggerFactory.getLogger(RsOverseasApplicationServiceImpl.class);
@@ -68,14 +73,49 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
@Autowired
private RsOverseasRegistrationMapper registrationMapper;
@Autowired(required = false)
private WorkflowService workflowService;
@Autowired
private org.dromara.system.service.ISysUserService userService;
@Autowired
private org.dromara.system.service.ISysDeptService deptService;
@Autowired
private org.dromara.system.service.ISysRoleService roleService;
/**
* 查询出国申请分页列表
*/
@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());
}
} else {
// 3. 普通用户:只能看到自己提交的申请
if (query.getApplicantId() == null) {
query.setApplicantId(currentUserId);
log.info("🔒 [个人权限过滤] 用户ID: {}, 只能查看自己的申请", currentUserId);
}
}
} else {
log.info("✅ [管理员权限] 用户ID: {}, 可以查看所有申请", currentUserId);
}
// 使用自定义查询方法,包含状态文本等扩展字段
List<RsOverseasApplicationVo> list = baseMapper.selectOverseasApplicationList(query);
@@ -179,6 +219,49 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
return baseMapper.selectPendingApplicationsByLevel(approvalLevel);
}
/**
* 查询当前用户的待审批申请列表
*/
@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());
}
/**
* 转换实体为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;
}
/**
* 新增出国申请(直接进入待审核状态)
*/
@@ -190,10 +273,23 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
add.setApplyTime(new Date());
// 直接设置为待审核状态(适应数据库DEFAULT '0'设计)
add.setStatus("0");
// 设置初始审批级别为部门主管
// 设置初始审批级别为部门负责人
add.setApprovalLevel("1");
// 设置当前审批环节描述
add.setCurrentApprover("待部门主管审批");
// 动态分配第1级审批人(部门负责人)
try {
Long deptApproverId = findDeptApproverByApplicant(add.getApplicantId());
if (deptApproverId != null) {
add.setCurrentApproverId(deptApproverId);
add.setCurrentApprover("待部门负责人审批");
} else {
add.setCurrentApprover("待指定部门负责人");
log.warn("未找到申请人 {} 对应的部门负责人", add.getApplicantId());
}
} catch (Exception e) {
log.error("分配部门负责人失败,申请人ID: {}, 错误: {}", add.getApplicantId(), e.getMessage());
add.setCurrentApprover("待指定部门负责人");
}
// 自动计算出国天数
if (add.getStartDate() != null && add.getEndDate() != null) {
long diffInMillies = add.getEndDate().getTime() - add.getStartDate().getTime();
@@ -341,7 +437,7 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
}
/**
* 提交出国申请(简化版 - 仅更新状态,工作流由前端直接启动)
* 提交出国申请(传统审批模式)
*/
@Override
@Transactional
@@ -351,21 +447,20 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
throw new RuntimeException("申请不存在");
}
// 只有草稿状态的申请才能提交
if (application.getApprovalStatus() != null &&
!"draft".equals(application.getApprovalStatus()) &&
!"".equals(application.getApprovalStatus())) {
throw new RuntimeException("只有草稿状态的申请才能提交");
// 只有草稿状态或待审核状态的申请才能提交
if ("1".equals(application.getStatus()) || "4".equals(application.getStatus())) {
throw new RuntimeException("已通过或已完成的申请不能重新提交");
}
// 设置状态为待审核(工作流状态由工作流监听器管理)
// 设置状态为待审核
application.setStatus("0");
application.setApprovalStatus(BusinessStatusEnum.WAITING.getStatus());
application.setApplyTime(new Date());
application.setApprovalLevel("1");
application.setCurrentApprover("待审批");
boolean updated = baseMapper.updateById(application) > 0;
log.info("✅ [出国申请提交] 申请状态更新成功, applicationId: {}, 申请人: {}",
log.info("✅ [出国申请提交] 提交成功, applicationId: {}, 申请人: {}",
applicationId, application.getApplicantName());
return updated;
@@ -406,6 +501,15 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
throw new RuntimeException("只有待审核状态的申请才能审批");
}
// 🔴 新增:验证审批人权限级别
String currentApprovalLevel = application.getApprovalLevel();
if (!validateApproverLevel(approverId, currentApprovalLevel)) {
throw new ServiceException(String.format(
"您没有第%s级审批权限,当前申请需要第%s级审批人审批",
currentApprovalLevel, currentApprovalLevel));
}
log.info("✅ 审批人 {} 拥有第{}级审批权限验证通过", approverName, currentApprovalLevel);
// 创建审批记录
RsOverseasApproval approval = new RsOverseasApproval();
approval.setApplicationId(applicationId);
@@ -432,8 +536,13 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
String nextLevel = getNextApprovalLevel(application.getApprovalLevel());
if (nextLevel != null) {
// 进入下一级审批
Long nextApproverId = getNextApprover(nextLevel);
application.setApprovalLevel(nextLevel);
application.setCurrentApprover("待指定");
application.setCurrentApproverId(nextApproverId);
application.setCurrentApprover(getApprovalLevelDescription(nextLevel));
log.info("审批流转到第{}级,申请ID: {}, 下一级审批人ID: {}",
nextLevel, applicationId, nextApproverId);
} else {
// 所有级别审批完成 - 先尝试创建出国记录,再更新申请状态
log.info("开始最终审批处理,申请ID: {}, 当前状态: {}, 申请人: {}",
@@ -471,14 +580,51 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
return updateResult;
}
/**
* 审批出国申请(支持标准工作流模式)
* 🔴 标准化改造:新增重载方法支持工作流任务ID
*/
@Override
@Transactional
public Boolean approveApplication(Long applicationId, Long approverId, String approverName,
String approvalStatus, String approvalOpinion, String taskId) {
// 🔴 标准化改造:如果提供了taskId,说明是标准工作流模式
if (taskId != null && !taskId.trim().isEmpty()) {
log.info("🔄 [标准工作流审批] 申请ID: {}, 任务ID: {}, 审批人: {}, 审批状态: {}",
applicationId, taskId, approverName, approvalStatus);
// TODO: 这里应该调用工作流引擎的API来完成审批
// 例如:workflowService.completeTask(taskId, approvalStatus, approvalOpinion);
// 目前先调用原有的审批逻辑(向后兼容)
log.info("⚠️ [兼容模式] 暂时使用原有审批逻辑,等待集成工作流引擎API");
}
// 调用原有的审批方法
return approveApplication(applicationId, approverId, approverName, approvalStatus, approvalOpinion);
}
/**
* 获取下一级审批级别
* 修改为单级审批制度:审批通过后直接完成,不进入下一级
* 五级审批制度:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
*/
private String getNextApprovalLevel(String currentLevel) {
// 单级审批:直接返回null表示审批完成
switch (currentLevel) {
case "1":
return "2"; // 部门负责人 → 人事处
case "2":
return "3"; // 人事处 → 纪委
case "3":
return "4"; // 纪委 → 学院分管领导
case "4":
return "5"; // 学院分管领导 → 学院党委书记
case "5":
return null; // 学院党委书记 → 审批完成
default:
return null;
}
}
/**
* 修复已审批通过但未创建记录的申请
@@ -742,7 +888,7 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
@Transactional
public Boolean deleteWithValidByIds(List<Long> ids, Boolean isValid) {
validatedByIds(ids, isValid);
return baseMapper.deleteBatchIds(ids) > 0;
return baseMapper.deleteByIds(ids) > 0;
}
/**
@@ -826,134 +972,330 @@ public class RsOverseasApplicationServiceImpl implements IRsOverseasApplicationS
return result;
}
// ==================== 动态审批人获取方法实现 ====================
/**
* 工作流总体流程监听器
* 监听工作流状态变化(草稿、撤销、退回、作废、终止、已完成等)
* 根据申请人ID获取部门审批人ID
*
* @param processEvent 工作流事件
* @param applicantId 申请人ID
* @return 部门审批人ID
*/
@EventListener(condition = "#processEvent.flowCode=='overseas-application'")
public void processHandler(ProcessEvent processEvent) {
log.info("🔄 [工作流监听] 出国申请工作流事件: {}", processEvent.toString());
Long applicationId = Long.valueOf(processEvent.getBusinessId());
RsOverseasApplication application = baseMapper.selectById(applicationId);
if (application == null) {
log.error("❌ [工作流监听] 申请不存在, applicationId: {}", applicationId);
return;
}
// 更新工作流状态
String workflowStatus = processEvent.getStatus();
application.setApprovalStatus(workflowStatus);
log.info("📝 [工作流监听] 更新审批状态: {} → {}", applicationId, workflowStatus);
// 根据不同的工作流状态更新业务状态
if (BusinessStatusEnum.FINISH.getStatus().equals(workflowStatus)) {
// 工作流完成 - 所有5级审批通过
log.info("🎉 [工作流监听] 所有审批级别完成,申请ID: {}, 开始创建出国记录", applicationId);
@Override
public Long getDepartmentApproverId(Long applicantId) {
try {
// 创建出国记录
Boolean recordCreated = overseasRecordService.createRecordFromApplication(applicationId);
if (recordCreated) {
application.setStatus("1"); // 已通过
application.setApprovalTime(new Date());
application.setCurrentApprover("审批完成");
log.info("✅ [工作流监听] 出国记录创建成功, applicationId: {}", applicationId);
} else {
log.error("❌ [工作流监听] 出国记录创建失败, applicationId: {}", applicationId);
application.setAdminRemark("审批完成但创建出国记录失败,请联系管理员");
// 1. 根据申请人ID获取用户信息
SysUserVo user = userService.selectUserById(applicantId);
if (user == null) {
log.warn("用户不存在,applicantId: {}", applicantId);
return null;
}
// 2. 获取用户所在部门ID
Long deptId = user.getDeptId();
if (deptId == null) {
log.warn("用户没有部门信息,applicantId: {}", applicantId);
return null;
}
// 3. 根据部门ID获取部门负责人
SysDeptVo dept = deptService.selectDeptById(deptId);
if (dept == null) {
log.warn("部门不存在,deptId: {}", deptId);
return null;
}
// 4. 获取部门负责人ID(假设部门表中有leader字段存储负责人ID)
Long leaderId = dept.getLeader();
if (leaderId == null) {
log.warn("部门没有负责人,deptId: {}", deptId);
// 如果没有设置负责人,可以返回默认的部门审批人或者抛出异常
return null;
}
log.info("获取到部门审批人,applicantId: {}, deptId: {}, leaderId: {}", applicantId, deptId, leaderId);
return leaderId;
} catch (Exception e) {
log.error("❌ [工作流监听] 创建出国记录异常: ", e);
application.setAdminRemark("审批完成但创建出国记录异常: " + e.getMessage());
log.error("获取部门审批人失败,applicantId: {}", applicantId, e);
return null;
}
} else if (BusinessStatusEnum.BACK.getStatus().equals(workflowStatus)) {
// 工作流退回 - 审批拒绝
application.setStatus("2"); // 已拒绝
application.setCurrentApprover("审批拒绝");
log.info("⚠️ [工作流监听] 审批被拒绝, applicationId: {}", applicationId);
} else if (BusinessStatusEnum.CANCEL.getStatus().equals(workflowStatus)) {
// 工作流取消
application.setStatus("3"); // 已取消
application.setCurrentApprover("已取消");
log.info("⚠️ [工作流监听] 申请已取消, applicationId: {}", applicationId);
} else if (BusinessStatusEnum.WAITING.getStatus().equals(workflowStatus)) {
// 工作流进行中
application.setStatus("0"); // 待审核
log.info("🔄 [工作流监听] 审批进行中, applicationId: {}", applicationId);
} else if (BusinessStatusEnum.DRAFT.getStatus().equals(workflowStatus)) {
// 工作流草稿
application.setStatus(null); // 草稿状态
application.setCurrentApprover("草稿");
log.info("📝 [工作流监听] 申请状态:草稿, applicationId: {}", applicationId);
}
// 处理审批意见和审批人信息
Map<String, Object> params = processEvent.getParams();
if (MapUtil.isNotEmpty(params)) {
String handler = Convert.toStr(params.get("handler"));
String message = Convert.toStr(params.get("message"));
log.info("👤 [工作流监听] 审批人: {}, 审批意见: {}", handler, message);
// 将审批意见追加到管理员备注
if (StringUtils.isNotEmpty(message)) {
String existingRemark = application.getAdminRemark();
if (StringUtils.isNotEmpty(existingRemark)) {
application.setAdminRemark(existingRemark + "\n" + message);
} else {
application.setAdminRemark(message);
}
}
}
// 如果是提交状态,设置为等待审批
if (processEvent.getSubmit()) {
application.setApprovalStatus(BusinessStatusEnum.WAITING.getStatus());
application.setStatus("0");
application.setCurrentApprover("部门负责人待审批");
log.info("📮 [工作流监听] 申请已提交,等待审批, applicationId: {}", applicationId);
}
baseMapper.updateById(application);
log.info("✅ [工作流监听] 申请状态更新完成, applicationId: {}", applicationId);
}
/**
* 监听删除流程事件
* 当工作流被删除时同步更新业务状态
* 根据申请人ID获取人事处审批人列表
*
* @param processDeleteEvent 删除事件
* @param applicantId 申请人ID
* @return 人事处审批人ID列表
*/
@EventListener(condition = "#processDeleteEvent.flowCode=='overseas-application'")
public void processDeleteHandler(ProcessDeleteEvent processDeleteEvent) {
log.info("🗑️ [工作流监听] 监听删除流程事件: {}", processDeleteEvent.toString());
@Override
public List<Long> getHrApprovers(Long applicantId) {
try {
// 1. 查询人事处部门(假设人事处的部门名称包含"人事"或者有特定的部门编码)
List<SysDeptVo> hrDepts = deptService.selectDeptList(new SysDeptBo());
List<Long> hrApprovers = new ArrayList<>();
Long applicationId = Long.valueOf(processDeleteEvent.getBusinessId());
RsOverseasApplication application = baseMapper.selectById(applicationId);
for (SysDeptVo dept : hrDepts) {
// 根据部门名称判断是否为人事处
if (dept.getDeptName() != null &&
(dept.getDeptName().contains("人事") ||
dept.getDeptName().contains("人力资源"))) {
if (ObjectUtil.isNull(application)) {
log.warn("⚠️ [工作流监听] 申请不存在,无需处理, applicationId: {}", applicationId);
return;
// 获取该部门的负责人作为审批人
if (dept.getLeader() != null) {
hrApprovers.add(dept.getLeader());
}
}
}
// 更新为取消状态
application.setApprovalStatus("cancel");
application.setStatus("3"); // 已取消
application.setCurrentApprover("已取消");
baseMapper.updateById(application);
log.info("✅ [工作流监听] 申请状态已更新为取消, applicationId: {}", applicationId);
// 如果没有找到人事处审批人,返回默认的人事处审批人列表
if (hrApprovers.isEmpty()) {
log.warn("没有找到人事处审批人,applicantId: {}", applicantId);
// 可以配置默认的人事处审批人ID列表
// hrApprovers.add(defaultHrApproverId);
}
log.info("获取到人事处审批人列表,applicantId: {}, hrApprovers: {}", applicantId, hrApprovers);
return hrApprovers;
} catch (Exception e) {
log.error("获取人事处审批人失败,applicantId: {}", applicantId, e);
return new ArrayList<>();
}
}
/**
* 根据申请人查找对应的部门负责人审批人ID
*
* @param applicantId 申请人ID
* @return 部门负责人审批人ID
*/
private Long findDeptApproverByApplicant(Long applicantId) {
try {
// 1. 获取申请人信息
SysUserVo applicant = userService.selectUserById(applicantId);
if (applicant == null || applicant.getDeptId() == null) {
log.warn("申请人不存在或未分配部门,申请人ID: {}", applicantId);
return null;
}
Long deptId = applicant.getDeptId();
log.info("查找部门负责人,申请人: {}, 部门ID: {}", applicant.getUserName(), deptId);
// 2. 查找该部门对应的部门负责人审批人
return findDeptApproverByDeptId(deptId);
} catch (Exception e) {
log.error("查找部门负责人失败,申请人ID: {}", applicantId, e);
return null;
}
}
/**
* 根据部门ID查找对应的部门负责人审批人
*
* @param deptId 部门ID
* @return 部门负责人审批人ID
*/
private Long findDeptApproverByDeptId(Long deptId) {
try {
// 1. 获取"部门负责人审批(出国申请)"角色
SysRoleBo roleQuery = new SysRoleBo();
roleQuery.setRoleKey("dept_head_overseas_approver");
List<SysRoleVo> roles = roleService.selectRoleList(roleQuery);
if (roles.isEmpty()) {
log.error("未找到部门负责人审批角色: dept_head_overseas_approver");
return null;
}
SysRoleVo deptApproverRole = roles.get(0);
// 2. 获取该角色下的所有用户
SysUserBo userQuery = new SysUserBo();
userQuery.setRoleId(deptApproverRole.getRoleId());
PageQuery pageQuery = new PageQuery();
pageQuery.setPageSize(1000); // 设置较大的页面大小以获取所有用户
List<SysUserVo> allocatedUsers = userService.selectAllocatedList(userQuery, pageQuery).getRows();
if (allocatedUsers.isEmpty()) {
log.warn("部门负责人审批角色下没有用户");
return null;
}
// 3. 遍历审批人,找到与申请人同部门的审批人
for (SysUserVo approver : allocatedUsers) {
if (approver.getDeptId() != null && approver.getDeptId().equals(deptId)) {
log.info("找到部门负责人审批人: {} ({}), 部门ID: {}",
approver.getUserName(), approver.getNickName(), deptId);
return approver.getUserId();
}
}
// 4. 如果没找到同部门审批人,尝试查找父部门负责人
log.info("当前部门 {} 没有找到负责人,尝试查找父部门负责人", deptId);
return findParentDeptApprover(deptId, allocatedUsers);
} catch (Exception e) {
log.error("查找部门负责人失败,部门ID: {}", deptId, e);
return null;
}
}
/**
* 查找父部门的负责人审批人
*
* @param deptId 当前部门ID
* @param approvers 所有部门负责人审批人列表
* @return 父部门负责人审批人ID
*/
private Long findParentDeptApprover(Long deptId, List<SysUserVo> approvers) {
try {
// 获取当前部门信息
SysDeptVo currentDept = deptService.selectDeptById(deptId);
if (currentDept == null || currentDept.getParentId() == null || currentDept.getParentId() == 0) {
log.warn("部门 {} 没有父部门", deptId);
return null;
}
Long parentDeptId = currentDept.getParentId();
log.info("查找父部门负责人,父部门ID: {}", parentDeptId);
// 在审批人列表中查找父部门的负责人
for (SysUserVo approver : approvers) {
if (approver.getDeptId() != null && approver.getDeptId().equals(parentDeptId)) {
log.info("找到父部门负责人审批人: {} ({}), 父部门ID: {}",
approver.getUserName(), approver.getNickName(), parentDeptId);
return approver.getUserId();
}
}
// 递归查找更上级的父部门
return findParentDeptApprover(parentDeptId, approvers);
} catch (Exception e) {
log.error("查找父部门负责人失败,部门ID: {}", deptId, e);
return null;
}
}
/**
* 根据审批级别获取下一级审批人ID
*
* @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;
}
}
/**
* 根据审批级别获取审批环节描述
*
* @param approvalLevel 审批级别
* @return 审批环节描述
*/
private String getApprovalLevelDescription(String approvalLevel) {
switch (approvalLevel) {
case "1":
return "待部门负责人审批";
case "2":
return "待人事处审批";
case "3":
return "待纪委审批";
case "4":
return "待学院分管领导审批";
case "5":
return "待学院党委书记审批";
default:
return "待审批";
}
}
// 工作流监听器已移至独立的 OverseasApplicationWorkflowListener 类中
// 状态更新方法已移至独立的 OverseasApplicationWorkflowListener 类中
// 删除事件监听器已移至独立的 OverseasApplicationWorkflowListener 类中
// 任务创建监听器已移至独立的 OverseasApplicationWorkflowListener 类中
// 删除了复杂的审批记录创建方法 - 使用标准工作流管理审批记录
// 删除了复杂的部门负责人查找方法 - 工作流引擎会自动处理审批人分配
/**
* 🔴 新增方法:验证审批人是否有指定级别的审批权限
*
* @param approverId 审批人ID
* @param approvalLevel 需要的审批级别(1-5)
* @return true-有权限,false-无权限
*/
private boolean validateApproverLevel(Long approverId, String approvalLevel) {
// 检查通用审批权限(超级管理员/高级审批者)
if (StpUtil.hasPermission("rs:overseas:approve")) {
log.info("用户 {} 拥有通用审批权限 rs:overseas:approve", approverId);
return true;
}
// 检查分级审批权限
String requiredPermission = "rs:overseas:approve" + approvalLevel;
boolean hasPermission = StpUtil.hasPermission(requiredPermission);
if (hasPermission) {
log.info("用户 {} 拥有第{}级审批权限 {}",
approverId, approvalLevel, requiredPermission);
} else {
log.warn("用户 {} 缺少第{}级审批权限 {},权限验证失败",
approverId, approvalLevel, requiredPermission);
}
return hasPermission;
}
}
@@ -1,6 +1,7 @@
package org.dromara.rs.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
@@ -51,8 +52,8 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
log.info("查询审批中的申请列表,查询条件:{}", query);
// 构建查询条件
LambdaQueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
wrapper.eq(RsOverseasApplication::getStatus, "0"); // 只查询待审核状态
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
wrapper.eq("status", "0"); // 只查询待审核状态
// 分页查询
Page<RsOverseasApplication> page = applicationMapper.selectPage(
@@ -80,8 +81,8 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
log.info("查询已完成审批的申请列表,查询条件:{}", query);
// 构建查询条件
LambdaQueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
wrapper.eq(RsOverseasApplication::getStatus, "1"); // 只查询已通过状态
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
wrapper.eq("status", "1"); // 只查询已通过状态
// 分页查询
Page<RsOverseasApplication> page = applicationMapper.selectPage(
@@ -137,9 +138,9 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
// 1. 待审批总数
Long pendingCount = applicationMapper.selectCount(
Wrappers.<RsOverseasApplication>lambdaQuery()
.eq(RsOverseasApplication::getStatus, "0")
.eq(RsOverseasApplication::getTenantId, tenantId));
Wrappers.<RsOverseasApplication>query()
.eq("status", "0")
.eq("tenant_id", tenantId));
statistics.put("pendingCount", pendingCount);
// 2. 各审批级别的待审批数量
@@ -151,25 +152,25 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
// 3. 已完成审批总数
Long completedCount = applicationMapper.selectCount(
Wrappers.<RsOverseasApplication>lambdaQuery()
.eq(RsOverseasApplication::getStatus, "1")
.eq(RsOverseasApplication::getTenantId, tenantId));
Wrappers.<RsOverseasApplication>query()
.eq("status", "1")
.eq("tenant_id", tenantId));
statistics.put("completedCount", completedCount);
// 4. 今日新增申请数
LocalDate today = LocalDate.now();
Date startOfToday = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
Long todayCount = applicationMapper.selectCount(
Wrappers.<RsOverseasApplication>lambdaQuery()
.ge(RsOverseasApplication::getApplyTime, startOfToday)
.eq(RsOverseasApplication::getTenantId, tenantId));
Wrappers.<RsOverseasApplication>query()
.ge("apply_time", startOfToday)
.eq("tenant_id", tenantId));
statistics.put("todayCount", todayCount);
// 5. 已拒绝总数
Long rejectedCount = applicationMapper.selectCount(
Wrappers.<RsOverseasApplication>lambdaQuery()
.eq(RsOverseasApplication::getStatus, "2")
.eq(RsOverseasApplication::getTenantId, tenantId));
Wrappers.<RsOverseasApplication>query()
.eq("status", "2")
.eq("tenant_id", tenantId));
statistics.put("rejectedCount", rejectedCount);
log.info("审批进度统计结果:{}", statistics);
@@ -181,43 +182,50 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
/**
* 构建查询条件
*/
private LambdaQueryWrapper<RsOverseasApplication> buildQueryWrapper(RsOverseasApprovalMonitorQuery query) {
LambdaQueryWrapper<RsOverseasApplication> wrapper = Wrappers.lambdaQuery();
private QueryWrapper<RsOverseasApplication> buildQueryWrapper(RsOverseasApprovalMonitorQuery query) {
QueryWrapper<RsOverseasApplication> wrapper = Wrappers.query();
// 申请人姓名(模糊查询)
wrapper.like(StringUtils.isNotBlank(query.getApplicantName()),
RsOverseasApplication::getApplicantName, query.getApplicantName());
if (StringUtils.isNotBlank(query.getApplicantName())) {
wrapper.like("applicant_name", query.getApplicantName());
}
// 所属部门(模糊查询)
wrapper.like(StringUtils.isNotBlank(query.getDepartment()),
RsOverseasApplication::getDepartment, query.getDepartment());
if (StringUtils.isNotBlank(query.getDepartment())) {
wrapper.like("department", query.getDepartment());
}
// 目的地国家(模糊查询)
wrapper.like(StringUtils.isNotBlank(query.getDestination()),
RsOverseasApplication::getDestination, query.getDestination());
if (StringUtils.isNotBlank(query.getDestination())) {
wrapper.like("destination", query.getDestination());
}
// 审批级别
wrapper.eq(StringUtils.isNotBlank(query.getApprovalLevel()),
RsOverseasApplication::getApprovalLevel, query.getApprovalLevel());
if (StringUtils.isNotBlank(query.getApprovalLevel())) {
wrapper.eq("approval_level", query.getApprovalLevel());
}
// 当前审批人(模糊查询)
wrapper.like(StringUtils.isNotBlank(query.getCurrentApprover()),
RsOverseasApplication::getCurrentApprover, query.getCurrentApprover());
if (StringUtils.isNotBlank(query.getCurrentApprover())) {
wrapper.like("current_approver", query.getCurrentApprover());
}
// 申请时间范围
wrapper.ge(query.getApplyTimeStart() != null,
RsOverseasApplication::getApplyTime, query.getApplyTimeStart());
wrapper.le(query.getApplyTimeEnd() != null,
RsOverseasApplication::getApplyTime, query.getApplyTimeEnd());
if (query.getApplyTimeStart() != null) {
wrapper.ge("apply_time", query.getApplyTimeStart());
}
if (query.getApplyTimeEnd() != null) {
wrapper.le("apply_time", query.getApplyTimeEnd());
}
// 租户ID
String tenantId = StringUtils.isNotBlank(query.getTenantId())
? query.getTenantId()
: LoginHelper.getTenantId();
wrapper.eq(RsOverseasApplication::getTenantId, tenantId);
wrapper.eq("tenant_id", tenantId);
// 按申请时间倒序排列
wrapper.orderByDesc(RsOverseasApplication::getApplyTime);
wrapper.orderByDesc("apply_time");
return wrapper;
}
@@ -232,10 +240,11 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
vo.setApplicationId(application.getApplicationId());
// 查询关联的出国记录ID(如果已完成审批)
if ("1".equals(application.getStatus()) || "4".equals(application.getStatus())) {
String status = application.getStatus();
if ("1".equals(status) || "4".equals(status)) {
RsOverseasRecordVo record = recordMapper
.selectOverseasRecordByApplicationId(application.getApplicationId());
if (record != null) {
if (record != null && record.getRecordId() != null) {
vo.setRecordId(record.getRecordId());
}
}
@@ -313,10 +322,10 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
private void fillApprovalProgress(RsOverseasApprovalMonitorVo vo, RsOverseasApplication application) {
// 查询该申请的所有审批记录
List<RsOverseasApproval> approvals = approvalMapper.selectList(
Wrappers.<RsOverseasApproval>lambdaQuery()
.eq(RsOverseasApproval::getApplicationId, application.getApplicationId())
.orderByAsc(RsOverseasApproval::getApprovalLevel)
.orderByDesc(RsOverseasApproval::getApprovalTime));
Wrappers.<RsOverseasApproval>query()
.eq("application_id", application.getApplicationId())
.orderByAsc("approval_level")
.orderByDesc("approval_time"));
if (approvals != null && !approvals.isEmpty()) {
// 已审批级别数(状态为1-通过的记录)
@@ -393,10 +402,10 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
*/
private Long countByStatusAndLevel(String status, String level, String tenantId) {
return applicationMapper.selectCount(
Wrappers.<RsOverseasApplication>lambdaQuery()
.eq(RsOverseasApplication::getStatus, status)
.eq(RsOverseasApplication::getApprovalLevel, level)
.eq(RsOverseasApplication::getTenantId, tenantId));
Wrappers.<RsOverseasApplication>query()
.eq("status", status)
.eq("approval_level", level)
.eq("tenant_id", tenantId));
}
/**
@@ -24,7 +24,6 @@ import org.dromara.rs.domain.query.RsOverseasApprovalQuery;
import org.dromara.rs.service.IRsOverseasApprovalService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.dromara.common.mybatis.core.page.PageQuery;
/**
* 出国审批记录Service业务层处理
@@ -45,12 +44,23 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService
*/
@Override
public TableDataInfo<RsOverseasApprovalVo> queryPageList(RsOverseasApprovalQuery query) {
log.info("==================== 查询审批列表 ====================");
log.info("原始查询参数: {}", query);
// 🔧 应用数据权限过滤
applyDataPermissionFilter(query);
log.info("权限过滤后查询参数: {}", query);
log.info(" - applicationId: {}", query.getApplicationId());
log.info(" - approverId: {}", query.getApproverId());
log.info(" - applicantId: {}", query.getApplicantId());
log.info(" - deptId: {}", query.getDeptId());
// 使用自定义SQL查询,包含申请信息的联合查询
List<RsOverseasApprovalVo> list = baseMapper.selectOverseasApprovalList(query);
log.info("查询结果数量: {}", list.size());
// 手动分页处理
int pageNum = query.getPageNum();
int pageSize = query.getPageSize();
@@ -65,6 +75,9 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService
page.setRecords(pageList);
page.setTotal(total);
log.info("分页结果: 总数={}, 当前页={}, 每页={}, 返回={}", total, pageNum, pageSize, pageList.size());
log.info("==================== 查询完成 ====================");
return TableDataInfo.build(page);
}
@@ -208,28 +221,56 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService
*/
private void applyDataPermissionFilter(RsOverseasApprovalQuery query) {
Long currentUserId = StpUtil.getLoginIdAsLong();
String currentUsername = LoginHelper.getUsername();
Long currentDeptId = LoginHelper.getDeptId();
boolean isSuperAdmin = LoginHelper.isSuperAdmin();
log.info("权限过滤 - 用户ID: {}, 部门ID: {}", currentUserId, currentDeptId);
log.info("==================== 开始应用数据权限过滤 ====================");
log.info("当前用户: {}, ID: {}, 部门ID: {}, 超级管理员: {}",
currentUsername, currentUserId, currentDeptId, isSuperAdmin);
// 超级管理员不需要过滤
if (isSuperAdmin) {
log.info("→ 权限结果:超级管理员,跳过所有过滤");
log.info("==================== 权限过滤结束 ====================");
return;
}
// 申请人只能看到自己的申请
boolean hasApplicantRole = StpUtil.hasRole("rs-Applicant");
log.info("✓ 角色检查 [rs-Applicant]: {}", hasApplicantRole);
if (hasApplicantRole) {
query.setApplicantId(currentUserId);
log.info("→ 权限结果:申请人角色,设置申请人过滤 applicantId={}", currentUserId);
log.info("==================== 权限过滤结束 ====================");
return;
}
// 🔧 优先检查部门负责人权限(避免被rs-admin角色覆盖)
boolean isDeptApprover = isDeptApprover(currentUserId);
log.info("✓ 部门负责人检查结果: {}", isDeptApprover);
if (isDeptApprover) {
log.info("用户 {} 是部门负责人,应用审批人过滤: {}", currentUserId, currentUserId);
query.setApproverId(currentUserId); // 只看分配给自己的审批
query.setDeptId(currentDeptId); // 同时应用部门过滤
// 部门负责人只能看到本部门的所有申请(不限制审批人)
query.setDeptId(currentDeptId);
log.info("→ 权限结果:部门负责人,设置部门过滤 deptId={}", currentDeptId);
log.info("==================== 权限过滤结束 ====================");
return;
}
// 人事管理员可以看到所有申请(仅当不是部门负责人时)
if (StpUtil.hasRole("rs-admin")) {
log.info("用户 {} 是人事管理员,可查看所有申请", currentUserId);
boolean hasAdminRole = StpUtil.hasRole("rs-admin");
log.info("✓ 角色检查 [rs-admin]: {}", hasAdminRole);
if (hasAdminRole) {
log.info("→ 权限结果:管理员角色,不设置过滤(可查看所有数据)");
log.info("==================== 权限过滤结束 ====================");
return; // 全局权限
}
// 普通用户只能看到自己的申请
log.info("用户 {} 是普通用户,只能查看自己的申请", currentUserId);
query.setApplicantId(currentUserId);
// 其他角色只能看到分配给自己的审批
query.setApproverId(currentUserId);
log.info("→ 权限结果:普通审批人,设置审批人过滤 approverId={}", currentUserId);
log.info("==================== 权限过滤结束 ====================");
}
/**
@@ -403,29 +444,35 @@ public class RsOverseasApprovalServiceImpl implements IRsOverseasApprovalService
private boolean isDeptApprover(Long userId) {
String currentUsername = LoginHelper.getUsername();
log.info("========== 开始检查部门审批人权限 ==========");
log.info("用户名: {}, 用户ID: {}", currentUsername, userId);
// 检查用户是否具有部门负责人审批权限
// 方法1:检查是否有第1级审批权限
boolean hasApprove1 = StpUtil.hasPermission("rs:overseas:approve1");
log.info("用户 {} 权限检查 - rs:overseas:approve1: {}", currentUsername, hasApprove1);
log.info("✓ 权限检查 [rs:overseas:approve1]: {}", hasApprove1);
if (hasApprove1) {
log.info("========== 检查结果:是部门审批人(通过rs:overseas:approve1权限) ==========");
return true;
}
// 方法2:检查是否有部门负责人角色(如果有定义的话)
boolean hasDeptHeadRole = StpUtil.hasRole("dept_head_role");
log.info("用户 {} 角色检查 - dept_head_role: {}", currentUsername, hasDeptHeadRole);
log.info("✓ 角色检查 [dept_head_role]: {}", hasDeptHeadRole);
if (hasDeptHeadRole) {
log.info("========== 检查结果:是部门审批人(通过dept_head_role角色) ==========");
return true;
}
// 方法3:检查是否有旧版权限标识符(兼容性)
boolean hasOldPermission = StpUtil.hasPermission("overseas:application:dept_head:approve");
log.info("用户 {} 权限检查 - overseas:application:dept_head:approve: {}", currentUsername, hasOldPermission);
log.info("✓ 权限检查 [overseas:application:dept_head:approve]: {}", hasOldPermission);
if (hasOldPermission) {
log.info("========== 检查结果:是部门审批人(通过旧版权限) ==========");
return true;
}
log.info("用户 {} 不具有部门负责人权限", currentUsername);
log.info("========== 检查结果:不是部门审批人 ==========");
return false;
}
@@ -227,6 +227,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="tenantId != null and tenantId != ''">
AND oa.tenant_id = #{tenantId}
</if>
<!-- 🔴 新增:部门负责人权限过滤 - 只能查看本部门员工提交的申请 -->
<if test="applicantDeptId != null">
AND EXISTS (
SELECT 1 FROM sys_user u
WHERE u.user_id = oa.applicant_id
AND u.dept_id = #{applicantDeptId}
AND u.del_flag = '0'
)
</if>
</where>
order by oa.apply_time desc, oa.create_time desc
</select>
@@ -280,6 +289,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by oa.apply_time desc, oa.create_time desc
</select>
<select id="selectOverseasApplicationById" parameterType="Long" resultMap="RsOverseasApplicationVoResult">
<include refid="selectOverseasApplicationVo"/>
where oa.application_id = #{applicationId} and oa.deleted = '0'
+157 -7
View File
@@ -24,9 +24,26 @@
**模块名称**:overseas / 出国申请管理
**基础路径**:`/api/rs/overseas`
**版本**:v1.0
**版本**:v1.1
**负责人**:weizzzz
**更新时间**:2025-01-28
**更新时间**:2025-10-28
### 🆕 最近更新
**v1.1 (2025-10-28) - 分级审批权限升级**
- ✅ **审批权限升级为五级审批制度**
- 新增 `rs:overseas:approve1-5` 五个分级审批权限
- 保留 `rs:overseas:approve` 作为通用审批权限
- ✅ **Controller层采用OR模式权限验证**
- 用户拥有任一审批权限即可访问审批接口
- 解决部门负责人403权限错误问题
- ✅ **Service层增加审批级别验证**
- 新增 `validateApproverLevel()` 方法
- 精确验证用户权限是否匹配申请的审批级别
- 防止越级审批
- ✅ **前端按钮权限优化**
- 审批按钮支持多级权限显示
- 改善用户体验,按钮显示逻辑更合理
### 模块功能
@@ -59,13 +76,107 @@
- `rs:overseas:record:create` - 创建记录权限
- `rs:overseas:record:complete` - 完成记录权限
#### 出国审批权限
- **权限前缀**:`rs:overseas:approval:`
- **权限列表**:
#### 出国审批权限(🆕 2025-10-28 更新为五级审批)
- **权限前缀**:`rs:overseas:`
- **审批记录查看权限**:
- `rs:overseas:approval:list` - 查询审批记录列表权限
- `rs:overseas:approval:query` - 查询审批记录详情权限
- `rs:overseas:approval:export` - 导出审批记录权限
- **分级审批权限**(五级审批制度):
- `rs:overseas:approve` - 通用审批权限(超级管理员/高级审批者,可审批任意级别)
- `rs:overseas:approve1` - 第1级:部门负责人审批权限
- `rs:overseas:approve2` - 第2级:人事处审批权限
- `rs:overseas:approve3` - 第3级:纪委审批权限
- `rs:overseas:approve4` - 第4级:学院分管领导审批权限
- `rs:overseas:approve5` - 第5级:学院党委书记审批权限
- **权限使用说明**:
- 审批接口采用 **OR模式**,用户拥有以上任一权限即可调用接口
- 具体审批权限在业务层验证,必须匹配申请的 `approval_level`
- 部门负责人通过 `rs:overseas:approve1` 权限识别,自动应用部门数据过滤
### 🔐 数据权限控制
#### 权限分级说明
出国申请系统采用**基于角色和部门的多级数据权限控制**,确保用户只能访问其权限范围内的数据。
#### 角色权限矩阵
| 角色 | 角色标识 | 数据访问范围 | 可见申请范围 | 备注 |
|------|----------|------------|-------------|------|
| **超级管理员** | `admin` | 全局数据 | 所有申请 | 不受任何限制 |
| **人事管理员** | `rs-admin` | 全局数据 | 所有申请 | 仅当不是部门负责人时 |
| **申请人** | `rs-Applicant` | 个人数据 | 仅自己的申请 | 只能查看和管理自己的申请 |
| **部门负责人** | 具有`rs:overseas:approve1`权限 | 本部门数据 | 本部门所有申请 | **优先级最高** |
| **普通审批人** | 其他审批权限 | 分配数据 | 分配给自己的审批 | 只能看到分配的审批任务 |
#### 部门权限过滤规则(🆕 2025-01-28)
**核心原则**:部门负责人**只能看到本部门成员提交的申请**,不能看到其他部门的申请。
**实现逻辑**:
1. **判断用户是否为部门负责人**(三种方式任选其一):
- 具有 `rs:overseas:approve1` 权限
- 具有 `dept_head_role` 角色
- 具有 `overseas:application:dept_head:approve` 权限(兼容旧版)
2. **应用部门过滤**:
- 查询条件:`申请人的dept_id = 当前用户的dept_id`
- SQL实现:通过 `sys_user` 表关联,筛选申请人所属部门
- **重要**:不限制审批人(approverId),只限制申请人部门(deptId)
3. **权限优先级**(从高到低):
```
超级管理员 > 申请人(自己的数据)> 部门负责人(本部门)> 人事管理员(全局)> 普通审批人(分配数据)
```
**示例场景**:
假设有以下用户和部门:
- **张三**:`dept_id = 001`(测试部门A),角色:申请人
- **李四**:`dept_id = 001`(测试部门A),角色:部门负责人
- **王五**:`dept_id = 002`(测试部门B),角色:部门负责人
- **赵六**:`dept_id = 003`(测试部门C),角色:部门负责人
**张三提交申请后:**
- ✅ **李四**(同部门):可以看到张三的申请
- ❌ **王五**(不同部门):不能看到张三的申请
- ❌ **赵六**(不同部门):不能看到张三的申请
**技术实现**:
```java
// Service层权限过滤逻辑
private void applyDeptPermissionFilter(RsOverseasApprovalQuery query) {
// 1. 判断是否为部门负责人
boolean isDeptApprover = isDeptApprover(currentUserId);
// 2. 如果是部门负责人,只设置部门过滤
if (isDeptApprover) {
query.setDeptId(currentDeptId); // 只应用部门过滤
return; // 不设置approverId,避免过度限制
}
}
```
```xml
<!-- Mapper XML中的部门权限过滤SQL -->
<if test="deptId != null">
AND EXISTS (
SELECT 1 FROM sys_user u
WHERE u.user_id = app.applicant_id
AND u.dept_id = #{deptId}
)
</if>
```
**相关文档**:
- 测试指南:`docs/测试文档/出国申请部门权限过滤测试指南.md`
- 验证脚本:`部门权限过滤验证脚本.sql`
- 代码实现:`RsOverseasApprovalServiceImpl.java`
---
## 📊 接口概览
@@ -152,6 +263,7 @@ interface OverseasApplicationVO {
accommodationPlan: string; // 住宿安排
transportationPlan: string; // 交通安排
status: string; // 申请状态编码
approvalStatus: string; // 工作流状态编码
currentApprover: string; // 当前审批人
approvalLevel: string; // 审批级别
applyTime: string; // 申请时间
@@ -162,6 +274,7 @@ interface OverseasApplicationVO {
updateTime: string; // 更新时间
// 显示字段
statusText: string; // 状态文本
approvalStatusText: string; // 工作流状态文本
purposeText: string; // 目的文本
budgetSourceText: string; // 经费来源文本
approvalLevelText: string; // 审批级别文本
@@ -403,7 +516,27 @@ interface OverseasApplicationForm {
### 4. 审批出国申请
**接口地址**:`POST /rs/overseas/application/approve`
**权限要求**:`rs:overseas:application:approve`
**权限要求**:`rs:overseas:approve` (通用) 或 `rs:overseas:approve1-5` (分级) - **支持多级审批权限(2025-10-28更新)**
#### 🔴 权限说明(重要更新)
**分级审批权限体系**(OR模式,满足任一权限即可访问接口):
- `rs:overseas:approve` - 通用审批权限(超级管理员/高级审批者)
- `rs:overseas:approve1` - 第1级:部门负责人审批权限
- `rs:overseas:approve2` - 第2级:人事处审批权限
- `rs:overseas:approve3` - 第3级:纪委审批权限
- `rs:overseas:approve4` - 第4级:学院分管领导审批权限
- `rs:overseas:approve5` - 第5级:学院党委书记审批权限
**权限验证机制**:
1. **接口层验证**:Controller使用 `@SaCheckPermission(value={...}, mode=SaMode.OR)`,满足任一权限即可调用接口
2. **业务层验证**:Service层根据申请的 `approval_level` 字段验证用户是否拥有对应级别的审批权限
3. **权限不匹配处理**:如果用户审批级别与申请要求不符,返回业务异常"您没有第X级审批权限"
**示例场景**:
- 部门负责人(拥有 `approve1`)只能审批 `approval_level=1` 的申请
- 尝试审批 `approval_level=2` 的申请将被拒绝
- 拥有 `rs:overseas:approve` 通用权限的用户可以审批任意级别
#### 请求参数
@@ -412,6 +545,7 @@ interface ApprovalRequest {
applicationId: number | string; // 申请ID(必须)
approvalStatus: string; // 审批状态:1-通过,2-拒绝(必须)
approvalOpinion?: string; // 审批意见
taskId?: string; // 🔴 工作流任务ID(可选,支持标准工作流模式)
}
```
@@ -600,6 +734,20 @@ referer: http://localhost
| 3 | 已取消 | 申请被申请人主动取消 |
| 4 | 已完成 | 出国活动已完成 |
### 工作流状态 (approvalStatus)
| 编码 | 含义 | 描述 |
| ----------- | ------ | ------------------------ |
| cancel | 已撤销 | 申请人主动撤销申请 |
| draft | 草稿 | 申请尚未提交,处于编辑状态 |
| waiting | 待审核 | 申请已提交,正在审批流程中 |
| finish | 已完成 | 所有审批节点都已通过,流程结束 |
| invalid | 已作废 | 申请因某种原因被标记为无效 |
| back | 已退回 | 审批被拒绝,退回给申请人修改 |
| termination | 已终止 | 流程被管理员或系统强制终止 |
> **📝 说明**:工作流状态由 Warm-Flow 工作流引擎自动管理,用于总体流程监听和状态跟踪。
### 出国目的 (purpose)
| 编码 | 含义 | 描述 |
@@ -630,7 +778,7 @@ referer: http://localhost
| 2 | 分管领导 | 第二级审批:分管领导 |
| 3 | 校领导 | 第三级审批:校领导 |
### 审批状态 (approvalStatus)
### 审批记录状态 (审批记录表中的 approvalStatus)
| 编码 | 含义 | 描述 |
| ---- | ------ | ------------------------ |
@@ -638,6 +786,8 @@ referer: http://localhost
| 1 | 通过 | 审批通过 |
| 2 | 拒绝 | 审批拒绝 |
> **⚠️ 注意**:此状态是审批记录表中的状态,与申请表中的工作流状态(approvalStatus)是不同的字段。
### 完成状态 (completionStatus)
> **⚠️ 重要说明**:完成状态(completionStatus)与申请状态(status)是两个不同的概念!
+45 -5
View File
@@ -152,11 +152,12 @@ export interface OverseasApplicationForm {
approvalStatus?: string; // 工作流审批状态
}
// 审批请求类型
// 审批请求类型(支持标准工作流模式)
export interface ApprovalRequest {
applicationId: number | string;
approvalStatus: string;
approvalOpinion?: string;
taskId?: string; // 🔴 新增:工作流任务ID,支持标准工作流模式
}
// 出国记录查询参数类型
@@ -327,15 +328,30 @@ export function listMyOverseasApplication(query: OverseasApplicationQuery) {
});
}
// 查询待审批的申请列表
export function listPendingOverseasApplication(query: OverseasApplicationQuery) {
// 查询当前用户的待审批申请列表(五级审批模式)
export function listPendingOverseasApplication() {
return request({
url: '/rs/overseas/application/pending/list',
method: 'get',
params: query
method: 'get'
});
}
// ========================================
// 🚨 标准化改造:删除自定义待审批查询API
//
// 原因:出国申请工作流应使用标准工作流模式
// 用户应通过工作流API pageByTaskWait() 获取待办任务
// 而不是调用自定义的业务API
//
// 删除的API:listPendingOverseasApplication()
//
// 标准工作流模式:
// 1. 导入工作流API:import { pageByTaskWait } from '@/api/workflow/task'
// 2. 调用标准API:pageByTaskWait(queryParams)
// 3. 获得待办任务列表,包含businessId
// 4. 点击办理跳转:/rs/overseas/application/edit?taskId=xxx&applicationId=businessId
// ========================================
// 根据状态查询申请列表
export function listOverseasApplicationByStatus(status: string) {
return request({
@@ -990,3 +1006,27 @@ export function getItineraryChangeHistory(recordId: string | number) {
method: 'get'
});
}
// ==================== 动态审批人获取API ====================
/**
* 根据申请人ID获取部门审批人ID
* @param applicantId 申请人ID
*/
export function getDepartmentApproverId(applicantId: string | number): Promise<{ data: number }> {
return request({
url: `/rs/overseas/application/getDepartmentApproverId/${applicantId}`,
method: 'get'
});
}
/**
* 根据申请人ID获取人事处审批人列表
* @param applicantId 申请人ID
*/
export function getHrApprovers(applicantId: string | number): Promise<{ data: number[] }> {
return request({
url: `/rs/overseas/application/getHrApprovers/${applicantId}`,
method: 'get'
});
}
+185 -553
View File
@@ -1,54 +1,55 @@
<template>
<div class="app-container">
<el-card class="box-card">
<!-- 审批管理选项卡 -->
<el-tabs v-model="activeTab" type="border-card" @tab-click="handleTabClick">
<!-- 出国审批 -->
<el-tab-pane label="出国审批" name="pending">
<div class="tab-content">
<!-- 搜索区域 -->
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showPendingSearch" class="mb-[10px]">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="pendingQueryForm" :model="pendingQueryParams" label-width="100px">
<el-form ref="queryForm" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="5">
<el-form-item label="申请人" prop="applicantName">
<el-input
v-model="pendingQueryParams.applicantName"
v-model="queryParams.applicantName"
placeholder="请输入申请人姓名"
clearable
@keyup.enter="handlePendingQuery"
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="所属部门" prop="department">
<el-input
v-model="pendingQueryParams.department"
placeholder="请输入所属部门"
clearable
@keyup.enter="handlePendingQuery"
/>
<el-input v-model="queryParams.department" placeholder="请输入所属部门" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="目的地" prop="destination">
<el-input
v-model="pendingQueryParams.destination"
v-model="queryParams.destination"
placeholder="请输入目的地国家"
clearable
@keyup.enter="handlePendingQuery"
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="申请状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择申请状态" clearable style="width: 100%">
<el-option label="待审核" value="0" />
<el-option label="已通过" value="1" />
<el-option label="已拒绝" value="2" />
<el-option label="已取消" value="3" />
<el-option label="已完成" value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handlePendingQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetPendingQuery">重置</el-button>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
@@ -61,179 +62,79 @@
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button icon="Download" @click="handleExportPending" v-hasPermi="['rs:overseas:export']" style="display: none">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showPendingSearch" @queryTable="getPendingList"></right-toolbar>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 待审批申请表格 -->
<el-table v-loading="pendingLoading" border :data="pendingList" @row-dblclick="handleViewApplication" v-hasPermi="['rs:overseas:list']">
<el-table-column label="姓名" align="center" prop="applicantName" min-width="100" :show-overflow-tooltip="true" />
<el-table-column label="现工作部门" align="center" prop="department" min-width="140" :show-overflow-tooltip="true" />
<el-table-column label="职称" align="center" prop="professionalTitle" min-width="110" :show-overflow-tooltip="true" />
<el-table-column label="前往国家" align="center" prop="destination" min-width="110" :show-overflow-tooltip="true" />
<el-table-column label="出境事由" align="center" prop="exitReason" min-width="100" :show-overflow-tooltip="true" />
<el-table-column label="出国天数" align="center" prop="duration" min-width="90">
<template #default="scope">{{ scope.row.duration || '-' }}天</template>
</el-table-column>
<el-table-column label="申请状态" align="center" prop="statusText" min-width="100">
<!-- 数据表格 -->
<div class="table-container">
<el-table
v-loading="loading"
:data="applicationList"
@row-dblclick="handleView"
stripe
border
style="width: 100%"
height="auto"
>
<el-table-column label="申请人" align="center" prop="applicantName" min-width="100" show-overflow-tooltip />
<el-table-column label="部门" align="center" prop="department" min-width="140" show-overflow-tooltip />
<el-table-column label="目的地" align="center" prop="destination" min-width="120" show-overflow-tooltip />
<el-table-column label="出国事由" align="center" prop="purpose" min-width="140" show-overflow-tooltip />
<el-table-column label="申请时间" align="center" prop="applyTime" min-width="170">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">{{ scope.row.statusText }}</el-tag>
<span>{{ scope.row.applyTime ? new Date(scope.row.applyTime).toLocaleString() : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="applyTime" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="操作" align="center" width="180">
<el-table-column label="申请状态" align="center" min-width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看详情" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleViewApplication(scope.row)"
size="small"
@click="handleView(scope.row)"
v-hasPermi="['rs:overseas:query']"
></el-button>
/>
</el-tooltip>
<el-tooltip content="查看审批历史" placement="top" v-if="scope.row.status !== '0'">
<el-button
link
type="info"
icon="List"
@click.stop="handleViewApprovalHistory(scope.row)"
v-hasPermi="['rs:overseas:query']"
></el-button>
</el-tooltip>
<el-tooltip content="通过" placement="top" v-if="scope.row.status === '0'">
<el-tooltip content="审批" placement="top" v-if="scope.row.status === '0'">
<el-button
link
type="success"
icon="Check"
@click="handleApprovalPass(scope.row)"
v-hasPermi="['rs:overseas:approve']"
></el-button>
</el-tooltip>
<el-tooltip content="拒绝" placement="top" v-if="scope.row.status === '0'">
<el-button
link
type="danger"
icon="Close"
@click="handleApprovalReject(scope.row)"
v-hasPermi="['rs:overseas:approve']"
></el-button>
size="small"
@click="handleApproval(scope.row)"
v-hasPermi="[
'rs:overseas:approve',
'rs:overseas:approve1',
'rs:overseas:approve2',
'rs:overseas:approve3',
'rs:overseas:approve4',
'rs:overseas:approve5'
]"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-show="pendingTotal > 0"
:total="pendingTotal"
v-model:page="pendingQueryParams.pageNum"
v-model:limit="pendingQueryParams.pageSize"
@pagination="getPendingList"
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
</el-tab-pane>
<!-- 审批记录 -->
<el-tab-pane label="审批记录" name="record">
<div class="tab-content">
<!-- 搜索区域 -->
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showRecordSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="recordQueryForm" :model="recordQueryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="5">
<el-form-item label="申请人" prop="applicantName">
<el-input
v-model="recordQueryParams.applicantName"
placeholder="请输入申请人姓名"
clearable
@keyup.enter="handleRecordQuery"
/>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="所属部门" prop="department">
<el-input v-model="recordQueryParams.department" placeholder="请输入所属部门" clearable @keyup.enter="handleRecordQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="目的地" prop="destination">
<el-input
v-model="recordQueryParams.destination"
placeholder="请输入目的地国家"
clearable
@keyup.enter="handleRecordQuery"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleRecordQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetRecordQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 操作按钮区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button icon="Download" @click="handleExportRecord" v-hasPermi="['rs:overseas:export']" style="display: none">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showRecordSearch" @queryTable="getRecordList"></right-toolbar>
</el-row>
</template>
<!-- 审批记录表格 -->
<el-table v-loading="recordLoading" border :data="recordList" @row-dblclick="handleViewRecord" v-hasPermi="['rs:overseas:list']">
<el-table-column label="申请人" align="center" prop="applicantName" width="100" :show-overflow-tooltip="true" />
<el-table-column label="出国目的" align="center" prop="purposeText" width="100" :show-overflow-tooltip="true" />
<el-table-column label="目的地" align="center" prop="destination" width="120" :show-overflow-tooltip="true" />
<el-table-column label="所属部门" align="center" prop="department" width="140" :show-overflow-tooltip="true" />
<el-table-column label="出国时间" align="center" min-width="220" :show-overflow-tooltip="true">
<template #default="scope"> {{ formatDate(scope.row.startDate) }} 至 {{ formatDate(scope.row.endDate) }} </template>
</el-table-column>
<el-table-column label="天数" align="center" prop="duration" width="60" />
<el-table-column label="审批时间" align="center" prop="approvalTime" width="160" :show-overflow-tooltip="true" />
<el-table-column label="审批状态" align="center" width="100">
<template #default="scope">
<el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)">{{ scope.row.approvalStatusText }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<el-button link type="primary" icon="View" @click="handleViewRecord(scope.row)" v-hasPermi="['rs:overseas:query']"></el-button>
</el-tooltip>
<!-- 查看审批历史按钮已隐藏 -->
</template>
</el-table-column>
</el-table>
<pagination
v-show="recordTotal > 0"
:total="recordTotal"
v-model:page="recordQueryParams.pageNum"
v-model:limit="recordQueryParams.pageSize"
@pagination="getRecordList"
/>
</el-card>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 申请详情对话框 -->
@@ -408,21 +309,6 @@
/>
</div>
<!-- 查看审批记录 -->
<div class="mt-4">
<h4>审批记录</h4>
<el-table :data="applicationApprovalList" border>
<el-table-column label="审批人" align="center" prop="approverName" />
<el-table-column label="审批级别" align="center" prop="approvalLevelText" />
<el-table-column label="审批结果" align="center" prop="approvalStatusText">
<template #default="scope">
<el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)">{{ scope.row.approvalStatusText }}</el-tag>
</template>
</el-table-column>
<el-table-column label="审批意见" align="center" prop="approvalOpinion" show-overflow-tooltip />
<el-table-column label="审批时间" align="center" prop="approvalTime" />
</el-table>
</div>
<template #footer v-if="viewData && viewData.status === '0'">
<div class="dialog-footer">
@@ -454,87 +340,21 @@
</template>
</el-dialog>
<!-- 审批历史对话框 -->
<el-dialog title="审批历史" v-model="approvalHistoryDialog.visible" width="900px" top="5vh" :close-on-click-modal="false">
<!-- 申请基本信息 -->
<div style="margin-bottom: 20px; padding: 15px; background-color: #f8f9fa; border-radius: 4px">
<h4 style="margin: 0 0 10px 0; color: #303133">申请信息</h4>
<el-row :gutter="20">
<el-col :span="8">
<span style="color: #909399">申请人:</span>
<span style="font-weight: 500">{{ currentApplication.applicantName }}</span>
</el-col>
<el-col :span="8">
<span style="color: #909399">目的地:</span>
<span style="font-weight: 500">{{ currentApplication.destination }}</span>
</el-col>
<el-col :span="8">
<span style="color: #909399">申请时间:</span>
<span style="font-weight: 500">{{ parseTime(currentApplication.applyTime) }}</span>
</el-col>
</el-row>
</div>
<!-- 审批历史列表 -->
<el-table v-loading="approvalHistoryLoading" :data="approvalHistoryList" border style="width: 100%">
<el-table-column label="审批级别" align="center" prop="approvalLevel" width="100">
<template #default="scope">
<span v-if="scope.row.approvalLevel === '1'">部门主管</span>
<span v-else-if="scope.row.approvalLevel === '2'">分管领导</span>
<span v-else-if="scope.row.approvalLevel === '3'">校领导</span>
<span v-else>{{ scope.row.approvalLevel }}</span>
</template>
</el-table-column>
<el-table-column label="审批人" align="center" prop="approverName" width="120" />
<el-table-column label="审批时间" align="center" prop="approvalTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.approvalTime) }}</span>
</template>
</el-table-column>
<el-table-column label="审批结果" align="center" prop="approvalStatus" width="100">
<template #default="scope">
<el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)">
<span v-if="scope.row.approvalStatus === '0'">待审核</span>
<span v-else-if="scope.row.approvalStatus === '1'">通过</span>
<span v-else-if="scope.row.approvalStatus === '2'">拒绝</span>
<span v-else>{{ scope.row.approvalStatus }}</span>
</el-tag>
</template>
</el-table-column>
<el-table-column label="审批意见" align="center" prop="approvalOpinion" min-width="200" :show-overflow-tooltip="true" />
</el-table>
<!-- 无数据提示 -->
<el-empty v-if="!approvalHistoryLoading && approvalHistoryList.length === 0" description="暂无审批历史记录" />
<template #footer>
<div class="dialog-footer">
<el-button @click="approvalHistoryDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="OverseasApproval">
import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
import { useRouter } from 'vue-router';
import {
listPendingOverseasApplication,
listOverseasApproval,
listOverseasApplication,
getOverseasApplication,
getOverseasApprovalByApplication,
getOverseasApprovalHistory,
approveOverseasApplication,
exportOverseasApplication,
exportOverseasApproval,
listOverseasAttachments,
type OverseasApplicationQuery,
type OverseasApplicationVO,
type OverseasApprovalQuery,
type OverseasApprovalVO,
type ApprovalRequest,
type OverseasAttachmentVO,
fixOverseasApprovalData
type OverseasAttachmentVO
} from '@/api/rs/overseas';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Document, Warning, Paperclip, View, Download } from '@element-plus/icons-vue';
@@ -544,50 +364,32 @@ import { getToken } from '@/utils/auth';
// 响应式数据
const { proxy } = getCurrentInstance() as any;
const router = useRouter();
// 附件列表数据
const viewAttachmentList = ref<OverseasAttachmentVO[]>([]);
const state = reactive({
// 选项卡相关
activeTab: 'pending',
// 待审批列表相关
pendingLoading: false,
pendingList: [] as OverseasApplicationVO[],
pendingTotal: 0,
showPendingSearch: true,
pendingQueryParams: {
// 列表相关
loading: false,
applicationList: [] as OverseasApplicationVO[],
total: 0,
showSearch: true,
queryParams: {
pageNum: 1,
pageSize: 10,
applicantName: '',
department: '',
destination: '',
status: '0' // 只查询待审核状态
status: ''
} as OverseasApplicationQuery,
// 审批记录列表相关
recordLoading: false,
recordList: [] as OverseasApprovalVO[],
recordTotal: 0,
showRecordSearch: true,
recordQueryParams: {
pageNum: 1,
pageSize: 10,
applicationId: undefined,
approvalLevel: '',
approvalStatus: '',
applicantName: '',
department: '',
destination: ''
} as OverseasApprovalQuery,
// 查看详情
viewDialog: {
visible: false
},
viewData: null as OverseasApplicationVO | null,
applicationApprovalList: [] as OverseasApprovalVO[],
// 审批表单
approvalDialog: {
@@ -601,221 +403,97 @@ const state = reactive({
} as ApprovalRequest,
approvalRules: {
approvalOpinion: [{ required: true, message: '请输入审批意见', trigger: 'blur' }]
},
// 审批历史
approvalHistoryDialog: {
visible: false
},
approvalHistoryLoading: false,
approvalHistoryList: [] as OverseasApprovalVO[],
currentApplication: {
applicationId: 0,
applicantName: '',
destination: '',
applyTime: '',
status: '0'
},
// 字典数据
approvalLevelOptions: [
{ label: '部门主管', value: '1' },
{ label: '分管领导', value: '2' },
{ label: '校领导', value: '3' }
],
approvalStatusOptions: [
{ label: '待审核', value: '0' },
{ label: '通过', value: '1' },
{ label: '拒绝', value: '2' }
]
}
});
const {
activeTab,
pendingLoading,
pendingList,
pendingTotal,
showPendingSearch,
pendingQueryParams,
recordLoading,
recordList,
recordTotal,
showRecordSearch,
recordQueryParams,
loading,
applicationList,
total,
showSearch,
queryParams,
viewDialog,
viewData,
applicationApprovalList,
approvalDialog,
approvalForm,
approvalRules,
approvalHistoryDialog,
approvalHistoryLoading,
approvalHistoryList,
currentApplication,
approvalLevelOptions,
approvalStatusOptions
approvalRules
} = toRefs(state);
// 页面加载时
onMounted(() => {
getPendingList();
getRecordList(); // 同时加载审批记录数据
getList();
});
// 标签页切换
const handleTabClick = (tab: any) => {
if (tab.name === 'pending') {
getPendingList();
} else if (tab.name === 'record') {
getRecordList();
}
};
// 获取待审批列表
const getPendingList = async () => {
state.pendingLoading = true;
// 获取申请列表
const getList = async () => {
state.loading = true;
try {
const response = await listPendingOverseasApplication(state.pendingQueryParams);
state.pendingList = response.rows;
state.pendingTotal = response.total;
const response = await listOverseasApplication(state.queryParams);
state.applicationList = response.rows || [];
state.total = response.total || 0;
} catch (error: any) {
console.error('获取待审批列表失败:', error);
proxy.$modal.msgError('获取待审批列表失败: ' + (error?.message || String(error)));
console.error('获取申请列表失败:', error);
ElMessage.error('获取申请列表失败');
state.applicationList = [];
state.total = 0;
} finally {
state.pendingLoading = false;
}
};
// 获取审批记录列表
const getRecordList = async () => {
console.log('🔍 开始获取审批记录列表...');
console.log('查询参数:', state.recordQueryParams);
state.recordLoading = true;
try {
const response = await listOverseasApproval(state.recordQueryParams);
console.log('📡 API响应:', response);
console.log('📊 响应数据结构-weizhong:', {
code: response.code,
msg: response.msg,
rows: response.rows,
total: response.total,
rowsLength: response.rows ? response.rows.length : 0
});
state.recordList = response.rows || [];
state.recordTotal = response.total || 0;
console.log('💾 设置后的状态:', {
recordList: state.recordList,
recordTotal: state.recordTotal,
listLength: state.recordList.length
});
if (!response.rows || response.rows.length === 0) {
console.warn('⚠️ 审批记录列表为空');
} else {
console.log('✅ 成功获取到', response.rows.length, '条审批记录');
console.log('第一条记录示例:', response.rows[0]);
}
} catch (error: any) {
console.error('❌ 获取审批记录列表失败:', error);
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
});
proxy.$modal.msgError('获取审批记录列表失败: ' + (error?.message || String(error)));
} finally {
state.recordLoading = false;
console.log('🏁 审批记录列表加载完成');
state.loading = false;
}
};
// 搜索查询
const handlePendingQuery = () => {
state.pendingQueryParams.pageNum = 1;
getPendingList();
};
const handleRecordQuery = () => {
state.recordQueryParams.pageNum = 1;
getRecordList();
const handleQuery = () => {
state.queryParams.pageNum = 1;
getList();
};
// 重置搜索
const resetPendingQuery = () => {
const resetQuery = () => {
try {
// 重置查询参数
Object.assign(state.pendingQueryParams, {
Object.assign(state.queryParams, {
pageNum: 1,
pageSize: 10,
applicantName: '',
department: '',
destination: '',
status: '0' // 保持查询待审核状态
status: ''
});
// 重置表单字段
const form = proxy.$refs.pendingQueryForm;
const form = proxy.$refs.queryForm;
if (form && typeof form.resetFields === 'function') {
form.resetFields();
}
handlePendingQuery();
handleQuery();
} catch (error) {
console.error('重置查询条件时发生错误:', error);
handlePendingQuery();
}
};
const resetRecordQuery = () => {
try {
// 重置查询参数
Object.assign(state.recordQueryParams, {
pageNum: 1,
pageSize: 10,
applicationId: undefined,
approvalLevel: '',
approvalStatus: '',
applicantName: '',
department: '',
destination: ''
});
// 重置表单字段
const form = proxy.$refs.recordQueryForm;
if (form && typeof form.resetFields === 'function') {
form.resetFields();
}
handleRecordQuery();
} catch (error) {
console.error('重置查询条件时发生错误:', error);
handleRecordQuery();
handleQuery();
}
};
// 查看申请详情
const handleViewApplication = async (row: OverseasApplicationVO) => {
console.log('📄 [查看详情] 开始查看申请详情,申请ID:', row.applicationId);
const handleView = async (row: OverseasApplicationVO) => {
try {
const response = await getOverseasApplication(row.applicationId);
state.viewData = response.data;
console.log('✅ [查看详情] 申请详情获取成功:', response.data);
// 获取审批记录
const approvalResponse = await getOverseasApprovalByApplication(row.applicationId);
state.applicationApprovalList = approvalResponse.data;
// 加载附件列表
console.log('🔄 [查看详情] 开始加载附件列表...');
await loadViewAttachmentList(row.applicationId);
console.log('✅ [查看详情] 附件列表加载完成');
state.viewDialog.visible = true;
} catch (error) {
console.error('❌ [查看详情] 获取申请详情失败:', error);
console.error('获取申请详情失败:', error);
ElMessage.error('获取申请详情失败');
}
};
// 审批操作
const handleApproval = (row: OverseasApplicationVO) => {
state.approvalForm.applicationId = row.applicationId;
state.approvalForm.approvalStatus = '1';
state.approvalForm.approvalOpinion = '';
state.approvalDialog.title = '审批申请';
state.approvalDialog.visible = true;
};
// 审批通过
const handleApprovalPass = (row: OverseasApplicationVO) => {
state.approvalForm.applicationId = row.applicationId;
@@ -843,7 +521,7 @@ const submitApproval = () => {
ElMessage.success('审批成功');
state.approvalDialog.visible = false;
state.viewDialog.visible = false;
getPendingList();
getList();
} catch (error) {
console.error('审批失败:', error);
}
@@ -861,77 +539,12 @@ const cancelApproval = () => {
};
};
// 导出
const handleExportPending = async () => {
try {
await exportOverseasApplication(state.pendingQueryParams);
ElMessage.success('导出成功');
} catch (error) {
console.error('导出失败:', error);
}
};
const handleExportRecord = async () => {
try {
await exportOverseasApproval(state.recordQueryParams);
ElMessage.success('导出成功');
} catch (error) {
console.error('导出失败:', error);
}
};
// 查看审批记录详情
const handleViewRecord = async (row: OverseasApprovalVO) => {
console.log('📄 [查看记录详情] 开始查看审批记录详情,申请ID:', row.applicationId);
try {
// 通过申请ID获取申请详情
const response = await getOverseasApplication(row.applicationId);
state.viewData = response.data;
console.log('✅ [查看记录详情] 申请详情获取成功');
// 获取完整的审批记录历史
const approvalResponse = await getOverseasApprovalByApplication(row.applicationId);
state.applicationApprovalList = approvalResponse.data;
// 加载附件列表
console.log('🔄 [查看记录详情] 开始加载附件列表...');
await loadViewAttachmentList(row.applicationId);
console.log('✅ [查看记录详情] 附件列表加载完成');
// 显示查看对话框
state.viewDialog.visible = true;
} catch (error: any) {
console.error('❌ [查看记录详情] 获取审批记录详情失败:', error);
proxy.$modal.msgError('获取详情失败: ' + (error?.message || String(error)));
}
};
// 修复审批记录数据
const handleFixData = async () => {
try {
proxy.$modal.confirm('确定要修复审批记录数据吗?这将重置所有测试数据。').then(async () => {
const response = await fixOverseasApprovalData();
proxy.$modal.msgSuccess(response.msg || '数据修复成功!');
// 重新加载列表
getRecordList();
});
} catch (error: any) {
console.error('修复数据失败:', error);
proxy.$modal.msgError('修复数据失败: ' + (error?.message || String(error)));
}
};
// 工具方法
const formatDate = (date: string) => {
if (!date) return '';
return new Date(date).toLocaleDateString();
};
const formatMoney = (amount: number) => {
if (!amount) return '0.00';
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
const getStatusTagType = (status: string): 'success' | 'warning' | 'danger' | 'info' | 'primary' => {
const typeMap: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
'0': 'warning', // 待审核
@@ -943,55 +556,15 @@ const getStatusTagType = (status: string): 'success' | 'warning' | 'danger' | 'i
return typeMap[status] || 'info';
};
// 查看审批历史
const handleViewApprovalHistory = async (row: any) => {
approvalHistoryLoading.value = true;
approvalHistoryDialog.value.visible = true;
// 确保有applicationId
const applicationId = row.applicationId || 0;
if (!applicationId) {
ElMessage.warning('无法获取申请ID,请重试');
approvalHistoryLoading.value = false;
return;
}
// 设置当前应用信息
Object.assign(currentApplication.value, {
applicationId: applicationId,
applicantName: row.applicantName || '未知用户',
destination: row.destination || '未知目的地',
applyTime: row.applyTime || '',
status: row.status || '0'
});
try {
// 获取审批历史记录
const historyResponse = await getOverseasApprovalHistory(applicationId);
if (historyResponse.code === 200 && historyResponse.data) {
approvalHistoryList.value = historyResponse.data || [];
} else {
console.warn('审批历史API响应不成功:', historyResponse);
approvalHistoryList.value = [];
ElMessage.warning('获取审批历史记录未返回有效数据');
}
} catch (error) {
console.error('获取审批历史记录失败:', error);
ElMessage.error('获取审批历史记录失败');
approvalHistoryList.value = [];
} finally {
approvalHistoryLoading.value = false;
}
const getStatusText = (status: string): string => {
const statusMap: Record<string, string> = {
'0': '待审核',
'1': '已通过',
'2': '已拒绝',
'3': '已取消',
'4': '已完成'
};
const getApprovalStatusTagType = (status: string): 'success' | 'warning' | 'danger' | 'info' | 'primary' => {
const typeMap: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
'0': 'warning', // 待审核
'1': 'success', // 通过
'2': 'danger' // 拒绝
};
return typeMap[status] || 'info';
return statusMap[status] || '未知状态';
};
// ============== 附件相关方法 ==============
@@ -1230,4 +803,63 @@ const handlePreviewStampedFile = async (data: OverseasApplicationVO) => {
.mt-4 {
margin-top: 1rem;
}
/* ========================================
表格布局优化样式
======================================== */
.table-container {
overflow-x: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.action-buttons .el-button {
margin: 0 !important;
padding: 4px 8px;
}
/* 表格响应式优化 */
:deep(.el-table) {
font-size: 14px;
}
:deep(.el-table .cell) {
padding: 0 8px;
word-break: break-all;
}
:deep(.el-table th) {
background-color: #fafafa;
font-weight: 600;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background-color: #fafafa;
}
/* 状态标签样式优化 */
:deep(.el-tag) {
font-weight: 500;
border-radius: 4px;
}
/* 响应式布局 */
@media (max-width: 768px) {
.action-buttons {
gap: 4px;
}
.action-buttons .el-button {
padding: 2px 4px;
font-size: 12px;
}
}
</style>
+80 -47
View File
@@ -78,14 +78,7 @@
<el-tag :type="getStatusTagType(scope.row.status)">{{ scope.row.statusText }}</el-tag>
</template>
</el-table-column>
<el-table-column label="工作流状态" align="center" prop="approvalStatusText" min-width="110">
<template #default="scope">
<el-tag v-if="scope.row.approvalStatus" :type="getApprovalStatusTagType(scope.row.approvalStatus)">
{{ scope.row.approvalStatusText || '-' }}
</el-tag>
<el-tag v-else type="info">未启动</el-tag>
</template>
</el-table-column>
<!-- 移除工作流状态列 - 避免与申请状态重复,简化用户界面 -->
<el-table-column label="申请时间" align="center" prop="applyTime" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="操作" align="center" width="240">
<template #default="scope">
@@ -469,7 +462,9 @@
destroy-on-close
>
<template #default>
<!-- 工作流按钮组件 -->
<!-- 工作流按钮组件 - 在弹窗模式下隐藏,使用footer按钮 -->
<!-- 注释掉,因为弹窗模式下使用footer中的按钮 -->
<!--
<approvalButton
v-if="state.pageType"
@submitForm="submitForm"
@@ -479,6 +474,7 @@
:status="state.applicationForm.approvalStatus"
:pageType="state.pageType"
/>
-->
<div style="max-height: 65vh; overflow-y: auto; padding-right: 10px;">
<el-form ref="applicationFormRef" :model="applicationForm" :rules="applicationRules" label-width="120px">
@@ -572,8 +568,8 @@
<!-- 第五排:前往国家、出境事由 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="前往国家" prop="destinationCountry">
<el-input v-model="applicationForm.destinationCountry" placeholder="请输入前往国家" />
<el-form-item label="前往国家或地区" prop="destinationCountry">
<el-input v-model="applicationForm.destinationCountry" placeholder="请输入前往国家或地区" />
</el-form-item>
</el-col>
<el-col :span="16">
@@ -589,7 +585,7 @@
<!-- 探亲时显示的额外字段 -->
<el-row :gutter="20" v-if="applicationForm.exitReason === '探亲'">
<el-col :span="24">
<el-form-item label="探望对象关系" prop="visitRelationship">
<el-form-item label="探望对象姓名及与申请者关系" prop="visitRelationship">
<el-input
v-model="applicationForm.visitRelationship"
placeholder="请输入探望对象姓名及与申请者关系"
@@ -711,10 +707,10 @@
style="width: 60%;"
/>
</el-form-item>
<el-form-item label="应急联系人及电话" prop="emergencyContact" label-width="160px" style="margin-left: 10%;">
<el-form-item label="应急联系人及联系电话" prop="emergencyContact" label-width="160px" style="margin-left: 10%;">
<el-input
v-model="applicationForm.emergencyContact"
placeholder="请输入应急联系人及电话"
placeholder="请输入应急联系人及联系电话"
style="width: 90%;"
/>
</el-form-item>
@@ -1233,9 +1229,11 @@
<el-table v-loading="approvalHistoryLoading" :data="approvalHistoryList" border style="width: 100%">
<el-table-column label="审批级别" align="center" prop="approvalLevel" width="100">
<template #default="scope">
<span v-if="scope.row.approvalLevel === '1'">部门主管</span>
<span v-else-if="scope.row.approvalLevel === '2'">分管领导</span>
<span v-else-if="scope.row.approvalLevel === '3'">校领导</span>
<span v-if="scope.row.approvalLevel === '1'">部门负责人</span>
<span v-else-if="scope.row.approvalLevel === '2'">人事处</span>
<span v-else-if="scope.row.approvalLevel === '3'">纪委</span>
<span v-else-if="scope.row.approvalLevel === '4'">学院分管领导</span>
<span v-else-if="scope.row.approvalLevel === '5'">学院党委书记</span>
<span v-else>{{ scope.row.approvalLevel }}</span>
</template>
</el-table-column>
@@ -1279,7 +1277,7 @@
<approvalRecord
ref="approvalRecordRef"
:businessId="state.applicationForm.applicationId"
flowCode="overseas-application"
flowCode="dyjs-cgsq"
/>
</div>
</template>
@@ -1307,6 +1305,7 @@ import {
validateApplicationPrerequisites,
listOverseasAttachments,
uploadOverseasAttachment,
getDepartmentApproverId,
deleteOverseasAttachment,
getItineraryChangeHistory,
type OverseasApplicationQuery,
@@ -1413,7 +1412,7 @@ const state = reactive({
department: [{ required: true, message: '现工作部门不能为空', trigger: 'blur' }],
professionalTitle: [{ required: true, message: '职称不能为空', trigger: 'blur' }],
position: [{ required: true, message: '职务不能为空', trigger: 'blur' }],
destinationCountry: [{ required: true, message: '前往国家不能为空', trigger: 'blur' }],
destinationCountry: [{ required: true, message: '前往国家或地区不能为空', trigger: 'blur' }],
exitReason: [{ required: true, message: '出境事由不能为空', trigger: 'change' }],
startDate: [{ required: true, message: '出发日期不能为空', trigger: 'change' }],
endDate: [{ required: true, message: '返回日期不能为空', trigger: 'change' }],
@@ -1421,7 +1420,7 @@ const state = reactive({
{ required: true, message: '联系电话不能为空', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
emergencyContact: [{ required: true, message: '应急联系人及电话不能为空', trigger: 'blur' }]
emergencyContact: [{ required: true, message: '应急联系人及联系电话不能为空', trigger: 'blur' }]
} as any,
// 查看详情
@@ -1678,13 +1677,12 @@ const resetRecordQuery = () => {
// 新增登记
const handleAddApplication = () => {
// 跳转到新增申请页面
proxy.$router.push({
path: '/rs/overseas/overseas-form',
query: {
type: 'add'
}
});
// 显示文件通知弹窗
state.noticeDialog.visible = true;
state.countdown = 5;
state.countdownActive = true;
state.readCompleted = false;
startCountdown();
};
// 开始倒计时
@@ -1721,14 +1719,17 @@ const handleCloseNotice = () => {
// 编辑申请
const handleUpdateApplication = async (row: OverseasApplicationVO) => {
// 跳转到编辑申请页面
proxy.$router.push({
path: '/rs/overseas/overseas-form',
query: {
type: 'edit',
id: row.applicationId
try {
// 加载申请详情
const response = await getOverseasApplication(row.applicationId);
state.applicationForm = response.data;
state.applicationDialog.title = '编辑因私出国(境)申请登记报备表';
state.pageType = 'edit';
state.applicationDialog.visible = true;
} catch (error) {
console.error('加载申请详情失败:', error);
ElMessage.error('加载申请详情失败');
}
});
};
// 查看申请详情
@@ -1791,14 +1792,18 @@ const handleSubmitApplication = (row: OverseasApplicationVO) => {
// 修改并重新提交申请(用于已拒绝状态)
const handleUpdateAndResubmitApplication = async (row: OverseasApplicationVO) => {
// 跳转到编辑申请页面,标记为重新提交模式
proxy.$router.push({
path: '/rs/overseas/overseas-form',
query: {
type: 'resubmit',
id: row.applicationId
try {
// 加载申请详情
const response = await getOverseasApplication(row.applicationId);
state.applicationForm = response.data;
state.applicationDialog.title = '修改并重新提交因私出国(境)申请';
state.pageType = 'resubmit';
state.isResubmitMode = true; // 标记为重新提交模式
state.applicationDialog.visible = true;
} catch (error) {
console.error('加载申请详情失败:', error);
ElMessage.error('加载申请详情失败');
}
});
};
// 取消申请
@@ -1944,11 +1949,13 @@ const getOldApprovalStatusTagType = (status: string): 'success' | 'warning' | 'd
*/
const getApprovalStatusTagType = (status: string): 'success' | 'warning' | 'danger' | 'info' | 'primary' => {
const statusMap: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
'cancel': 'info', // 已撤销 - 灰色
'draft': 'info', // 草稿 - 灰色
'waiting': 'warning', // 待审批 - 橙色
'waiting': 'warning', // 待审核 - 橙色
'finish': 'success', // 已完成 - 绿色
'invalid': 'danger', // 已作废 - 红色
'back': 'danger', // 已退回 - 红色
'cancel': 'info', // 已取消 - 灰色
'termination': 'primary' // 已终止 - 蓝色
};
return statusMap[status] || 'info';
};
@@ -1981,7 +1988,7 @@ const handleViewProcess = (row: OverseasApplicationVO) => {
if (approvalRecordRef.value && row.applicationId) {
// 使用标准工作流组件查看审批记录
approvalRecordRef.value.open(row.applicationId, 'overseas-application');
approvalRecordRef.value.open(row.applicationId, 'dyjs-cgsq');
} else {
ElMessage.warning('无法查看流程信息');
}
@@ -2022,16 +2029,42 @@ const submitForm = async (type: string) => {
// 2. 如果是提交审批,使用标准工作流API
if (type === 'submit') {
// 动态获取审批人信息
let departmentApproverId: number | null = null;
try {
// 获取部门审批人ID
const deptApproverResponse = await getDepartmentApproverId(state.applicationForm.applicantId || userStore.userId);
if (deptApproverResponse && deptApproverResponse.data) {
departmentApproverId = deptApproverResponse.data;
console.log('获取到部门审批人ID:', departmentApproverId);
} else {
console.warn('获取部门审批人ID失败');
ElMessage.error('获取部门审批人信息失败,请稍后重试');
return;
}
} catch (error) {
console.error('调用获取部门审批人ID接口失败:', error);
ElMessage.error('获取部门审批人信息失败,请稍后重试');
return;
}
// 注意:人事处审批通过角色配置,不需要动态获取审批人
console.log('人事处审批人通过hr_dept_role角色配置,无需动态获取');
// 标准工作流启动参数
const workflowData = {
flowCode: 'overseas-application',
flowCode: 'dyjs-cgsq', // 出国申请工作流标识KEY
businessId: String(applicationId),
variables: {
// dyjs-cgsq 使用的流程变量
applicantName: state.applicationForm.applicantName,
department: state.applicationForm.department,
destination: state.applicationForm.destination,
duration: state.applicationForm.duration,
budget: state.applicationForm.budget
budget: state.applicationForm.budget,
// 动态审批人变量
bmfzrsp: departmentApproverId // 部门负责人审批(对应工作流中的入库主键)
}
};
@@ -2078,7 +2111,7 @@ const handleApprovalRecord = () => {
console.log('📜 [审批记录] 打开审批记录');
if (approvalRecordRef.value && state.applicationForm.applicationId) {
approvalRecordRef.value.open(state.applicationForm.applicationId, 'overseas-application');
approvalRecordRef.value.open(state.applicationForm.applicationId, 'dyjs-cgsq');
} else {
ElMessage.warning('无法查看审批记录');
}
@@ -1,9 +1,32 @@
<template>
<div class="app-container">
<el-card class="box-card">
<!-- 🔴 标准化改造:提示用户使用标准工作流页面 -->
<el-alert
title="功能迁移通知"
type="info"
description="出国行程变更审批功能已标准化,请使用系统统一的工作流页面查看和处理任务"
show-icon
:closable="false"
class="mb-4"
>
<template #default>
<div class="mt-2">
<p><strong>推荐操作:</strong></p>
<ul class="ml-4">
<li>访问 <strong>工作流 > 我的任务 > 待办任务</strong> 页面</li>
<li>在统一的待办列表中查看"出国行程变更审批"任务</li>
<li>使用工作流的筛选功能按业务类型查看</li>
</ul>
<div class="mt-3">
<el-button type="primary" @click="navigateToTaskWaiting"> 前往待办任务页面 </el-button>
</div>
</div>
</template>
</el-alert>
<!-- 审批管理选项卡 -->
<el-tabs v-model="activeTab" type="border-card" @tab-click="handleTabClick">
<!-- 待审批 -->
<el-tab-pane label="待审批变更" name="pending">
<div class="tab-content">
@@ -46,8 +69,13 @@
</el-row>
</template>
<!-- 待审批列表 -->
<el-table v-loading="pendingLoading" border :data="pendingList" @row-dblclick="handleView">
<!-- 🔴 标准化改造:显示迁移提示 -->
<el-empty description="请使用标准工作流待办页面查看审批任务" :image-size="120">
<el-button type="primary" @click="navigateToTaskWaiting">前往待办页面</el-button>
</el-empty>
<!-- 保留原表格结构用于兼容(隐藏显示) -->
<el-table v-loading="pendingLoading" border :data="pendingList" @row-dblclick="handleView" style="display: none">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="申请人" align="center" prop="applicantName" min-width="100" />
<el-table-column label="部门" align="center" prop="department" min-width="120" />
@@ -55,14 +83,10 @@
<el-table-column label="新目的地" align="center" prop="newDestination" min-width="120" />
<el-table-column label="变更类型" align="center" prop="changeType" min-width="150" />
<el-table-column label="原出国时间" align="center" min-width="200">
<template #default="scope">
{{ formatDate(scope.row.originalStartDate) }} 至 {{ formatDate(scope.row.originalEndDate) }}
</template>
<template #default="scope"> {{ formatDate(scope.row.originalStartDate) }} 至 {{ formatDate(scope.row.originalEndDate) }} </template>
</el-table-column>
<el-table-column label="新出国时间" align="center" min-width="200">
<template #default="scope">
{{ formatDate(scope.row.newStartDate) }} 至 {{ formatDate(scope.row.newEndDate) }}
</template>
<template #default="scope"> {{ formatDate(scope.row.newStartDate) }} 至 {{ formatDate(scope.row.newEndDate) }} </template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="createTime" min-width="160" />
<el-table-column label="操作" align="center" width="200" fixed="right">
@@ -71,10 +95,22 @@
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['rs:itineraryChange:query']"></el-button>
</el-tooltip>
<el-tooltip content="通过" placement="top">
<el-button link type="success" icon="Check" @click="handleApprovalPass(scope.row)" v-hasPermi="['rs:itineraryChange:approve']"></el-button>
<el-button
link
type="success"
icon="Check"
@click="handleApprovalPass(scope.row)"
v-hasPermi="['rs:itineraryChange:approve']"
></el-button>
</el-tooltip>
<el-tooltip content="拒绝" placement="top">
<el-button link type="danger" icon="Close" @click="handleApprovalReject(scope.row)" v-hasPermi="['rs:itineraryChange:approve']"></el-button>
<el-button
link
type="danger"
icon="Close"
@click="handleApprovalReject(scope.row)"
v-hasPermi="['rs:itineraryChange:approve']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
@@ -230,14 +266,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="审批意见" prop="opinion">
<el-input
v-model="approvalForm.opinion"
type="textarea"
:rows="4"
placeholder="请输入审批意见"
maxlength="500"
show-word-limit
/>
<el-input v-model="approvalForm.opinion" type="textarea" :rows="4" placeholder="请输入审批意见" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
@@ -248,13 +277,13 @@
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="ItineraryChangeApproval">
import { ref, reactive, toRefs, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useRouter } from 'vue-router';
import animate from '@/animate';
import { parseTime } from '@/utils/ruoyi';
import {
@@ -269,6 +298,7 @@ import {
// ==================== 响应式数据 ====================
const router = useRouter();
const pendingQueryFormRef = ref();
const recordQueryFormRef = ref();
const approvalFormRef = ref();
@@ -366,18 +396,17 @@ const handleTabClick = (tab: any) => {
}
};
// 查询待审批列表
// 🔴 标准化改造:简化待审批列表获取(仅用于兼容模式)
const getPendingList = async () => {
state.pendingLoading = true;
try {
const response = await listPendingItineraryChange(state.pendingQueryParams);
state.pendingList = response.rows;
state.pendingTotal = response.total;
} catch (error) {
console.error('获取待审批列表失败:', error);
} finally {
// 提示用户使用标准工作流页面
ElMessage.info('请使用标准工作流待办页面查看审批任务');
// 清空数据,不再调用自定义API
state.pendingList = [];
state.pendingTotal = 0;
state.pendingLoading = false;
}
};
// 查询审批记录列表
@@ -400,16 +429,20 @@ const getRecordList = async () => {
}
};
// 🔴 标准化改造:导航到标准工作流待办页面
const navigateToTaskWaiting = () => {
// 跳转到标准工作流待办页面
router.push('/workflow/task/taskWaiting');
};
// 搜索查询 - 待审批列表
const handlePendingQuery = () => {
state.pendingQueryParams.pageNum = 1;
getPendingList();
ElMessage.info('请使用标准工作流待办页面查看审批任务');
};
// 重置搜索 - 待审批列表
const resetPendingQuery = () => {
pendingQueryFormRef.value?.resetFields();
handlePendingQuery();
ElMessage.info('请使用标准工作流待办页面查看审批任务');
};
// 搜索查询 - 审批记录
@@ -492,7 +525,6 @@ const getStatusTagType = (status: string): 'primary' | 'success' | 'info' | 'war
};
return typeMap[status] || 'info';
};
</script>
<style scoped>
@@ -518,4 +550,3 @@ const getStatusTagType = (status: string): 'primary' | 'success' | 'info' | 'war
min-height: 400px;
}
</style>
+370 -111
View File
@@ -12,13 +12,13 @@
>
<div class="notice-content">
<!-- 文件头部 -->
<div class="notice-header" style="text-align: center; margin-bottom: 20px;">
<h2 style="color: #1890ff; margin: 0;">杭州第一技师学院</h2>
<h3 style="color: #666; margin: 10px 0;">关于因私出国(境)申请管理的通知</h3>
<div class="notice-header" style="text-align: center; margin-bottom: 20px">
<h2 style="color: #1890ff; margin: 0">杭州第一技师学院</h2>
<h3 style="color: #666; margin: 10px 0">关于因私出国(境)申请管理的通知</h3>
</div>
<!-- 文件内容 -->
<div class="notice-body" style="line-height: 1.8; font-size: 14px; color: #333;">
<div class="notice-body" style="line-height: 1.8; font-size: 14px; color: #333">
<p><strong>各部门、各位教职工:</strong></p>
<p>为规范我院教职工因私出国(境)申请管理,根据相关规定,现将有关事项通知如下:</p>
@@ -46,7 +46,7 @@
<p><strong>五、其他</strong></p>
<p>本通知自发布之日起执行,如有疑问请联系人事处。</p>
<div style="text-align: right; margin-top: 30px;">
<div style="text-align: right; margin-top: 30px">
<p>杭州第一技师学院</p>
<p>{{ new Date().getFullYear() }}年{{ new Date().getMonth() + 1 }}月{{ new Date().getDate() }}日</p>
</div>
@@ -55,33 +55,12 @@
<!-- 弹窗底部操作按钮 -->
<template #footer>
<div class="dialog-footer" style="text-align: center;">
<el-button
v-if="countdownActive"
type="info"
disabled
size="large"
style="min-width: 150px;"
>
请阅读文件内容 ({{ countdown }}s)
</el-button>
<el-button
v-else-if="readCompleted"
type="primary"
@click="handleReadComplete"
size="large"
style="min-width: 150px;"
>
<div class="dialog-footer" style="text-align: center">
<el-button v-if="countdownActive" type="info" disabled size="large" style="min-width: 150px"> 请阅读文件内容 ({{ countdown }}s) </el-button>
<el-button v-else-if="readCompleted" type="primary" @click="handleReadComplete" size="large" style="min-width: 150px">
阅读完成,进入申请
</el-button>
<el-button
type="default"
@click="handleCloseNotice"
size="large"
style="margin-left: 20px;"
>
取消
</el-button>
<el-button type="default" @click="handleCloseNotice" size="large" style="margin-left: 20px"> 取消 </el-button>
</div>
</template>
</el-dialog>
@@ -90,10 +69,28 @@
<!-- 顶部按钮区域 -->
<div class="top-button-section">
<div class="left-buttons">
<!-- ========================================
🚨 标准化改造:根据模式显示不同按钮
======================================== -->
<!-- 审批模式按钮 -->
<template v-if="isApprovalMode">
<el-button type="success" :loading="approvalLoading" @click="handleApprove('1')" v-hasPermi="['rs:overseas:approve']">
<el-icon><Check /></el-icon> 通过
</el-button>
<el-button type="danger" :loading="approvalLoading" @click="handleApprove('2')" v-hasPermi="['rs:overseas:approve']">
<el-icon><Close /></el-icon> 拒绝
</el-button>
<el-button @click="handleBackToWorkflow">返回待办</el-button>
</template>
<!-- 普通编辑模式按钮 -->
<template v-else>
<el-button type="primary" :loading="loading" @click="handleSubmit" v-hasPermi="['rs:overseas:add', 'rs:overseas:edit']">
{{ isResubmit ? '修改并重新提交' : (isEdit ? '更新' : '提交') }}
{{ isResubmit ? '修改并重新提交' : isEdit ? '更新' : '提交' }}
</el-button>
<el-button @click="handleCancel">取消</el-button>
</template>
</div>
</div>
@@ -101,9 +98,7 @@
<el-card shadow="never" class="form-card">
<template #header>
<div class="card-header">
<span class="section-title">
{{ isResubmit ? '修改并重新提交' : (isEdit ? '编辑' : '新增') }}因私出国(境)申请登记报备表
</span>
<span class="section-title"> {{ isResubmit ? '修改并重新提交' : isEdit ? '编辑' : '新增' }}因私出国(境)申请登记报备表 </span>
</div>
</template>
@@ -202,16 +197,45 @@
</el-form-item>
</el-col>
</el-row>
<!-- 第五排:户籍所在地、移动电话 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="户籍所在地" prop="registeredAddress">
<el-input v-model="applicationForm.registeredAddress" placeholder="请输入户籍所在地(填写至县市区)" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="移动电话" prop="mobilePhone">
<el-input v-model="applicationForm.mobilePhone" placeholder="请输入移动电话" />
</el-form-item>
</el-col>
</el-row>
<!-- 第六排:人员类别 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="人员类别" prop="personnelCategory">
<el-select v-model="applicationForm.personnelCategory" placeholder="请选择人员类别" style="width: 100%">
<el-option label="教学人员" value="教学人员" />
<el-option label="管理人员" value="管理人员" />
<el-option label="教辅人员" value="教辅人员" />
<el-option label="工勤人员" value="工勤人员" />
<el-option label="其他人员" value="其他" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 出国信息 -->
<div class="form-section">
<div class="section-title">出国信息</div>
<!-- 第五排:前往国家、出境事由 -->
<!-- 第五排:前往国家或地区、出境事由 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="前往国家" prop="destinationCountry">
<el-input v-model="applicationForm.destinationCountry" placeholder="请输入前往国家" />
<el-form-item label="前往国家或地区" prop="destinationCountry">
<el-input v-model="applicationForm.destinationCountry" placeholder="请输入前往国家或地区" />
</el-form-item>
</el-col>
<el-col :span="16">
@@ -227,10 +251,21 @@
<!-- 探亲时显示的额外字段 -->
<el-row :gutter="20" v-if="applicationForm.exitReason === '探亲'">
<el-col :span="24">
<el-form-item label="探望对象关系" prop="visitRelationship">
<el-form-item label="探望对象姓名及与申请者关系" prop="visitRelationship">
<el-input v-model="applicationForm.visitRelationship" placeholder="请输入探望对象姓名及与申请者关系" />
</el-form-item>
</el-col>
</el-row>
<!-- 备注 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="applicationForm.visitRelationship"
placeholder="请输入探望对象姓名及与申请者关系"
v-model="applicationForm.remarks"
type="textarea"
:rows="2"
placeholder="请输入备注信息(可选)"
/>
</el-form-item>
</el-col>
@@ -253,7 +288,7 @@
<!-- 出国(境)时间 -->
<el-row :gutter="10">
<el-col :span="2">
<el-form-item label="出国(境)时间" label-width="110px" style="font-weight: 600; white-space: nowrap;">
<el-form-item label="出国(境)时间" label-width="110px" style="font-weight: 600; white-space: nowrap">
<span></span>
</el-form-item>
</el-col>
@@ -263,7 +298,7 @@
v-model="applicationForm.startDate"
type="date"
placeholder="请选择出发日期"
style="width: 100%; min-width: 180px;"
style="width: 100%; min-width: 180px"
format="YYYY年MM月DD日"
value-format="YYYY-MM-DD"
/>
@@ -275,7 +310,7 @@
v-model="applicationForm.endDate"
type="date"
placeholder="请选择返回日期"
style="width: 100%; min-width: 180px;"
style="width: 100%; min-width: 180px"
format="YYYY年MM月DD日"
value-format="YYYY-MM-DD"
/>
@@ -283,7 +318,7 @@
</el-col>
<el-col :span="5" :offset="1">
<el-form-item label="出国天数" prop="duration">
<el-input v-model="applicationForm.duration" placeholder="天数" disabled style="min-width: 120px;">
<el-input v-model="applicationForm.duration" placeholder="天数" disabled style="min-width: 120px">
<template #append>天</template>
</el-input>
</el-form-item>
@@ -294,27 +329,19 @@
<div
v-if="showPartyRetentionUpload"
class="party-retention-upload-section"
style="margin: 10px 0; padding: 10px; border: 1px dashed #ff4757; border-radius: 4px; background: #fff5f5;">
<div style="font-size: 14px; color: #ff4757; margin-bottom: 8px; font-weight: bold;">
style="margin: 10px 0; padding: 10px; border: 1px dashed #ff4757; border-radius: 4px; background: #fff5f5"
>
<div style="font-size: 14px; color: #ff4757; margin-bottom: 8px; font-weight: bold">
<el-icon><Warning /></el-icon>
党籍保留附件上传(必填)
</div>
<div style="font-size: 12px; color: #606266; margin-bottom: 8px;">
<div style="font-size: 12px; color: #606266; margin-bottom: 8px">
您是中共党员且出国天数超过30天,需要先提交党籍保留流程,审批通过后下载并上传盖章附件
</div>
<!-- 下载PDF空白模板按钮 -->
<div style="margin-bottom: 10px;">
<el-button
type="primary"
size="small"
icon="Download"
@click="handleDownloadPartyRetention"
>
下载党籍保留申请PDF模板
</el-button>
<span style="margin-left: 10px; font-size: 12px; color: #909399;">
* 请先下载空白模板并手工填写,然后上传盖章后的文件
</span>
<div style="margin-bottom: 10px">
<el-button type="primary" size="small" icon="Download" @click="handleDownloadPartyRetention"> 下载党籍保留申请PDF模板 </el-button>
<span style="margin-left: 10px; font-size: 12px; color: #909399"> * 请先下载空白模板并手工填写,然后上传盖章后的文件 </span>
</div>
<div class="attachment-upload-container">
<el-upload
@@ -332,13 +359,9 @@
list-type="text"
class="upload-demo"
>
<el-button size="small" type="primary" icon="Upload">
上传党籍保留附件
</el-button>
<el-button size="small" type="primary" icon="Upload"> 上传党籍保留附件 </el-button>
<template #tip>
<div class="el-upload__tip" style="font-size: 12px; color: #909399;">
支持 PDF、图片、Word 格式,文件大小不超过 10MB
</div>
<div class="el-upload__tip" style="font-size: 12px; color: #909399">支持 PDF、图片、Word 格式,文件大小不超过 10MB</div>
</template>
</el-upload>
</div>
@@ -354,19 +377,17 @@
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 备注 -->
<div class="form-section">
<!-- 联系方式 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="applicationForm.remarks"
type="textarea"
:rows="1"
placeholder="请输入备注信息"
/>
<el-col :span="12">
<el-form-item label="联系电话" prop="mobilePhone">
<el-input v-model="applicationForm.mobilePhone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="应急联系人及联系电话" prop="emergencyContact">
<el-input v-model="applicationForm.emergencyContact" placeholder="请输入应急联系人及联系电话" />
</el-form-item>
</el-col>
</el-row>
@@ -381,9 +402,17 @@ import { ref, reactive, computed, onMounted, getCurrentInstance, watch, nextTick
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { Warning, Download, Upload } from '@element-plus/icons-vue';
import { Warning, Download, Upload, Check, Close } from '@element-plus/icons-vue';
import { getToken } from '@/utils/auth';
import request from '@/utils/request';
import {
addOverseasApplication,
updateOverseasApplication,
getOverseasApplication,
approveOverseasApplication,
type OverseasApplicationForm,
type ApprovalRequest
} from '@/api/rs/overseas';
// 获取当前实例和路由
const { proxy } = getCurrentInstance();
@@ -396,6 +425,16 @@ const formLoading = ref(false);
const applicationFormRef = ref<FormInstance>();
const partyRetentionUploadRef = ref();
// ========================================
// 🚨 标准化改造:审批相关状态
// ========================================
const approvalLoading = ref(false);
const approvalDialogVisible = ref(false);
const approvalForm = reactive({
approvalOpinion: '',
approvalStatus: '1'
});
// 党籍保留附件相关
const partyRetentionFileList = ref([]);
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/rs/overseas/application/attachment/upload');
@@ -430,12 +469,27 @@ const uploadHeaders = computed(() => {
};
});
// ========================================
// 🚨 标准化改造:支持工作流审批模式
// ========================================
// 判断是否为审批模式(通过工作流跳转)
const isApprovalMode = computed(() => !!route.query.taskId);
// 判断是否为编辑模式
const isEdit = computed(() => route.query.type === 'edit' || route.query.type === 'resubmit' || !!route.query.id);
// 判断是否为重新提交模式
const isResubmit = computed(() => route.query.type === 'resubmit');
// 获取工作流任务ID
const taskId = computed(() => route.query.taskId as string);
// 获取申请ID(从路由参数)
const applicationId = computed(() => {
return route.query.applicationId || route.query.id;
});
// 申请表单数据
const applicationForm = reactive({
applicationId: '',
@@ -448,6 +502,9 @@ const applicationForm = reactive({
position: '',
documentName: '',
documentNumber: '',
registeredAddress: '', // ✅ 新增:户籍所在地
mobilePhone: '', // ✅ 新增:移动电话
personnelCategory: '', // ✅ 新增:人员类别
destinationCountry: '',
exitReason: '',
visitRelationship: '',
@@ -471,6 +528,13 @@ const applicationRules: FormRules = {
position: [{ required: true, message: '请输入职务', trigger: 'blur' }],
documentName: [{ required: true, message: '请输入申领证件名称', trigger: 'blur' }],
documentNumber: [{ required: true, message: '请输入证件号码', trigger: 'blur' }],
// ✅ 新增必填字段验证
registeredAddress: [{ required: true, message: '请输入户籍所在地', trigger: 'blur' }],
mobilePhone: [
{ required: true, message: '请输入移动电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '移动电话格式不正确', trigger: 'blur' }
],
personnelCategory: [{ required: true, message: '请选择人员类别', trigger: 'change' }],
destinationCountry: [{ required: true, message: '请输入前往国家', trigger: 'blur' }],
exitReason: [{ required: true, message: '请选择出境事由', trigger: 'change' }],
itinerary: [{ required: true, message: '请输入行程安排', trigger: 'blur' }],
@@ -538,9 +602,8 @@ const handleDownloadPartyRetention = async () => {
});
// 创建blob并触发下载(response已经是blob对象)
const blob = response instanceof Blob
? new Blob([response], { type: 'application/pdf' })
: new Blob([response.data], { type: 'application/pdf' });
const blob =
response instanceof Blob ? new Blob([response], { type: 'application/pdf' }) : new Blob([response.data], { type: 'application/pdf' });
console.log('📥 生成的Blob大小:', blob.size, 'bytes');
console.log('📥 Blob类型:', blob.type);
@@ -569,9 +632,14 @@ const handleDownloadPartyRetention = async () => {
// 党籍保留附件上传前检查
const beforePartyRetentionUpload = async (file: any) => {
// 检查文件格式
const isValidType = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']
.includes(file.type);
const isValidType = [
'application/pdf',
'image/jpeg',
'image/jpg',
'image/png',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
].includes(file.type);
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isValidType) {
@@ -684,73 +752,264 @@ const handleSubmit = async () => {
loading.value = true;
// 这里调用保存API
// 由于我只能修改rs模块内容,暂时模拟提交成功
await new Promise(resolve => setTimeout(resolve, 1000));
// 准备提交数据 - 修复字段映射关系
const submitData: OverseasApplicationForm = {
...applicationForm,
// ✅ 按照纸质版表格字段直接映射,不做转换
// 保持前端字段名与数据库字段名一致
destinationCountry: applicationForm.destinationCountry || '', // 前往国家或地区
exitReason: applicationForm.exitReason || '', // 出境事由
itinerary: applicationForm.itinerary || '', // 行程安排
// 以下字段用于兼容后端现有逻辑
purpose: applicationForm.exitReason || '', // 兼容字段
purposeDetail: applicationForm.itinerary || '', // 兼容字段
destination: applicationForm.destinationCountry || '', // 兼容字段
// ✅ 使用用户实际填写的字段值,不再使用默认值
registeredAddress: applicationForm.registeredAddress || '',
mobilePhone: applicationForm.mobilePhone || '',
personnelCategory: applicationForm.personnelCategory || '',
emergencyContact: applicationForm.emergencyContact || '', // 应急联系人及联系电话
remarks: applicationForm.remarks || '', // 备注
// 保留的默认值字段
budget: 0, // 如果没有预算字段,设置默认值
contactPhone: applicationForm.mobilePhone || '暂无', // 使用移动电话作为联系电话
duration: applicationForm.duration ? Number(applicationForm.duration) : 0
};
// 新增模式:删除系统自动设置的字段,避免验证错误
if (!isEdit.value) {
delete submitData.applicantId;
// ✅ 保留用户填写的姓名字段,不要删除
// delete submitData.applicantName;
// ✅ 保留部门字段,后端需要此字段
// delete submitData.department;
delete submitData.tenantId;
delete submitData.applyTime;
}
// 保留办公电话字段的过滤逻辑(可选字段)
if (!submitData.officePhone) delete submitData.officePhone;
let result;
if (isEdit.value && applicationForm.applicationId) {
// 更新模式
result = await updateOverseasApplication(submitData);
} else {
// 新增模式
result = await addOverseasApplication(submitData);
// 保存返回的applicationId
if (result.data?.applicationId) {
applicationForm.applicationId = result.data.applicationId;
}
}
const successMessage = isResubmit.value
? '修改并重新提交成功!您的申请已重新进入审批流程。'
: (isEdit.value ? '更新成功' : '申请提交成功!您的申请已进入审批流程,请耐心等待审批结果。');
: isEdit.value
? '更新成功'
: '申请提交成功!您的申请已进入审批流程,请耐心等待审批结果。';
ElMessage.success(successMessage);
// 返回上一页
handleCancel();
} catch (error) {
console.error('表单验证失败:', error);
handleGoBack();
} catch (error: any) {
console.error('提交失败:', error);
ElMessage.error(error?.message || '提交失败,请稍后重试');
} finally {
loading.value = false;
}
};
// 取消操作
// 保存申请(用于获取applicationId)
const handleSave = async () => {
if (!applicationFormRef.value) return;
await applicationFormRef.value.validate();
// 如果已有ID,无需重复保存
if (applicationForm.applicationId) {
return;
}
// 准备保存数据 - 修复字段映射关系
const saveData: OverseasApplicationForm = {
...applicationForm,
// ✅ 按照纸质版表格字段直接映射,不做转换
// 保持前端字段名与数据库字段名一致
destinationCountry: applicationForm.destinationCountry || '', // 前往国家或地区
exitReason: applicationForm.exitReason || '', // 出境事由
itinerary: applicationForm.itinerary || '', // 行程安排
// 以下字段用于兼容后端现有逻辑
purpose: applicationForm.exitReason || '', // 兼容字段
purposeDetail: applicationForm.itinerary || '', // 兼容字段
destination: applicationForm.destinationCountry || '', // 兼容字段
// ✅ 使用用户实际填写的字段值,不再使用默认值
registeredAddress: applicationForm.registeredAddress || '',
mobilePhone: applicationForm.mobilePhone || '',
personnelCategory: applicationForm.personnelCategory || '',
emergencyContact: applicationForm.emergencyContact || '', // 应急联系人及联系电话
remarks: applicationForm.remarks || '', // 备注
// 保留的默认值字段
budget: 0, // 如果没有预算字段,设置默认值
contactPhone: applicationForm.mobilePhone || '暂无', // 使用移动电话作为联系电话
duration: applicationForm.duration ? Number(applicationForm.duration) : 0
};
// 删除系统自动设置的字段,避免验证错误
delete saveData.applicantId;
// ✅ 保留用户填写的姓名字段,不要删除
// delete saveData.applicantName;
delete saveData.department;
delete saveData.tenantId;
delete saveData.applyTime;
// 保留办公电话字段的过滤逻辑(可选字段)
if (!saveData.officePhone) delete saveData.officePhone;
try {
const result = await addOverseasApplication(saveData);
if (result.data?.applicationId) {
applicationForm.applicationId = result.data.applicationId;
ElMessage.success('申请信息保存成功');
}
} catch (error) {
console.error('保存失败:', error);
throw new Error('保存申请信息失败');
}
};
// 直接返回上一页(不弹确认框)
const handleGoBack = () => {
// 关闭当前页面,返回上一页
proxy.$tab.closePage(route);
router.go(-1);
};
// 取消操作(弹出确认框)
const handleCancel = () => {
ElMessageBox.confirm('是否确认取消操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 关闭当前页面,返回上一页
proxy.$tab.closePage(route);
router.go(-1);
}).catch(() => {
})
.then(() => {
handleGoBack();
})
.catch(() => {
// 用户取消
});
};
// 初始化数据
const initData = async () => {
if (isEdit.value && route.query.id) {
// 🔴 标准化改造:支持审批模式和编辑模式
const targetId = applicationId.value;
if ((isEdit.value && route.query.id) || (isApprovalMode.value && targetId)) {
formLoading.value = true;
try {
// 这里应该根据ID获取数据
// 由于我只能修改rs模块内容,暂时模拟数据加载
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟填充编辑数据
// 根据ID获取申请数据(支持审批模式和编辑模式)
const result = await getOverseasApplication(targetId as string);
if (result.data) {
// 映射数据到表单 - 修复字段映射关系
Object.assign(applicationForm, {
applicationId: route.query.id,
applicantName: '张三',
gender: '男',
// ... 其他字段
applicationId: result.data.applicationId,
applicantName: result.data.applicantName || '',
gender: result.data.gender || '',
birthDate: result.data.birthDate || '',
politicalStatus: result.data.politicalStatus || '',
department: result.data.department || '',
professionalTitle: result.data.professionalTitle || '',
position: result.data.position || '',
documentName: result.data.documentName || '',
documentNumber: result.data.documentNumber || '',
// ✅ 新增字段的映射
registeredAddress: result.data.registeredAddress || '',
mobilePhone: result.data.mobilePhone || '',
personnelCategory: result.data.personnelCategory || '',
// ✅ 修复字段映射关系
destinationCountry: result.data.destination || '', // 后端destination -> 前端destinationCountry
exitReason: result.data.purpose || '', // 后端purpose -> 前端exitReason
itinerary: result.data.purposeDetail || result.data.itinerary || '', // 后端purposeDetail -> 前端itinerary
visitRelationship: result.data.visitRelationship || '',
startDate: result.data.startDate || '',
endDate: result.data.endDate || '',
duration: result.data.duration || '',
remarks: result.data.applicantRemark || result.data.remarks || '',
partyRetentionAttachment: result.data.partyRetentionFilePath || '',
partyRetentionAttachmentId: result.data.partyRetentionFileOssId || ''
});
}
} catch (error) {
console.error('获取数据失败:', error);
ElMessage.error('获取数据失败');
ElMessage.error('获取申请数据失败');
} finally {
formLoading.value = false;
}
}
};
// ========================================
// 🚨 标准化改造:审批相关处理函数
// ========================================
// 处理审批操作
const handleApprove = async (approvalStatus: string) => {
try {
const title = approvalStatus === '1' ? '审批通过' : '审批拒绝';
const placeholder = approvalStatus === '1' ? '请填写审批意见(可选)' : '请填写拒绝理由';
const { value: opinion } = await ElMessageBox.prompt(
`确定要${title}此申请吗?`,
title,
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: placeholder,
inputType: 'textarea',
inputValidator: approvalStatus === '2' ? (value) => {
return value && value.trim() !== '' || '拒绝时必须填写理由';
} : null
}
);
approvalLoading.value = true;
const approvalRequest: ApprovalRequest = {
applicationId: applicationId.value as string,
approvalStatus,
approvalOpinion: opinion || '',
taskId: taskId.value
};
await approveOverseasApplication(approvalRequest);
ElMessage.success(`${title}成功!`);
// 返回到待办任务页面
handleBackToWorkflow();
} catch (error: any) {
if (error !== 'cancel') {
console.error('审批失败:', error);
ElMessage.error(error?.message || '审批失败,请稍后重试');
}
} finally {
approvalLoading.value = false;
}
};
// 返回到工作流待办页面
const handleBackToWorkflow = () => {
router.push('/workflow/task/taskWaiting');
};
// 组件挂载时初始化
onMounted(() => {
nextTick(() => {
initData();
// 新增模式下显示文件阅读弹窗
if (route.query.type === 'add') {
// 新增模式下显示文件阅读弹窗(审批模式下不显示)
if (route.query.type === 'add' && !isApprovalMode.value) {
noticeDialog.visible = true;
startCountdown();
}
+15 -6
View File
@@ -463,14 +463,23 @@ function handleTabClick(tab) {
/** 查询出国审批列表 */
function getList() {
loading.value = true;
console.log('临时清空出国审批列表内容');
const params = {
...queryParams.value,
pageNum: queryParams.value.pageNum,
pageSize: queryParams.value.pageSize
};
// 临时清空列表内容
approvalList.value = [];
total.value = 0;
listOverseasApproval(params)
.then((response) => {
approvalList.value = response.rows;
total.value = response.total;
loading.value = false;
console.log('出国审批列表已清空');
console.log('出国审批列表查询成功,记录数:', total.value);
})
.catch((error) => {
console.error('查询出国审批列表失败:', error);
loading.value = false;
});
}
/** 查询审批记录列表 */