提交代码

This commit is contained in:
zlwu committed 2025-11-28 20:01:32 +08:00
1 parent 4e34c7eb36
commit 375a2c8df7
54 files changed
+4631 -4352

No files matched your search

@@ -1,2 +0,0 @@
# 人事管理模块 - 控制器层
# 此目录用于存放REST API控制器类
@@ -24,6 +24,7 @@ import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
@@ -97,6 +98,8 @@ public class RsOverseasApplicationController extends BaseController {
/**
* 查询当前用户的待审批申请列表(五级审批模式)
*
* @deprecated 已废弃,请使用 pendingPageList 方法(支持分页和搜索)
*/
@SaCheckPermission("rs:overseasapproval:list")
@GetMapping("/pending/list")
@@ -105,6 +108,30 @@ public class RsOverseasApplicationController extends BaseController {
return R.ok(list);
}
/**
* 查询当前用户的待审批申请分页列表(根据角色过滤,支持分页和搜索)
*
* 🔴 核心逻辑:
* 1. 根据用户角色确定审批级别列表
* 2. 部门负责人(rs_bmfzr)只能看到本部门的申请
* 3. 其他角色可以看到全校的申请
* 4. 支持分页和搜索(前往国家、出境事由)
* 5. 只返回status='0'(待审核)的申请
* 6. 按创建时间倒序排列
*
* 角色映射:
* - rs_bmfzr → approval_level='1' + 部门过滤
* - hr_dept_role → approval_level='2'
* - discipline_committee_role → approval_level='3'
* - college_leader_role → approval_level='4'
* - party_secretary_role → approval_level='5'
*/
@SaCheckPermission("rs:overseasapproval:list")
@GetMapping("/pending/page")
public TableDataInfo<RsOverseasApplicationVo> pendingPageList(RsOverseasApplicationQuery query) {
return applicationService.queryPendingPageListByRole(query);
}
// ========================================
// 五级审批模式说明
//
@@ -321,7 +348,7 @@ public class RsOverseasApplicationController extends BaseController {
@PostMapping("/approve")
public R<Void> approve(@RequestBody ApprovalRequest request) {
Long approverId = LoginHelper.getUserId();
String approverName = LoginHelper.getUsername();
String approverName = StringUtils.defaultIfBlank(LoginHelper.getNickname(), LoginHelper.getUsername());
// 🔴 标准工作流模式:支持任务ID参数
// 如果传入了taskId,将通过工作流引擎进行审批
@@ -488,12 +515,6 @@ public class RsOverseasApplicationController extends BaseController {
public R<Map<String, Object>> uploadStampedFile(@RequestParam("file") MultipartFile file,
@RequestParam("applicationId") Long applicationId) {
try {
// 验证文件格式
String contentType = file.getContentType();
if (!isValidFileType(contentType)) {
return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件");
}
// 验证文件大小(10MB)
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) {
@@ -561,12 +582,6 @@ public class RsOverseasApplicationController extends BaseController {
public R<Map<String, Object>> uploadPartyRetentionFile(@RequestParam("file") MultipartFile file,
@RequestParam("applicationId") Long applicationId) {
try {
// 验证文件格式
String contentType = file.getContentType();
if (!isValidFileType(contentType)) {
return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件");
}
// 验证文件大小(10MB)
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) {
@@ -583,7 +598,7 @@ public class RsOverseasApplicationController extends BaseController {
ossResult.getOriginalName(), // 原始文件名
new Date(), // 上传时间
"1", // 上传状态:已上传
ossResult.getOssId() // OSS文件ID
String.valueOf(ossResult.getOssId()) // OSS文件ID - 转换为字符串
);
// 返回结果
@@ -616,25 +631,24 @@ public class RsOverseasApplicationController extends BaseController {
return;
}
// 处理多个OSS ID的情况(逗号分隔),取第一个ID进行下载
String ossIdStr = application.getPartyRetentionFileOssId();
Long ossId;
if (ossIdStr.contains(",")) {
// 如果包含逗号,取第一个ID
ossId = Long.parseLong(ossIdStr.split(",")[0].trim());
} else {
// 单个ID直接解析
ossId = Long.parseLong(ossIdStr);
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getPartyRetentionFileOssId(), response);
ossService.secureDownload(ossId, response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 验证文件类型
*/
private boolean isValidFileType(String contentType) {
if (contentType == null)
return false;
return contentType.equals("application/pdf") ||
contentType.equals("image/jpeg") ||
contentType.equals("image/jpg") ||
contentType.equals("image/png");
}
/**
* 获取文件扩展名
*/
@@ -62,17 +62,13 @@ public class RsOverseasItineraryChangeController extends BaseController {
}
/**
* 🔴 标准化改造:删除自定义待审批查询接口
*
* 原因:出国行程变更工作流应使用标准工作流模式
* 用户应通过工作流API pageByTaskWait() 获取待办任务
* 而不是调用自定义的业务API
* 查询待审批的行程变更申请列表(status=0)
*/
@SaCheckPermission("rs:itineraryChange:approve")
@GetMapping("/pending/list")
public R<String> pendingList(RsOverseasItineraryChangeQuery query) {
// 提示用户使用标准工作流API
return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务");
public TableDataInfo<RsOverseasItineraryChangeVo> pendingList(RsOverseasItineraryChangeQuery query) {
query.setTenantId(LoginHelper.getTenantId());
return changeService.queryPendingList(query);
}
/**
@@ -167,7 +163,7 @@ public class RsOverseasItineraryChangeController extends BaseController {
/**
* 导出行程变更申请列表
*/
@SaCheckPermission("rs:itineraryChange:export")
// @SaCheckPermission("rs:itineraryChange:export")
@Log(title = "行程变更申请", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(RsOverseasItineraryChangeQuery query, HttpServletResponse response) {
@@ -179,7 +175,7 @@ public class RsOverseasItineraryChangeController extends BaseController {
/**
* 查询某个出国记录的所有变更历史
*/
@SaCheckPermission("rs:itineraryChange:query")
// @SaCheckPermission("rs:itineraryChange:query")
@GetMapping("/record/{recordId}/history")
public R<List<RsOverseasItineraryChangeVo>> getChangeHistory(@PathVariable Long recordId) {
return R.ok(changeService.queryByRecordId(recordId));
@@ -32,7 +32,7 @@ import org.dromara.rs.service.IRsOverseasRegistrationService;
/**
* 出国登记Controller
*
*
* @author weizzzz
* @date 2025-09-30
*/
@@ -47,7 +47,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 查询出国登记列表
*/
@SaCheckPermission("rs:overseas:registration:list")
// @SaCheckPermission("rs:overseasregistration:list")
@GetMapping("/list")
public TableDataInfo<RsOverseasRegistrationVo> list(RsOverseasRegistrationQuery query) {
return registrationService.queryPageList(query);
@@ -56,7 +56,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 查询我的出国登记列表
*/
@SaCheckPermission("rs:overseas:registration:list")
// @SaCheckPermission("rs:overseasregistration:list")
@GetMapping("/my/list")
public TableDataInfo<RsOverseasRegistrationVo> myList(RsOverseasRegistrationQuery query) {
return registrationService.queryMyRegistrationList(query);
@@ -65,7 +65,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 导出出国登记列表
*/
@SaCheckPermission("rs:overseas:registration:export")
// @SaCheckPermission("rs:overseasregistration:export")
@Log(title = "出国登记", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(RsOverseasRegistrationQuery query, HttpServletResponse response) {
@@ -76,27 +76,26 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 获取出国登记详细信息
*/
@SaCheckPermission("rs:overseas:registration:query")
// @SaCheckPermission("rs:overseasregistration:query")
@GetMapping("/{registrationId}")
public R<RsOverseasRegistrationVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<RsOverseasRegistrationVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return R.ok(registrationService.queryById(registrationId));
}
/**
* 根据申请ID获取出国登记信息
*/
@SaCheckPermission("rs:overseas:registration:query")
// @SaCheckPermission("rs:overseasregistration:query")
@GetMapping("/application/{applicationId}")
public R<RsOverseasRegistrationVo> getByApplicationId(@NotNull(message = "申请ID不能为空")
@PathVariable Long applicationId) {
public R<RsOverseasRegistrationVo> getByApplicationId(
@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId) {
return R.ok(registrationService.queryByApplicationId(applicationId));
}
/**
* 新增出国登记
*/
@SaCheckPermission("rs:overseas:registration:add")
// @SaCheckPermission("rs:overseasregistration:add")
@Log(title = "出国登记", businessType = BusinessType.INSERT)
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody RsOverseasRegistrationBo bo) {
@@ -106,7 +105,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 新增并提交出国登记
*/
@SaCheckPermission("rs:overseas:registration:add")
// @SaCheckPermission("rs:overseasregistration:add")
@Log(title = "出国登记", businessType = BusinessType.INSERT)
@PostMapping("/addAndSubmit")
public R<Void> addAndSubmit(@Validated(AddGroup.class) @RequestBody RsOverseasRegistrationBo bo) {
@@ -116,7 +115,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 修改出国登记
*/
@SaCheckPermission("rs:overseas:registration:edit")
// @SaCheckPermission("rs:overseasregistration:edit")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody RsOverseasRegistrationBo bo) {
@@ -126,67 +125,61 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 删除出国登记
*/
@SaCheckPermission("rs:overseas:registration:remove")
// @SaCheckPermission("rs:overseasregistration:remove")
@Log(title = "出国登记", businessType = BusinessType.DELETE)
@DeleteMapping("/{registrationIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] registrationIds) {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] registrationIds) {
return toAjax(registrationService.deleteWithValidByIds(List.of(registrationIds), true));
}
/**
* 提交出国登记
*/
@SaCheckPermission("rs:overseas:registration:submit")
// @SaCheckPermission("rs:overseasregistration:submit")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/submit/{registrationId}")
public R<Void> submit(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<Void> submit(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return toAjax(registrationService.submitRegistration(registrationId));
}
/**
* 确认出国登记
*/
@SaCheckPermission("rs:overseas:registration:confirm")
// @SaCheckPermission("rs:overseasregistration:confirm")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/confirm/{registrationId}")
public R<Void> confirm(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<Void> confirm(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return toAjax(registrationService.confirmRegistration(registrationId));
}
/**
* 撤回出国登记
*/
@SaCheckPermission("rs:overseas:registration:withdraw")
// @SaCheckPermission("rs:overseasregistration:withdraw")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/withdraw/{registrationId}")
public R<Void> withdraw(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<Void> withdraw(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return toAjax(registrationService.withdrawRegistration(registrationId));
}
/**
* 基于出国申请创建登记记录
*/
@SaCheckPermission("rs:overseas:registration:create")
// @SaCheckPermission("rs:overseasregistration:create")
@Log(title = "出国登记", businessType = BusinessType.INSERT)
@PostMapping("/create/{applicationId}")
public R<Void> createFromApplication(@NotNull(message = "申请ID不能为空")
@PathVariable Long applicationId) {
public R<Void> createFromApplication(@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId) {
return toAjax(registrationService.createFromApplication(applicationId));
}
/**
* 批准出国登记
*/
@SaCheckPermission("rs:registration:approve")
// @SaCheckPermission("rs:registration:approve")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/approve/{registrationId}")
public R<Void> approve(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
public R<Void> approve(@NotNull(message = "主键不能为空") @PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
String approveNotes = (bo != null && bo.getRemarks() != null) ? bo.getRemarks() : "同意";
return toAjax(registrationService.approveRegistration(registrationId, approveNotes));
}
@@ -194,13 +187,12 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 拒绝出国登记
*/
@SaCheckPermission("rs:registration:reject")
// @SaCheckPermission("rs:registration:reject")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/reject/{registrationId}")
public R<Void> reject(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
public R<Void> reject(@NotNull(message = "主键不能为空") @PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
String rejectReason = (bo != null && bo.getRemarks() != null) ? bo.getRemarks() : "不同意";
return toAjax(registrationService.rejectRegistration(registrationId, rejectReason));
}
}
}
@@ -384,10 +384,10 @@ public class RsOverseasApplication extends BaseEntity {
private String partyRetentionUploadStatus;
/**
* 党籍保留附件OSS ID(用于安全下载)
* 党籍保留附件OSS ID(用于安全下载,支持多个ID用逗号分隔)
*/
@ExcelIgnore
private Long partyRetentionFileOssId;
private String partyRetentionFileOssId;
/**
* 租户编号
@@ -29,7 +29,7 @@ public class RsOverseasRegistration extends BaseEntity {
/**
* 登记ID
*/
@TableId(value = "registration_id", type = IdType.AUTO)
@TableId(value = "registration_id", type = IdType.ASSIGN_ID)
@ExcelProperty(value = "登记ID")
private Long registrationId;
@@ -317,6 +317,32 @@ public class RsOverseasApplicationBo extends BaseEntity {
*/
private String stampedFileUploadStatus;
/**
* 党籍保留附件文件路径
*/
private String partyRetentionFilePath;
/**
* 党籍保留附件文件名
*/
private String partyRetentionFileName;
/**
* 党籍保留附件上传时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date partyRetentionUploadTime;
/**
* 党籍保留附件上传状态:0-未上传,1-已上传
*/
private String partyRetentionUploadStatus;
/**
* 党籍保留附件OSS ID(支持多个ID用逗号分隔)
*/
private String partyRetentionFileOssId;
/**
* 租户ID
*/
@@ -365,9 +365,9 @@ public class RsOverseasApplicationVo implements Serializable {
private String partyRetentionUploadStatus;
/**
* 党籍保留附件OSS ID(用于安全下载)
* 党籍保留附件OSS ID(用于安全下载,支持多个ID用逗号分隔)
*/
private Long partyRetentionFileOssId;
private String partyRetentionFileOssId;
/**
* 租户编号
@@ -196,6 +196,11 @@ public class RsOverseasApprovalMonitorVo implements Serializable {
@ExcelProperty(value = "应急联系人及电话")
private String emergencyContact;
/**
* 申请人手写签名(base64)
*/
private String signature;
// ==================== 审批状态信息 ====================
/**
@@ -144,4 +144,23 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus<RsOverseasAp
* @return 待审批申请列表
*/
List<RsOverseasApplication> selectPendingApplications(List<String> approvalLevels, Long deptId);
/**
* 🔴 新增:根据角色查询待审批申请列表(支持分页和搜索)
*
* 查询逻辑:
* 1. 根据用户角色确定审批级别列表
* 2. 关联 sys_user 表获取申请人的部门信息
* 3. 按审批级别过滤(approvalLevel IN (...))
* 4. 按状态过滤(status = '0' 待审核)
* 5. 按部门过滤(部门负责人:u.dept_id = 当前用户部门ID)
* 6. 按搜索条件过滤(前往国家、出境事由)
* 7. 按创建时间倒序排列
*
* @param query 查询条件(包含分页、搜索条件)
* @param approvalLevels 审批级别列表,例如:["1", "2"]
* @param deptId 部门ID(部门负责人需要),为null则不过滤部门
* @return 待审批申请分页列表
*/
List<RsOverseasApplicationVo> selectPendingApplicationsByRole(RsOverseasApplicationQuery query, List<String> approvalLevels, Long deptId);
}
@@ -48,6 +48,14 @@ public interface IRsOverseasApplicationService {
*/
List<RsOverseasApplicationVo> queryPendingByCurrentUser();
/**
* 查询当前用户的待审批申请分页列表(根据角色过滤)
*
* @param query 查询条件(包含分页、搜索条件)
* @return 待审批申请分页集合
*/
TableDataInfo<RsOverseasApplicationVo> queryPendingPageListByRole(RsOverseasApplicationQuery query);
/**
* 查询出国申请详细信息
*
@@ -207,7 +215,7 @@ public interface IRsOverseasApplicationService {
* @return 是否成功
*/
Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
java.util.Date uploadTime, String uploadStatus, String ossId);
/**
* 校验出国登记前置条件
@@ -51,7 +51,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
/**
* 出国申请Service业务层处理
*
*
* @author weizzzz
* @date 2025-07-28
*/
@@ -232,14 +232,14 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤)
*
*
* 🆕 核心改进:
* 1. 不再依赖 current_approver_id 字段(该字段仅作为冗余展示用)
* 2. 动态查询用户的审批权限,转换为审批级别
* 3. 根据审批级别和状态查询待审批申请
* 4. 🔴 部门负责人只能看到本部门的申请
* 5. 支持人员岗位变动:权限调整后立即生效
*
*
* 查询逻辑:
* - 用户有 rs:overseas:approve1 权限(部门负责人)→ 查询本部门员工的、approval_level='1' 且
* status='0' 的申请
@@ -307,6 +307,156 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
return result;
}
/**
* 查询当前用户的待审批申请分页列表(根据角色过滤)
*
* 🔴 核心逻辑:
* 1. 根据用户角色确定审批级别列表
* 2. 部门负责人(rs_bmfzr)只能看到本部门的申请
* 3. 其他角色可以看到全校的申请
* 4. 支持分页和搜索(前往国家、出境事由)
* 5. 按创建时间倒序排列
* 6. 🔴 修复:当用户同时拥有多个角色时,分别查询后合并去重
*
* 角色映射:
* - rs_bmfzr → approval_level='1' + 部门过滤
* - hr_dept_role → approval_level='2'
* - discipline_committee_role → approval_level='3'
* - college_leader_role → approval_level='4'
* - party_secretary_role → approval_level='5'
*/
@Override
public TableDataInfo<RsOverseasApplicationVo> queryPendingPageListByRole(RsOverseasApplicationQuery query) {
Long currentUserId = LoginHelper.getUserId();
String currentUserName = LoginHelper.getUsername();
String tenantId = LoginHelper.getTenantId();
log.info("🔍 查询用户待审批申请分页列表 - 用户ID: {}, 用户名: {}", currentUserId, currentUserName);
// 1. 获取当前用户的角色列表
List<SysRoleVo> userRoles = roleService.selectRolesByUserId(currentUserId);
if (userRoles == null || userRoles.isEmpty()) {
log.warn("❌ 用户 {} 没有任何角色,返回空列表", currentUserName);
return TableDataInfo.build();
}
// 2. 根据角色编码确定审批级别列表
List<String> deptHeadLevels = new ArrayList<>(); // 部门负责人审批级别
List<String> otherLevels = new ArrayList<>(); // 其他审批级别
Long deptIdForFilter = null;
boolean hasDeptHeadRole = false;
boolean hasOtherRole = false;
for (SysRoleVo role : userRoles) {
String roleKey = role.getRoleKey();
if (roleKey == null) {
continue;
}
// 角色编码映射到审批级别
if ("rs_bmfzr".equals(roleKey)) {
deptHeadLevels.add("1");
hasDeptHeadRole = true;
} else if ("hr_dept_role".equals(roleKey)) {
otherLevels.add("2");
hasOtherRole = true;
} else if ("discipline_committee_role".equals(roleKey)) {
otherLevels.add("3");
hasOtherRole = true;
} else if ("college_leader_role".equals(roleKey)) {
otherLevels.add("4");
hasOtherRole = true;
} else if ("party_secretary_role".equals(roleKey)) {
otherLevels.add("5");
hasOtherRole = true;
}
}
if (!hasDeptHeadRole && !hasOtherRole) {
log.warn("❌ 用户 {} 没有任何出国审批角色,返回空列表", currentUserName);
return TableDataInfo.build();
}
log.info("✅ 用户 {} 拥有审批级别 - 部门负责人: {}, 其他角色: {}",
currentUserName, deptHeadLevels, otherLevels);
// 3. 如果是部门负责人,获取部门ID进行过滤
if (hasDeptHeadRole) {
SysUserVo currentUser = userService.selectUserById(currentUserId);
if (currentUser != null && currentUser.getDeptId() != null) {
deptIdForFilter = currentUser.getDeptId();
log.info("🔒 [部门权限过滤] 部门负责人,用户: {}, 部门ID: {}, 只查看本部门待审批",
currentUserName, deptIdForFilter);
} else {
log.warn("⚠️ 用户 {} 是部门负责人但没有部门信息,无法过滤", currentUserName);
}
}
// 4. 设置租户ID
if (StringUtils.isNotBlank(tenantId)) {
query.setTenantId(tenantId);
}
// 5. 🔴 修复:分别查询不同角色的数据,然后合并去重
List<RsOverseasApplicationVo> allList = new ArrayList<>();
Map<Long, RsOverseasApplicationVo> applicationMap = new HashMap<>(); // 用于去重,key=applicationId
// 5.1 查询部门负责人的数据(本部门,审批级别='1')
if (hasDeptHeadRole && !deptHeadLevels.isEmpty() && deptIdForFilter != null) {
List<RsOverseasApplicationVo> deptList = baseMapper.selectPendingApplicationsByRole(
query, deptHeadLevels, deptIdForFilter);
log.info("📋 [部门负责人] 查询到 {} 条本部门待审批申请", deptList.size());
for (RsOverseasApplicationVo vo : deptList) {
applicationMap.put(vo.getApplicationId(), vo);
}
}
// 5.2 查询其他角色的数据(全校,审批级别为其他级别)
if (hasOtherRole && !otherLevels.isEmpty()) {
List<RsOverseasApplicationVo> otherList = baseMapper.selectPendingApplicationsByRole(
query, otherLevels, null); // deptIdForFilter=null,不限制部门
log.info("📋 [其他角色] 查询到 {} 条全校待审批申请", otherList.size());
for (RsOverseasApplicationVo vo : otherList) {
applicationMap.put(vo.getApplicationId(), vo);
}
}
// 5.3 合并去重后的结果,按创建时间倒序排列
allList = new ArrayList<>(applicationMap.values());
allList.sort((a, b) -> {
if (a.getCreateTime() != null && b.getCreateTime() != null) {
return b.getCreateTime().compareTo(a.getCreateTime());
}
return 0;
});
log.info("✅ [合并结果] 去重后共 {} 条待审批申请", allList.size());
// 6. 手动分页处理
int pageNum = query.getPageNum();
int pageSize = query.getPageSize();
int total = allList.size();
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, total);
List<RsOverseasApplicationVo> pageList;
if (start >= total) {
pageList = new ArrayList<>();
} else {
pageList = allList.subList(start, end);
}
// 7. 构建分页对象
Page<RsOverseasApplicationVo> page = new Page<>(pageNum, pageSize);
page.setRecords(pageList);
page.setTotal(total);
log.info("📋 用户 {} 查询到 {} 条待审批申请,分页显示第 {} 页,共 {} 条",
currentUserName, total, pageNum, pageList.size());
return TableDataInfo.build(page);
}
/**
* 转换实体为VO对象
* 🔴 修复:使用Mapper查询完整的VO对象,包含字典翻译等扩展信息
@@ -514,7 +664,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 提交出国申请(动态审批人方案)
*
*
* 🆕 核心改进:
* 1. 不再设置固定的 current_approver_id(该字段改为冗余展示字段)
* 2. 仅设置 approval_level='1',审批人根据权限动态查询
@@ -599,7 +749,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
RsOverseasApproval approval = new RsOverseasApproval();
approval.setApplicationId(applicationId);
approval.setApproverId(approverId);
approval.setApproverName(approverName);
approval.setApproverName(resolveApproverDisplayName(approverId, approverName));
approval.setApprovalLevel(application.getApprovalLevel());
approval.setApprovalStatus(approvalStatus);
approval.setApprovalOpinion(approvalOpinion);
@@ -721,7 +871,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 修复已审批通过但未创建记录的申请
*
*
* @return 修复的记录数量
*/
@Override
@@ -950,6 +1100,16 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
entity.setStampedFileUploadTime(bo.getStampedFileUploadTime());
entity.setStampedFileUploadStatus(bo.getStampedFileUploadStatus());
// ==================== 党籍保留附件相关字段 ====================
entity.setPartyRetentionFilePath(bo.getPartyRetentionFilePath());
entity.setPartyRetentionFileName(bo.getPartyRetentionFileName());
entity.setPartyRetentionUploadTime(bo.getPartyRetentionUploadTime());
entity.setPartyRetentionUploadStatus(bo.getPartyRetentionUploadStatus());
entity.setPartyRetentionFileOssId(bo.getPartyRetentionFileOssId());
// ==================== 附件ID列表 ====================
entity.setAttachmentIds(bo.getAttachmentIds());
// ==================== 租户信息 ====================
// 🔴 关键修复:设置租户ID(如果Bo中有值则使用Bo的值,否则依赖MyBatis-Plus自动填充)
if (bo.getTenantId() != null) {
@@ -975,6 +1135,22 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
}
}
/**
* 解析审批人的展示名称:优先使用用户昵称,缺省时回退账号
*/
private String resolveApproverDisplayName(Long approverId, String fallbackName) {
String displayName = fallbackName;
try {
SysUserVo approver = userService.selectUserById(approverId);
if (approver != null && StringUtils.isNotBlank(approver.getNickName())) {
displayName = approver.getNickName();
}
} catch (Exception e) {
log.warn("⚠️ 获取审批人昵称失败, 使用账号作为展示名称. approverId={}, error={}", approverId, e.getMessage());
}
return displayName;
}
/**
* 批量删除出国申请
*/
@@ -1008,7 +1184,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
@Override
@Transactional
public Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId) {
java.util.Date uploadTime, String uploadStatus, String ossId) {
return baseMapper.update(null,
new LambdaUpdateWrapper<RsOverseasApplication>()
.eq(RsOverseasApplication::getApplicationId, applicationId)
@@ -1087,7 +1263,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据申请人ID获取部门审批人ID
*
*
* @param applicantId 申请人ID
* @return 部门审批人ID
*/
@@ -1134,7 +1310,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据申请人ID获取人事处审批人列表
*
*
* @param applicantId 申请人ID
* @return 人事处审批人ID列表
*/
@@ -1176,7 +1352,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据申请人查找对应的部门负责人审批人ID
*
*
* @param applicantId 申请人ID
* @return 部门负责人审批人ID
*/
@@ -1203,7 +1379,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据部门ID查找对应的部门负责人审批人
*
*
* @param deptId 部门ID
* @return 部门负责人审批人ID
*/
@@ -1251,7 +1427,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 查找父部门的负责人审批人
*
*
* @param deptId 当前部门ID
* @param approvers 所有部门负责人审批人列表
* @return 父部门负责人审批人ID
@@ -1288,7 +1464,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据审批级别获取下一级审批人ID
*
*
* @param approvalLevel 审批级别
* @return 下一级审批人ID
*/
@@ -1298,7 +1474,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据审批级别获取审批环节描述
*
*
* @param approvalLevel 审批级别
* @return 审批环节描述
*/
@@ -1321,14 +1497,14 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 🆕 根据当前登录用户的审批权限,获取其可审批的级别列表
*
*
* 权限到级别的映射:
* - rs:overseas:approve1 → 审批级别 "1" (部门负责人)
* - rs:overseas:approve2 → 审批级别 "2" (人事处)
* - rs:overseas:approve3 → 审批级别 "3" (纪委)
* - rs:overseas:approve4 → 审批级别 "4" (学院分管领导)
* - rs:overseas:approve5 → 审批级别 "5" (学院党委书记)
*
*
* @param userId 用户ID
* @return 可审批的级别列表,例如:["2", "3"]
*/
@@ -1371,7 +1547,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 🆕 根据审批级别获取对应的权限标识
*
*
* @param approvalLevel 审批级别(2-5)
* @return 权限标识,例如:"rs:overseas:approve2"
*/
@@ -1396,7 +1572,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 🔴 新增方法:验证审批人是否有指定级别的审批权限
*
*
* @param approverId 审批人ID
* @param approvalLevel 需要的审批级别(1-5)
* @return true-有权限,false-无权限
@@ -1423,4 +1599,4 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
return hasPermission;
}
}
}
@@ -311,6 +311,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
vo.setMobilePhone(application.getMobilePhone());
vo.setContactPhone(application.getContactPhone());
vo.setEmergencyContact(application.getEmergencyContact());
vo.setSignature(application.getSignature());
// 审批状态信息
vo.setStatus(application.getStatus());
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.rs.domain.RsOverseasItineraryChange;
@@ -13,14 +14,23 @@ import org.dromara.rs.domain.bo.RsOverseasItineraryChangeBo;
import org.dromara.rs.domain.query.RsOverseasItineraryChangeQuery;
import org.dromara.rs.domain.vo.RsOverseasItineraryChangeVo;
import org.dromara.rs.mapper.RsOverseasItineraryChangeMapper;
import org.dromara.rs.mapper.RsOverseasApplicationMapper;
import org.dromara.rs.mapper.RsOverseasRecordMapper;
import org.dromara.rs.domain.RsOverseasApplication;
import org.dromara.rs.domain.RsOverseasRecord;
import org.dromara.rs.domain.vo.RsOverseasRecordVo;
import org.dromara.rs.service.IRsOverseasItineraryChangeService;
import org.dromara.rs.service.IRsOverseasApplicationService;
import org.dromara.rs.service.IRsOverseasRecordService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* 出国行程变更申请Service业务层处理
@@ -35,14 +45,38 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
private final RsOverseasItineraryChangeMapper changeMapper;
private final IRsOverseasApplicationService applicationService;
private final RsOverseasApplicationMapper applicationMapper;
private final IRsOverseasRecordService recordService;
private final RsOverseasRecordMapper recordMapper;
/**
* 查询出国行程变更申请分页列表
*/
@Override
public TableDataInfo<RsOverseasItineraryChangeVo> queryPageList(RsOverseasItineraryChangeQuery query) {
// 查询所有符合条件的数据(不分页)
List<RsOverseasItineraryChangeVo> list = changeMapper.selectChangeList(query);
return TableDataInfo.build(list);
// 手动分页处理
int pageNum = query.getPageNum();
int pageSize = query.getPageSize();
int total = list.size();
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, total);
List<RsOverseasItineraryChangeVo> pageList;
if (start >= total) {
pageList = new ArrayList<>();
} else {
pageList = list.subList(start, end);
}
// 构建分页对象
Page<RsOverseasItineraryChangeVo> page = new Page<>(pageNum, pageSize);
page.setRecords(pageList);
page.setTotal(total);
return TableDataInfo.build(page);
}
/**
@@ -89,11 +123,12 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(RsOverseasItineraryChangeBo bo) {
// 查询出国申请信息
org.dromara.rs.domain.vo.RsOverseasApplicationVo application = applicationService.queryById(bo.getApplicationId());
org.dromara.rs.domain.vo.RsOverseasApplicationVo application = applicationService
.queryById(bo.getApplicationId());
if (application == null) {
throw new ServiceException("出国申请不存在");
}
// 检查申请状态是否为已通过
if (!"1".equals(application.getStatus())) {
throw new ServiceException("只能为已通过审批的出国申请提交变更申请");
@@ -102,8 +137,8 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
// 检查是否有未完成的变更申请
LambdaQueryWrapper<RsOverseasItineraryChange> wrapper = Wrappers.lambdaQuery();
wrapper.eq(RsOverseasItineraryChange::getApplicationId, bo.getApplicationId())
.in(RsOverseasItineraryChange::getStatus, Arrays.asList("0")) // 待审核
.eq(RsOverseasItineraryChange::getDeleted, 0);
.in(RsOverseasItineraryChange::getStatus, Arrays.asList("0")) // 待审核
.eq(RsOverseasItineraryChange::getDeleted, 0);
Long count = changeMapper.selectCount(wrapper);
if (count > 0) {
throw new ServiceException("该出国申请已有待审核的变更申请,请先处理");
@@ -117,11 +152,12 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
entity.setApplicantId(userId);
entity.setApplicantName(LoginHelper.getUsername());
entity.setApplicationId(bo.getApplicationId());
// 从出国申请中复制原始信息
entity.setOriginalDestination(application.getDestination());
entity.setOriginalDestinationCity(application.getDestinationCity());
entity.setOriginalExitReason(application.getExitReason() != null ? application.getExitReason() : application.getPurposeDetail());
entity.setOriginalExitReason(
application.getExitReason() != null ? application.getExitReason() : application.getPurposeDetail());
entity.setOriginalStartDate(application.getStartDate());
entity.setOriginalEndDate(application.getEndDate());
entity.setOriginalDuration(application.getDuration());
@@ -148,9 +184,10 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
throw new ServiceException("变更申请不存在");
}
// 只有待审核或已拒绝的申请可以修改
if (!"0".equals(original.getStatus()) && !"2".equals(original.getStatus())) {
throw new ServiceException("只有待审核或已拒绝的变更申请可以修改");
// 只有待审核、已拒绝或已撤回的申请可以修改
if (!"0".equals(original.getStatus()) && !"2".equals(original.getStatus())
&& !"3".equals(original.getStatus())) {
throw new ServiceException("只有待审核、已拒绝或已撤回的变更申请可以修改");
}
// 检查权限:只能修改自己的申请
@@ -161,7 +198,7 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
// 转换BO到Entity
RsOverseasItineraryChange entity = BeanUtil.toBean(bo, RsOverseasItineraryChange.class);
// 重新提交时,状态改为待审核
entity.setStatus("0");
entity.setApprovalOpinion(null);
@@ -179,24 +216,24 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
@Transactional(rollbackFor = Exception.class)
public Boolean deleteByIds(List<Long> changeIds) {
Long userId = LoginHelper.getUserId();
for (Long changeId : changeIds) {
RsOverseasItineraryChange change = changeMapper.selectById(changeId);
if (change == null) {
continue;
}
// 检查权限:只能删除自己的申请
if (!userId.equals(change.getApplicantId())) {
throw new ServiceException("只能删除自己的变更申请");
}
// 只有待审核或已拒绝的申请可以删除
if (!"0".equals(change.getStatus()) && !"2".equals(change.getStatus())) {
throw new ServiceException("只有待审核或已拒绝的变更申请可以删除");
}
}
return changeMapper.deleteBatchIds(changeIds) > 0;
}
@@ -229,6 +266,12 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
/**
* 审批变更申请
*
* 🔴 审批通过后,需要更新原申请单和记录的数据:
* 1. 前往国家 → 更新到 rs_overseas_application 和 rs_overseas_record
* 2. 实际出行开始时间、结束时间 → 更新到 rs_overseas_application 和 rs_overseas_record
* 3. 行程安排 → 更新到 rs_overseas_application 和 rs_overseas_record
* 4. 出境事由 → 追加到 rs_overseas_application 的备注字段
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -249,9 +292,139 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
change.setApprovalOpinion(opinion);
change.setApprovalTime(new Date());
change.setApproverId(userId);
change.setApproverName(LoginHelper.getUsername());
String displayName = StringUtils.defaultIfBlank(LoginHelper.getNickname(), LoginHelper.getUsername());
change.setApproverName(displayName);
return changeMapper.updateById(change) > 0;
// 更新变更申请状态
boolean updateChangeResult = changeMapper.updateById(change) > 0;
if (!updateChangeResult) {
throw new ServiceException("更新变更申请状态失败");
}
// 🔴 如果审批通过,需要更新原申请单和记录的数据
if (approved && change.getApplicationId() != null) {
log.info("🔧 [审批通过] 开始更新原申请单和记录数据,变更ID: {}, 申请ID: {}", changeId, change.getApplicationId());
try {
// 1. 更新 rs_overseas_application 表
RsOverseasApplication application = applicationMapper.selectById(change.getApplicationId());
if (application != null) {
LambdaUpdateWrapper<RsOverseasApplication> appUpdateWrapper = new LambdaUpdateWrapper<>();
appUpdateWrapper.eq(RsOverseasApplication::getApplicationId, change.getApplicationId());
// 更新前往国家(destination 和 destinationCountry 同步更新)
if (change.getNewDestination() != null && !change.getNewDestination().trim().isEmpty()) {
appUpdateWrapper.set(RsOverseasApplication::getDestination, change.getNewDestination());
appUpdateWrapper.set(RsOverseasApplication::getDestinationCountry, change.getNewDestination());
log.info("✅ [更新申请单] 前往国家: {} (同步更新 destination 和 destinationCountry)",
change.getNewDestination());
}
// 更新出发日期和返回日期
if (change.getNewStartDate() != null) {
appUpdateWrapper.set(RsOverseasApplication::getStartDate, change.getNewStartDate());
log.info("✅ [更新申请单] 出发日期: {}", change.getNewStartDate());
}
if (change.getNewEndDate() != null) {
appUpdateWrapper.set(RsOverseasApplication::getEndDate, change.getNewEndDate());
log.info("✅ [更新申请单] 返回日期: {}", change.getNewEndDate());
// 重新计算出国天数
if (change.getNewStartDate() != null && change.getNewEndDate() != null) {
long diffInMillies = change.getNewEndDate().getTime() - change.getNewStartDate().getTime();
int days = (int) (diffInMillies / (1000 * 60 * 60 * 24)) + 1;
appUpdateWrapper.set(RsOverseasApplication::getDuration, days);
log.info("✅ [更新申请单] 出国天数: {} 天", days);
}
}
// 更新行程安排
if (change.getNewItinerary() != null && !change.getNewItinerary().trim().isEmpty()) {
appUpdateWrapper.set(RsOverseasApplication::getItinerary, change.getNewItinerary());
log.info("✅ [更新申请单] 行程安排已更新");
}
// 追加出境事由到备注字段
if (change.getNewExitReason() != null && !change.getNewExitReason().trim().isEmpty()) {
String currentRemarks = application.getRemarks() != null ? application.getRemarks() : "";
String newRemarks = currentRemarks.isEmpty()
? "行程变更-出境事由: " + change.getNewExitReason()
: currentRemarks + "\n行程变更-出境事由: " + change.getNewExitReason();
appUpdateWrapper.set(RsOverseasApplication::getRemarks, newRemarks);
log.info("✅ [更新申请单] 备注已追加出境事由: {}", change.getNewExitReason());
}
boolean appUpdateResult = applicationMapper.update(null, appUpdateWrapper) > 0;
if (!appUpdateResult) {
log.warn("⚠️ [更新申请单] 更新失败,申请ID: {}", change.getApplicationId());
} else {
log.info("✅ [更新申请单] 更新成功,申请ID: {}", change.getApplicationId());
}
} else {
log.warn("⚠️ [更新申请单] 申请单不存在,申请ID: {}", change.getApplicationId());
}
// 2. 更新 rs_overseas_record 表
RsOverseasRecordVo recordVo = recordMapper
.selectOverseasRecordByApplicationId(change.getApplicationId());
if (recordVo != null) {
RsOverseasRecord record = recordMapper.selectById(recordVo.getRecordId());
if (record != null) {
LambdaUpdateWrapper<RsOverseasRecord> recordUpdateWrapper = new LambdaUpdateWrapper<>();
recordUpdateWrapper.eq(RsOverseasRecord::getRecordId, record.getRecordId());
// 更新前往国家
if (change.getNewDestination() != null && !change.getNewDestination().trim().isEmpty()) {
recordUpdateWrapper.set(RsOverseasRecord::getDestination, change.getNewDestination());
log.info("✅ [更新记录] 前往国家: {}", change.getNewDestination());
}
// 更新实际开始日期和结束日期
if (change.getNewStartDate() != null) {
recordUpdateWrapper.set(RsOverseasRecord::getActualStartDate, change.getNewStartDate());
log.info("✅ [更新记录] 实际开始日期: {}", change.getNewStartDate());
}
if (change.getNewEndDate() != null) {
recordUpdateWrapper.set(RsOverseasRecord::getActualEndDate, change.getNewEndDate());
log.info("✅ [更新记录] 实际结束日期: {}", change.getNewEndDate());
// 重新计算实际出国天数
if (change.getNewStartDate() != null && change.getNewEndDate() != null) {
long diffInMillies = change.getNewEndDate().getTime()
- change.getNewStartDate().getTime();
int days = (int) (diffInMillies / (1000 * 60 * 60 * 24)) + 1;
recordUpdateWrapper.set(RsOverseasRecord::getActualDuration, days);
log.info("✅ [更新记录] 实际出国天数: {} 天", days);
}
}
// 更新行程安排
if (change.getNewItinerary() != null && !change.getNewItinerary().trim().isEmpty()) {
recordUpdateWrapper.set(RsOverseasRecord::getItinerary, change.getNewItinerary());
log.info("✅ [更新记录] 行程安排已更新");
}
boolean recordUpdateResult = recordMapper.update(null, recordUpdateWrapper) > 0;
if (!recordUpdateResult) {
log.warn("⚠️ [更新记录] 更新失败,记录ID: {}", record.getRecordId());
} else {
log.info("✅ [更新记录] 更新成功,记录ID: {}", record.getRecordId());
}
} else {
log.warn("⚠️ [更新记录] 记录不存在,记录ID: {}", recordVo.getRecordId());
}
} else {
log.warn("⚠️ [更新记录] 未找到关联记录,申请ID: {}", change.getApplicationId());
}
log.info("✅ [审批通过] 数据更新完成,变更ID: {}", changeId);
} catch (Exception e) {
log.error("❌ [审批通过] 更新原申请单和记录数据失败,变更ID: {}, 错误: {}", changeId, e.getMessage(), e);
throw new ServiceException("审批通过后更新原申请单和记录数据失败: " + e.getMessage());
}
}
return true;
}
/**
@@ -262,4 +435,3 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
return changeMapper.selectChangeListByRecordId(recordId);
}
}
@@ -617,5 +617,42 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
ORDER BY a.apply_time DESC, a.create_time DESC
</select>
<!-- 🔴 新增:根据角色查询待审批申请列表(支持分页和搜索) -->
<select id="selectPendingApplicationsByRole" resultMap="RsOverseasApplicationVoResult">
<include refid="selectOverseasApplicationVo"/>
<where>
oa.deleted = '0'
AND oa.status = '0'
<!-- 审批级别过滤 -->
<if test="approvalLevels != null and approvalLevels.size() > 0">
AND oa.approval_level IN
<foreach collection="approvalLevels" item="level" open="(" separator="," close=")">
#{level}
</foreach>
</if>
<!-- 部门过滤(仅对部门负责人生效) -->
<if test="deptId != null">
AND EXISTS (
SELECT 1 FROM sys_user u
WHERE u.user_id = oa.applicant_id
AND u.dept_id = #{deptId}
)
</if>
<!-- 搜索条件:前往国家 -->
<if test="query != null and query.destination != null and query.destination != ''">
AND oa.destination LIKE CONCAT('%', #{query.destination}, '%')
</if>
<!-- 搜索条件:出境事由 -->
<if test="query != null and query.exitReason != null and query.exitReason != ''">
AND oa.exit_reason = #{query.exitReason}
</if>
<!-- 租户过滤 -->
<if test="query != null and query.tenantId != null and query.tenantId != ''">
AND oa.tenant_id = #{query.tenantId}
</if>
</where>
ORDER BY oa.create_time DESC
</select>
</mapper>
@@ -74,8 +74,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectOverseasRecordDetailVo">
select
ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department,
ors.position,
COALESCE(sp.post_name, ors.position) as position_text,
oa.position,
oa.position as position_text,
ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city,
ors.actual_start_date, ors.actual_end_date, ors.actual_duration, ors.actual_budget,
ors.invitation, ors.itinerary, ors.accommodation_plan, ors.transportation_plan,
@@ -134,7 +134,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
'record' as data_source
from rs_overseas_record ors
left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0'
left join sys_post sp on (ors.position = CAST(sp.post_id AS CHAR) or ors.position = sp.post_code) and sp.status = '0'
</sql>
<!-- 联合查询:出国记录 + 已完成审批的申请 -->
@@ -142,8 +141,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-- 查询已创建的出国记录(关联申请表获取扩展字段)
select
ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department,
ors.position,
COALESCE(sp1.post_name, ors.position) as position_text,
oa.position,
oa.position as position_text,
ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city,
ors.actual_start_date as actual_start_date, ors.actual_end_date as actual_end_date, ors.actual_duration as actual_duration,
ors.actual_budget as actual_budget,
@@ -212,7 +211,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
'record' as data_source
from rs_overseas_record ors
left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0'
left join sys_post sp1 on (ors.position = CAST(sp1.post_id AS CHAR) or ors.position = sp1.post_code) and sp1.status = '0'
where ors.deleted = '0'
UNION ALL
@@ -222,7 +220,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
NULL as record_id,
oa.application_id, oa.applicant_id, oa.applicant_name, oa.department,
oa.position,
COALESCE(sp2.post_name, oa.position) as position_text,
oa.position as position_text,
oa.contact_phone, oa.contact_email, oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city,
oa.start_date as actual_start_date, oa.end_date as actual_end_date, oa.duration as actual_duration,
NULL as actual_budget,
@@ -284,7 +282,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
'进行中' as completion_status_text,
'approved_application' as data_source
from rs_overseas_application oa
left join sys_post sp2 on (oa.position = CAST(sp2.post_id AS CHAR) or oa.position = sp2.post_code) and sp2.status = '0'
where oa.deleted = '0'
and oa.status = '1'
and not exists (
@@ -1,142 +0,0 @@
-- 招聘模块表结构
-- 创建于 2025-07-02
-- 职位表
DROP TABLE IF EXISTS `rs_job`;
CREATE TABLE `rs_job` (
`job_id` bigint(20) NOT NULL COMMENT '职位ID',
`title` varchar(100) NOT NULL COMMENT '职位名称',
`department` varchar(50) NOT NULL COMMENT '部门',
`salary_range` varchar(50) DEFAULT NULL COMMENT '薪资范围',
`location` varchar(100) DEFAULT NULL COMMENT '工作地点',
`description` text COMMENT '职位描述',
`requirements` text COMMENT '任职要求',
`is_active` char(1) DEFAULT '0' COMMENT '是否在招(0是 1否)',
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
`view_count` int(11) DEFAULT '0' COMMENT '浏览次数',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`job_id`),
KEY `idx_department` (`department`),
KEY `idx_is_active` (`is_active`)
) ENGINE=InnoDB COMMENT='招聘职位表';
-- 应聘者表
DROP TABLE IF EXISTS `rs_applicant`;
CREATE TABLE `rs_applicant` (
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '关联用户ID',
`name` varchar(50) NOT NULL COMMENT '姓名',
`phone` varchar(20) NOT NULL COMMENT '手机号码',
`email` varchar(100) NOT NULL COMMENT '电子邮箱',
`resume_file_path` varchar(255) DEFAULT NULL COMMENT '简历文件路径',
`self_intro` text COMMENT '自我介绍',
`current_status` varchar(20) DEFAULT 'active' COMMENT '当前状态(active-可申请 hired-已录用 rejected-已拒绝)',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`applicant_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB COMMENT='应聘者信息表';
-- 教育经历表
DROP TABLE IF EXISTS `rs_education`;
CREATE TABLE `rs_education` (
`edu_id` bigint(20) NOT NULL COMMENT '教育经历ID',
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`school_name` varchar(100) NOT NULL COMMENT '学校名称',
`major` varchar(100) DEFAULT NULL COMMENT '专业',
`degree` varchar(50) DEFAULT NULL COMMENT '学位',
`start_date` date DEFAULT NULL COMMENT '开始日期',
`end_date` date DEFAULT NULL COMMENT '结束日期',
`gpa` varchar(20) DEFAULT NULL COMMENT '绩点',
`description` text COMMENT '描述',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`edu_id`),
KEY `idx_applicant_id` (`applicant_id`)
) ENGINE=InnoDB COMMENT='教育经历表';
-- 工作经历表
DROP TABLE IF EXISTS `rs_work_experience`;
CREATE TABLE `rs_work_experience` (
`exp_id` bigint(20) NOT NULL COMMENT '工作经历ID',
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`company_name` varchar(100) NOT NULL COMMENT '公司名称',
`position` varchar(100) NOT NULL COMMENT '职位',
`start_date` date DEFAULT NULL COMMENT '开始日期',
`end_date` date DEFAULT NULL COMMENT '结束日期',
`is_current` char(1) DEFAULT '0' COMMENT '是否当前工作(0是 1否)',
`description` text COMMENT '工作描述',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`exp_id`),
KEY `idx_applicant_id` (`applicant_id`)
) ENGINE=InnoDB COMMENT='工作经历表';
-- 申请记录表
DROP TABLE IF EXISTS `rs_application`;
CREATE TABLE `rs_application` (
`application_id` bigint(20) NOT NULL COMMENT '申请ID',
`job_id` bigint(20) NOT NULL COMMENT '职位ID',
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`apply_time` datetime NOT NULL COMMENT '申请时间',
`status` varchar(20) DEFAULT 'pending' COMMENT '状态(pending-待处理 interviewed-已面试 offered-已录用 rejected-已拒绝)',
`hr_notes` text COMMENT 'HR备注',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`application_id`),
KEY `idx_job_id` (`job_id`),
KEY `idx_applicant_id` (`applicant_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB COMMENT='申请记录表';
-- 附件表
DROP TABLE IF EXISTS `rs_attachment`;
CREATE TABLE `rs_attachment` (
`attachment_id` bigint(20) NOT NULL COMMENT '附件ID',
`application_id` bigint(20) NOT NULL COMMENT '申请ID',
`file_name` varchar(100) NOT NULL COMMENT '文件名',
`file_path` varchar(255) NOT NULL COMMENT '文件路径',
`file_type` varchar(50) DEFAULT NULL COMMENT '文件类型',
`file_size` bigint(20) DEFAULT NULL COMMENT '文件大小(字节)',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`attachment_id`),
KEY `idx_application_id` (`application_id`)
) ENGINE=InnoDB COMMENT='申请附件表';
-- 插入初始测试数据
INSERT INTO `rs_job` (`job_id`, `title`, `department`, `salary_range`, `location`, `description`, `requirements`, `is_active`, `create_time`, `create_by`, `update_time`, `update_by`, `remark`, `deleted`, `tenant_id`)
VALUES
(1, '高级Java开发工程师', '教学', '15k-25k', '杭州', '负责校园教学管理系统的开发和维护,参与核心模块设计与实现,优化系统性能,解决技术难题。\n作为团队核心成员,参与技术方案评审,指导初级开发人员,确保项目按时高质量交付。', '计算机相关专业本科及以上学历,3年以上Java开发经验\n精通Java核心技术,熟悉Spring Boot、MyBatis等主流框架\n熟练掌握MySQL数据库设计与优化\n有教育行业相关系统开发经验者优先\n良好的沟通能力和团队协作精神', '0', NOW(), 1, NOW(), 1, '示例职位', '0', '000000'),
(2, '前端工程师', '教学', '12k-18k', '杭州', '负责校园教学管理系统前端开发,使用Vue3+TypeScript技术栈,参与UI设计与实现', '计算机相关专业本科及以上学历,2年以上前端开发经验\n精通HTML5/CSS3/JavaScript,熟悉Vue3/React等主流框架\n熟练使用TypeScript进行开发\n有教育行业相关系统开发经验者优先', '0', NOW(), 1, NOW(), 1, '示例职位', '0', '000000'),
(3, '行政助理', '行政', '5k-8k', '杭州', '协助部门主管处理日常行政事务,包括文件整理、会议安排、接待来访等工作', '大专及以上学历,行政管理相关专业优先\n熟悉办公软件,有较强的文字处理能力\n工作认真负责,有较强的组织协调能力\n有相关工作经验者优先', '0', NOW(), 1, NOW(), 1, '示例职位', '0', '000000');
@@ -1,11 +0,0 @@
-- -----------------------------
-- 删除培训申请表中的提交次数字段
-- -----------------------------
-- 删除 rs_training_application 表中的 submit_count 字段
-- 注意:MySQL 5.7及以下版本不支持 DROP COLUMN IF EXISTS 语法
-- 需要先检查字段是否存在,然后删除
ALTER TABLE rs_training_application DROP COLUMN submit_count;
-- 验证字段已删除
DESCRIBE rs_training_application;
@@ -1,62 +0,0 @@
-- -----------------------------
-- 培训审批数据一致性检查和修复
-- -----------------------------
-- 检查审批记录表是否为空
SELECT COUNT(*) AS approval_count FROM rs_training_approval WHERE deleted = '0';
-- 检查审批记录与申请记录是否一致
SELECT
COUNT(*) AS inconsistent_count
FROM
rs_training_approval a
LEFT JOIN
rs_training_application app ON a.application_id = app.application_id
WHERE
a.deleted = '0'
AND (app.application_id IS NULL OR app.deleted = '1');
-- 插入测试数据(如果没有审批记录)
INSERT INTO rs_training_approval (
approval_id, application_id, approver_id, approver_name,
approval_level, status, opinion, approval_time, tenant_id,
create_by, create_time, deleted
)
SELECT
FLOOR(RAND() * 1000000) + 100000, -- 随机审批ID
app.application_id, -- 使用真实申请ID
app.create_by, -- 使用申请的创建者ID作为审批人ID
(SELECT user_name FROM sys_user WHERE user_id = app.create_by LIMIT 1), -- 获取用户名
1, -- 审批级别
app.status, -- 使用申请状态
'系统自动生成的测试审批记录', -- 审批意见
app.update_time, -- 使用申请更新时间作为审批时间
app.tenant_id, -- 租户ID
app.create_by, -- 创建者
NOW(), -- 创建时间
'0' -- 未删除
FROM
rs_training_application app
WHERE
app.deleted = '0'
AND app.status IN ('1', '2') -- 已通过或已拒绝
AND NOT EXISTS (
SELECT 1
FROM rs_training_approval a
WHERE a.application_id = app.application_id AND a.deleted = '0'
)
LIMIT 5; -- 限制生成5条测试数据
-- 查询生成的测试数据
SELECT
a.approval_id, a.application_id, a.approver_name, a.status, a.opinion, a.approval_time,
app.user_name AS applicant_name, app.reason
FROM
rs_training_approval a
JOIN
rs_training_application app ON a.application_id = app.application_id
WHERE
a.deleted = '0'
AND a.create_time > DATE_SUB(NOW(), INTERVAL 1 HOUR) -- 最近一小时创建的记录
ORDER BY
a.create_time DESC;
@@ -1,38 +0,0 @@
-- ----------------------------
-- 培训管理模块测试数据
-- ----------------------------
-- ----------------------------
-- 培训课程表测试数据
-- ----------------------------
INSERT INTO `rs_training_course` (`course_id`, `title`, `description`, `instructor`, `duration`, `location`, `capacity`, `status`, `start_time`, `end_time`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `deleted`)
VALUES
(10001, '新入职教师培训', '针对新入职教师的岗前培训,包括教学理念、师德师风、教学方法等内容', '张教授', 16, '学术会议厅', 50, '1', '2025-08-01 09:00:00', '2025-08-03 17:00:00', '000000', 1, NOW(), 1, NOW(), '重要培训', '0'),
(10002, '教育技术与信息化', '介绍现代教育技术在教学中的应用,包括多媒体教学、在线教育平台等', '李博士', 12, '多媒体教室', 30, '1', '2025-08-10 09:00:00', '2025-08-12 17:00:00', '000000', 1, NOW(), 1, NOW(), '技术培训', '0'),
(10003, '教学方法与技能培训', '提升教师教学技能,包括课堂管理、学生互动、教学评估等', '王老师', 8, '实训楼', 40, '1', '2025-08-05 14:00:00', '2025-08-05 18:00:00', '000000', 1, NOW(), 1, NOW(), '实用培训', '0'),
(10004, '学生管理与心理健康', '帮助教师了解学生心理,掌握学生管理技巧和心理疏导方法', '陈心理师', 6, '学术报告厅', 60, '1', '2025-08-15 14:00:00', '2025-08-15 18:00:00', '000000', 1, NOW(), 1, NOW(), '心理培训', '0'),
(10005, '科研方法与学术写作', '指导教师进行科学研究,提升学术写作和论文发表能力', '赵教授', 20, '图书馆报告厅', 25, '1', '2025-08-25 09:00:00', '2025-08-30 17:00:00', '000000', 1, NOW(), 1, NOW(), '科研培训', '0');
-- ----------------------------
-- 培训申请表测试数据
-- ----------------------------
INSERT INTO `rs_training_application` (`application_id`, `course_id`, `user_id`, `user_name`, `department`, `position`, `reason`, `status`, `apply_time`, `current_approver`, `process_instance_id`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `deleted`)
VALUES
(20001, 10001, 2, '张老师', '教务处', '教师', '新入职需要培训', '1', '2025-07-25 12:03:49', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), NULL, '0'),
(20002, 10002, 3, '王老师', '信息技术中心', '工程师', '学习新技术', '1', '2025-08-01 12:37:17', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '未完成', '0'),
(20003, 10003, 4, '刘老师', '计算机学院', '讲师', '提升教学技能', '1', '2025-07-25 12:04:06', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '培训尚未结束', '0'),
(20004, 10004, 5, '李老师', '学生处', '辅导员', '提升学生管理能力', '1', '2025-08-06 14:24:37', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '未完成', '0'),
(20005, 10005, 6, '陈老师', '科研处', '副教授', '提升科研能力', '1', '2025-08-22 15:25:51', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '未完成', '0');
-- ----------------------------
-- 培训审批表测试数据
-- ----------------------------
INSERT INTO `rs_training_approval` (`approval_id`, `application_id`, `approver_id`, `approver_name`, `approval_level`, `status`, `opinion`, `approval_time`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `deleted`)
VALUES
(30001, 20001, 101, '王主任', 1, '1', '同意参加,安排好工作交接', '2025-07-21 09:30:00', '000000', 101, NOW(), 101, NOW(), NULL, '0'),
(30002, 20003, 102, '孙院长', 1, '1', '批准参加,对提升教学质量有帮助', '2025-07-23 10:15:00', '000000', 102, NOW(), 102, NOW(), NULL, '0'),
(30003, 20004, 103, '周主任', 1, '2', '近期有重要工作交接,建议下次再参加', '2025-07-24 14:20:00', '000000', 103, NOW(), 103, NOW(), NULL, '0');
-- ----------------------------
-- 培训记录表测试数据已删除,请通过申请界面新增
-- ----------------------------
@@ -1,43 +0,0 @@
-- -----------------------------
-- 培训审批数据修复脚本
-- -----------------------------
-- 检查comment列是否存在,如果存在则迁移数据到opinion列
SET @columnExists = 0;
SELECT COUNT(*) INTO @columnExists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'rs_training_approval'
AND COLUMN_NAME = 'comment';
-- 如果comment列存在,将数据从comment迁移到opinion
SET @sqlStmt = IF(@columnExists > 0,
'UPDATE rs_training_approval SET opinion = comment WHERE opinion IS NULL AND comment IS NOT NULL',
'SELECT "comment列不存在,无需迁移" AS message');
PREPARE stmt FROM @sqlStmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 添加测试数据(如果表为空)
INSERT INTO rs_training_approval (
approval_id, application_id, approver_id, approver_name,
approval_level, status, opinion, approval_time, tenant_id,
create_by, create_time, deleted
)
SELECT
FLOOR(RAND() * 1000000) + 100000, -- 随机ID
app.application_id, -- 关联到真实的申请ID
1, -- 管理员ID
'系统管理员', -- 管理员名称
1, -- 第一级审批
'1', -- 已通过状态
'系统自动生成的测试审批记录', -- 审批意见
NOW(), -- 当前时间
app.tenant_id, -- 使用申请的租户ID
app.create_by, -- 使用申请的创建者
NOW(), -- 当前时间
'0' -- 未删除
FROM rs_training_application app
WHERE NOT EXISTS (SELECT 1 FROM rs_training_approval)
LIMIT 1;
@@ -1,58 +0,0 @@
-- -----------------------------
-- 培训审批历史记录完整性更新脚本
-- -----------------------------
-- 1. 在培训申请表中添加提交次数字段
ALTER TABLE rs_training_application ADD COLUMN IF NOT EXISTS submit_count int DEFAULT 1 COMMENT '提交次数';
-- 2. 检查并移除审批表可能存在的唯一约束(允许一个申请有多条审批记录)
ALTER TABLE rs_training_approval DROP INDEX IF EXISTS uk_application_approver;
ALTER TABLE rs_training_approval DROP INDEX IF EXISTS uk_application_level;
-- 3. 确保审批表中有足够的索引来高效查询
CREATE INDEX IF NOT EXISTS idx_training_approval_application ON rs_training_approval(application_id);
CREATE INDEX IF NOT EXISTS idx_training_approval_status ON rs_training_approval(status);
CREATE INDEX IF NOT EXISTS idx_training_approval_time ON rs_training_approval(approval_time);
-- 4. 添加审批记录表注释说明
ALTER TABLE rs_training_approval COMMENT = '培训审批表(记录所有审批动作,包括通过和拒绝记录)';
-- 5. 更新已有数据的提交次数为1(如果有)
UPDATE rs_training_application SET submit_count = 1 WHERE submit_count IS NULL;
-- 6. 检查并修复可能的数据不一致问题
-- 查找有申请状态为已拒绝但没有对应拒绝记录的数据
SELECT
app.application_id,
app.course_id,
app.user_name,
app.status,
COUNT(appr.approval_id) as approval_count
FROM
rs_training_application app
LEFT JOIN
rs_training_approval appr ON app.application_id = appr.application_id
WHERE
app.status = '2' AND app.deleted = '0'
GROUP BY
app.application_id
HAVING
approval_count = 0;
-- 查找有申请状态为已通过但没有对应通过记录的数据
SELECT
app.application_id,
app.course_id,
app.user_name,
app.status,
COUNT(appr.approval_id) as approval_count
FROM
rs_training_application app
LEFT JOIN
rs_training_approval appr ON app.application_id = appr.application_id
WHERE
app.status = '1' AND app.deleted = '0'
GROUP BY
app.application_id
HAVING
approval_count = 0;
@@ -1,19 +0,0 @@
-- -----------------------------
-- 培训审批历史记录更新
-- -----------------------------
-- 确保审批表中没有唯一约束,允许一个申请有多条审批记录
ALTER TABLE rs_training_approval DROP INDEX IF EXISTS uk_application_approver;
-- 添加审批意见列(如果不存在)
ALTER TABLE rs_training_approval ADD COLUMN IF NOT EXISTS opinion varchar(500) COMMENT '审批意见' AFTER status;
-- 添加comment列的别名以保持向后兼容(如果存在)
ALTER TABLE rs_training_approval CHANGE COLUMN comment opinion varchar(500) COMMENT '审批意见';
-- 更新审批记录表注释
ALTER TABLE rs_training_approval COMMENT = '培训审批表(记录所有审批动作,包括拒绝记录)';
-- 添加相关索引以提高查询效率
CREATE INDEX IF NOT EXISTS idx_training_approval_application ON rs_training_approval(application_id);
CREATE INDEX IF NOT EXISTS idx_training_approval_status ON rs_training_approval(status);
@@ -1,17 +0,0 @@
-- 更新培训课程表,添加create_dept字段
ALTER TABLE rs_training_course ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
-- 更新培训申请表,添加create_dept字段
ALTER TABLE rs_training_application ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
-- 更新培训申请表,添加location、duration、startTime和endTime字段
ALTER TABLE rs_training_application ADD COLUMN location varchar(200) COMMENT '培训地点' AFTER current_approver;
ALTER TABLE rs_training_application ADD COLUMN duration int(11) COMMENT '学时' AFTER location;
ALTER TABLE rs_training_application ADD COLUMN start_time datetime COMMENT '开始时间' AFTER duration;
ALTER TABLE rs_training_application ADD COLUMN end_time datetime COMMENT '结束时间' AFTER start_time;
-- 更新培训审批表,添加create_dept字段
ALTER TABLE rs_training_approval ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
-- 更新培训记录表,添加create_dept字段
ALTER TABLE rs_training_record ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
@@ -1,122 +0,0 @@
-- ----------------------------
-- 培训管理模块数据库表
-- ----------------------------
-- ----------------------------
-- 培训课程表
-- ----------------------------
drop table if exists rs_training_course;
create table rs_training_course (
course_id bigint(20) not null comment '课程ID',
title varchar(100) not null comment '课程名称',
category varchar(50) not null comment '课程类别',
description text comment '课程描述',
start_time datetime not null comment '开始时间',
end_time datetime not null comment '结束时间',
duration int(11) not null comment '学时数',
location varchar(200) comment '培训地点',
max_participants int(11) default 0 comment '最大参与人数',
current_participants int(11) default 0 comment '当前报名人数',
status char(1) default '0' comment '状态(0正常 1停用)',
tenant_id varchar(20) default '000000' comment '租户编号',
create_by bigint(20) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (course_id)
) engine=innodb comment = '培训课程表';
-- ----------------------------
-- 培训申请表
-- ----------------------------
drop table if exists rs_training_application;
create table rs_training_application (
application_id bigint(20) not null comment '申请ID',
course_name varchar(100) not null comment '培训名称',
user_id bigint(20) not null comment '申请人ID',
user_name varchar(50) not null comment '申请人姓名',
department varchar(50) not null comment '所属部门',
position varchar(50) comment '职位',
reason text comment '申请理由',
status char(1) default '0' comment '状态(0待审核 1已通过 2已拒绝 3进行中 4已完成)',
apply_time datetime comment '申请时间',
current_approver varchar(50) comment '当前审批人',
location varchar(200) comment '培训地点',
duration int(11) comment '学时',
start_time datetime comment '开始时间',
end_time datetime comment '结束时间',
tenant_id varchar(20) default '000000' comment '租户编号',
create_dept bigint(20) comment '创建部门',
reject_time datetime comment '拒绝时间',
create_by bigint(20) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (application_id)
) engine=innodb comment = '培训申请表';
-- ----------------------------
-- 培训审批表
-- ----------------------------
drop table if exists rs_training_approval;
create table rs_training_approval (
approval_id bigint(20) not null comment '审批ID',
application_id bigint(20) not null comment '申请ID',
approver_id bigint(20) not null comment '审批人ID',
approver_name varchar(50) not null comment '审批人姓名',
approval_level int(11) not null comment '审批级别(1部门主管 2分管领导 3校领导)',
status char(1) default '0' comment '状态(0待审核 1已通过 2已拒绝)',
opinion varchar(500) comment '审批意见',
approval_time datetime comment '审批时间',
tenant_id varchar(20) default '000000' comment '租户编号',
create_by bigint(20) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (approval_id)
) engine=innodb comment = '培训审批表';
-- ----------------------------
-- 培训记录表
-- ----------------------------
drop table if exists rs_training_record;
create table rs_training_record (
record_id bigint(20) not null comment '记录ID',
application_id bigint(20) not null comment '申请ID',
course_id bigint(20) not null comment '课程ID',
user_id bigint(20) not null comment '用户ID',
user_name varchar(50) not null comment '用户姓名',
completion_status char(1) default '0' comment '完成状态(0进行中 1已完成 2未完成)',
completion_time datetime comment '完成时间',
score decimal(5,2) comment '成绩',
certificate_path varchar(255) comment '证书路径',
certificate_number varchar(50) comment '证书编号',
attachment_ids varchar(255) comment '证书附件ID(多个用逗号分隔)',
feedback text comment '培训反馈',
location varchar(255) comment '培训地点',
duration int(11) comment '学时',
apply_time datetime comment '申请时间',
status varchar(10) comment '申请状态',
course_name varchar(255) comment '课程名称',
department varchar(255) comment '所属部门',
position varchar(255) comment '职位',
reason varchar(255) comment '申请理由',
current_approver varchar(255) comment '当前审批人',
start_time datetime comment '开始时间',
end_time datetime comment '结束时间',
tenant_id varchar(20) default '000000' comment '租户编号',
create_dept bigint(20) comment '创建部门',
create_by bigint(20) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (record_id)
) engine=innodb comment = '培训记录表';
@@ -1,5 +0,0 @@
-- 添加自定义课程名称字段
-- 创建于 2025-07-24
-- 在rs_training_application表中添加course_name列,用于存储自定义培训名称
ALTER TABLE `rs_training_application` ADD COLUMN `course_name` varchar(100) DEFAULT NULL COMMENT '自定义课程名称' AFTER `course_id`;
@@ -1,7 +0,0 @@
-- 为rs_application表添加附件相关字段
ALTER TABLE `rs_application`
ADD COLUMN `attachment_path` varchar(255) DEFAULT NULL COMMENT '附件路径' AFTER `self_description`,
ADD COLUMN `attachment_name` varchar(255) DEFAULT NULL COMMENT '附件名称' AFTER `attachment_path`;
-- 更新表注释
ALTER TABLE `rs_application` COMMENT '应聘申请记录表';
@@ -0,0 +1,15 @@
-- =============================================
-- 修改 rs_overseas_application 表结构
-- 将 party_retention_file_oss_id 字段从 bigint 改为 varchar(500)
-- 以支持存储多个附件OSS ID(逗号分隔)
-- 创建时间: 2025-11-27
-- =============================================
-- 修改字段类型(MySQL/MariaDB)
ALTER TABLE rs_overseas_application
MODIFY COLUMN party_retention_file_oss_id VARCHAR(500) COMMENT '党籍保留附件OSS ID(多个ID用逗号分隔)';
-- 注意:
-- 1. 此脚本会保留现有数据,bigint值会自动转换为字符串
-- 2. 建议在执行前备份数据库
-- 3. 如果字段当前有NOT NULL约束,会被保留
+21 -2
View File
@@ -316,7 +316,7 @@ interface OverseasApprovalVO {
approvalId: number | string; // 审批ID
applicationId: number; // 关联申请ID
approverId: number; // 审批人ID
approverName: string; // 审批人姓名
approverName: string; // 审批人姓名(优先返回昵称,缺省时回退账号)
approvalLevel: string; // 审批级别编码
approvalStatus: string; // 审批状态编码
approvalOpinion: string; // 审批意见
@@ -329,6 +329,8 @@ interface OverseasApprovalVO {
}
```
> 🆕 **2025-11-28 更新**:`approverName` 字段统一返回审批人的昵称,若用户未设置昵称则自动回退到账号,确保审批记录界面展示真实姓名。
---
## 🔧 核心接口详细说明
@@ -1048,6 +1050,7 @@ PDF模板包含以下空白填写区域(完全手工填写):
- 申请表单:党员且出国天数>30天时自动显示
- 申请详情:符合条件时在弹窗底部显示绿色按钮
- **前端实现**:必须使用 `request` 工具而不是 `fetch` API,以确保包含所有必要的认证请求头
- **附件格式**:自 2025-11-27 起,党籍保留附件支持任意文件类型上传,仅保留 10MB 单文件大小限制
#### 错误处理
@@ -1242,4 +1245,20 @@ import { getCurrentApproversForTask } from '@/api/workflow/common';
**更新时间**:2025-10-28
**维护人员**:开发团队
**变更记录**:新增工作流动态审批人方案说明
**变更记录**:新增工作流动态审批人方案说明
---
### 2025-11-28 更新:出国记录回执下载
- **前端**:`web/src/views/rs/overseas/index.vue` 中的 “下载因私出国(境)情况回执” 按钮改为直接基于 Word 模板生成文件,摆脱后端 PDF 下载接口依赖。
- **模板**:`web/public/chuguoqingkuanghuizhi.docx`,占位符 `{{applicantName}}`、`{{startYear}}`、`{{startMonth}}`、`{{startDay}}`、`{{endYear}}`、`{{endMonth}}`、`{{endDay}}`、`{{destinationText}}`、`{{exitReasonText}}`、`{{durationDays}}`、`{{expenseType}}`。
- **数据映射**:优先使用记录中的实际出行信息(`actualStartDate`、`actualEndDate`、`destinationCountry`、`exitReason` 等),缺省时回落到申请单数据,行程天数自动根据起止日期计算,默认费用说明为“费用自理”。
- **交互提示**:增加起止日期缺失时的前端校验,模板生成失败会提示用户检查模板或稍后再试。
### 2025-11-28 更新:审批监控页面审批表导出
- **前端**:`web/src/views/rs/overseas/monitor/index.vue` 的 “下载审批表” 按钮从 PDF 调整为 Word 导出,保持统一的 docxtemplater + image module 生成逻辑。
- **模板**:`web/public/chuguojingshenpibiao.docx`,覆盖个人信息、行程信息、申请人签名及五级审批节点(部门负责人、人事处、纪委、学院分管领导、学院党委书记)的签名、意见、日期占位符,支持 `{{%signature}}` 形式的图片变量。
- **数据来源**:优先取 `currentDetail` 详情接口返回值;审批节点数据由 `approvalHistory` 根据 `approvalLevel` 映射,若无签名/日期则留空。详情接口新增 `signature` 字段(申请人手写签名 base64),供模板直接使用。
- **交互提示**:仅在审批完成(状态为 `1`)时显示导出按钮,生成失败时给出友好提示并引导检查模板。
@@ -144,6 +144,10 @@
}
```
#### 字段补充说明(2025-11-28)
- `approverName`:后台统一返回审批人的 **昵称**,若用户未设置昵称则自动回退到账号;前端无需再做用户名到昵称的手动映射。
---
### 3. 查询待审批列表
+5 -1
View File
@@ -34,9 +34,12 @@
"await-to-js": "3.0.0",
"axios": "1.8.4",
"crypto-js": "4.2.0",
"docx": "^9.5.1",
"docxtemplater": "^3.67.5",
"docxtemplater-image-module-free": "^1.1.1",
"echarts": "^5.6.0",
"element-plus": "2.9.8",
"file-saver": "2.0.5",
"file-saver": "^2.0.5",
"highlight.js": "11.9.0",
"html2canvas": "^1.4.1",
"image-conversion": "2.1.1",
@@ -46,6 +49,7 @@
"jspdf-font": "^1.0.7",
"nprogress": "0.2.0",
"pinia": "3.0.2",
"pizzip": "^3.2.0",
"qrcode": "^1.5.4",
"screenfull": "6.0.2",
"vue": "3.5.13",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13 -1
View File
@@ -70,6 +70,7 @@ export interface OverseasRegistrationForm {
officePhone?: string; // 办公电话
mobilePhone?: string; // 移动电话
remarks?: string; // 备注
status?: string; // 登记状态:0-草稿,1-待审核,2-已通过,3-已拒绝
}
// 登记历史查询参数类型
@@ -142,6 +143,17 @@ export function getOverseasRegistrationList(query: OverseasRegistrationQuery) {
});
}
/**
* 查询我的出国登记列表(只查询当前登录人创建的记录)
*/
export function getMyOverseasRegistrationList(query: OverseasRegistrationQuery) {
return request({
url: '/rs/overseas/registration/my/list',
method: 'get',
params: query
});
}
/**
* 查询出国登记详细
*/
@@ -433,4 +445,4 @@ export function batchRejectOverseasRegistration(registrationIds: Array<number |
...data
}
});
}
}
+14 -3
View File
@@ -60,7 +60,7 @@ export interface OverseasApplicationVO {
partyRetentionFileName?: string; // 党籍保留附件文件名
partyRetentionUploadTime?: string; // 党籍保留附件上传时间
partyRetentionUploadStatus?: string; // 党籍保留附件上传状态
partyRetentionFileOssId?: number; // 党籍保留附件OSS ID
partyRetentionFileOssId?: string; // 党籍保留附件OSS ID(支持多个ID用逗号分隔)
purpose: string;
purposeDetail: string;
destination: string;
@@ -137,6 +137,7 @@ export interface OverseasApplicationForm {
partyRetentionFileName?: string; // 党籍保留附件文件名
partyRetentionUploadTime?: string; // 党籍保留附件上传时间
partyRetentionUploadStatus?: string; // 党籍保留附件上传状态:0-未上传,1-已上传
partyRetentionFileOssId?: string; // 党籍保留附件OSS ID(支持多个ID用逗号分隔)
purpose: string;
purposeDetail: string;
destination: string;
@@ -341,6 +342,15 @@ export function listPendingOverseasApplication() {
});
}
// 查询当前用户的待审批申请分页列表(根据角色过滤,支持分页和搜索)
export function listPendingOverseasApplicationPage(query: OverseasApplicationQuery) {
return request({
url: '/rs/overseas/application/pending/page',
method: 'get',
params: query
});
}
// ========================================
// 🚨 标准化改造:删除自定义待审批查询API
//
@@ -805,6 +815,7 @@ export interface ItineraryChangeQuery {
applicantName?: string;
destination?: string;
status?: string;
applicantId?: number;
params?: Record<string, any>;
}
@@ -842,8 +853,8 @@ export interface ItineraryChangeVO {
status: string;
statusText: string;
approvalOpinion?: string;
approvalBy?: number;
approvalByName?: string;
approverId?: number;
approverName?: string;
approvalTime?: string;
createTime: string;
updateTime?: string;
+1 -1
View File
@@ -34,7 +34,7 @@ router.beforeEach(async (to, from, next) => {
const [err] = await tos(useUserStore().getInfo());
if (err) {
await useUserStore().logout();
ElMessage.error(err);
// ElMessage.error(err.message || '获取用户信息失败');
next({ path: '/' });
} else {
isRelogin.show = false;
+8 -2
View File
@@ -204,8 +204,14 @@ service.interceptors.response.use(
err.response = res;
return Promise.reject(err);
} else if (code !== HttpStatus.SUCCESS) {
ElNotification.error({ title: msg });
const err = new Error(msg) as any;
// 确保错误消息不为空,避免只显示图标没有文字
const errorMsg = msg || errorCode['default'] || '操作失败,请稍后重试';
ElNotification.error({
title: '错误提示',
message: errorMsg,
duration: 5000
});
const err = new Error(errorMsg) as any;
err.response = res;
return Promise.reject(err);
} else {
+22 -6
View File
@@ -192,13 +192,29 @@ const getLoginData = () => {
* 获取租户列表
*/
const initTenantList = async () => {
const { data } = await getTenantList(false);
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
if (tenantEnabled.value) {
tenantList.value = data.voList;
if (tenantList.value != null && tenantList.value.length !== 0) {
loginForm.value.tenantId = tenantList.value[0].tenantId;
try {
const [err, res] = await to(getTenantList(false));
if (err) {
// 静默处理租户列表获取失败,不影响登录功能
console.warn('获取租户列表失败:', err);
// 设置默认值,确保登录功能正常
tenantEnabled.value = true;
return;
}
if (res && res.data) {
const { data } = res;
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
if (tenantEnabled.value) {
tenantList.value = data.voList;
if (tenantList.value != null && tenantList.value.length !== 0) {
loginForm.value.tenantId = tenantList.value[0].tenantId;
}
}
}
} catch (error) {
// 捕获其他可能的错误,静默处理
console.warn('初始化租户列表时发生错误:', error);
tenantEnabled.value = true;
}
};
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,30 +1,6 @@
<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">
<!-- 待审批 -->
@@ -69,13 +45,8 @@
</el-row>
</template>
<!-- 🔴 标准化改造:显示迁移提示 -->
<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 v-loading="pendingLoading" border :data="pendingList" @row-dblclick="handleView">
<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" />
@@ -190,7 +161,7 @@
</template>
</el-table-column>
<el-table-column label="审批时间" align="center" prop="approvalTime" min-width="160" />
<el-table-column label="审批人" align="center" prop="approvalByName" min-width="100" />
<el-table-column label="审批人" align="center" prop="approverName" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="80" fixed="right">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
@@ -243,7 +214,7 @@
<el-tag :type="getStatusTagType(viewData.status)">{{ viewData.statusText }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ viewData.createTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="审批人" v-if="viewData.approvalByName">{{ viewData.approvalByName }}</el-descriptions-item>
<el-descriptions-item label="审批人" v-if="viewData.approverName">{{ viewData.approverName }}</el-descriptions-item>
<el-descriptions-item label="审批时间" v-if="viewData.approvalTime">{{ viewData.approvalTime }}</el-descriptions-item>
<el-descriptions-item label="审批意见" :span="2" v-if="viewData.approvalOpinion">{{ viewData.approvalOpinion }}</el-descriptions-item>
</el-descriptions>
@@ -267,16 +238,10 @@
<el-form-item label="审批意见" prop="opinion">
<el-input v-model="approvalForm.opinion" type="textarea" :rows="4" placeholder="请输入审批意见" maxlength="500" show-word-limit />
</el-form-item>
<!-- 手写签字 -->
<el-form-item label="审批签字">
<SignaturePad
v-model="approvalForm.signature"
:width="450"
:height="120"
:auto-save="true"
:show-save-button="false"
/>
<SignaturePad v-model="approvalForm.signature" :width="450" :height="120" :auto-save="true" :show-save-button="false" />
<div class="signature-tip">请在上方签字板中手写签名</div>
</el-form-item>
</el-form>
@@ -413,17 +378,24 @@ const handleTabClick = (tab: any) => {
}
};
// 🔴 标准化改造:简化待审批列表获取(仅用于兼容模式)
// 获取待审批变更列表(status=0)
const getPendingList = async () => {
state.pendingLoading = true;
// 提示用户使用标准工作流页面
ElMessage.info('请使用标准工作流待办页面查看审批任务');
// 清空数据,不再调用自定义API
state.pendingList = [];
state.pendingTotal = 0;
state.pendingLoading = false;
try {
// 查询待审核状态的变更申请(status='0')
const params = { ...state.pendingQueryParams };
params.status = '0'; // 只查询待审核的
const response = await listPendingItineraryChange(params);
state.pendingList = response.rows || [];
state.pendingTotal = response.total || 0;
} catch (error) {
console.error('获取待审批列表失败:', error);
ElMessage.error('获取待审批列表失败');
state.pendingList = [];
state.pendingTotal = 0;
} finally {
state.pendingLoading = false;
}
};
// 查询审批记录列表
@@ -446,20 +418,22 @@ const getRecordList = async () => {
}
};
// 🔴 标准化改造:导航到标准工作流待办页面
const navigateToTaskWaiting = () => {
// 跳转到标准工作流待办页面
router.push('/workflow/task/taskWaiting');
};
// 搜索查询 - 待审批列表
const handlePendingQuery = () => {
ElMessage.info('请使用标准工作流待办页面查看审批任务');
state.pendingQueryParams.pageNum = 1;
getPendingList();
};
// 重置搜索 - 待审批列表
const resetPendingQuery = () => {
ElMessage.info('请使用标准工作流待办页面查看审批任务');
pendingQueryFormRef.value?.resetFields();
Object.assign(state.pendingQueryParams, {
pageNum: 1,
pageSize: 10,
applicantName: '',
destination: ''
});
getPendingList();
};
// 搜索查询 - 审批记录
@@ -574,4 +548,7 @@ const getStatusTagType = (status: string): 'primary' | 'success' | 'info' | 'war
color: #909399;
text-align: center;
}
:global(.app-container) {
padding: 0 !important;
}
</style>
+239 -182
View File
@@ -3,7 +3,6 @@
<el-card class="box-card">
<!-- Tab切换:变更申请 / 变更记录 -->
<el-tabs v-model="activeTab" type="border-card" @tab-click="handleTabClick">
<!-- Tab 1: 变更申请(待审核) -->
<el-tab-pane label="变更申请(待审核)" name="all">
<div class="tab-content">
@@ -23,15 +22,6 @@
<el-input v-model="queryParams.destination" placeholder="请输入目的地国家" clearable />
</el-form-item>
</el-col>
<el-col :span="6">
<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-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
@@ -52,9 +42,7 @@
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['rs:itineraryChange:add']">
新增变更申请
</el-button>
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['rs:itineraryChange:add']"> 新增变更申请 </el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@@ -62,45 +50,65 @@
<!-- 变更行程列表 -->
<el-table v-loading="loading" border :data="changeList" @row-dblclick="handleView">
<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="originalDestination" min-width="120" />
<el-table-column label="新目的地" align="center" prop="newDestination" min-width="120" />
<el-table-column label="原出国时间" align="center" min-width="200">
<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>
</el-table-column>
<el-table-column label="变更状态" align="center" prop="statusText" min-width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">{{ scope.row.statusText }}</el-tag>
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="createTime" min-width="160" />
<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="originalDestination" min-width="120" />
<el-table-column label="新目的地" align="center" prop="newDestination" min-width="120" />
<el-table-column label="原出国时间" align="center" min-width="200">
<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>
</el-table-column>
<el-table-column label="变更状态" align="center" prop="statusText" min-width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">{{ scope.row.statusText }}</el-tag>
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="createTime" min-width="160" />
<el-table-column label="操作" align="center" width="250" fixed="right">
<template #default="scope">
<el-tooltip content="查看详情" placement="top">
<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="info" icon="List" @click="handleViewHistory(scope.row)" v-hasPermi="['rs:itineraryChange:query']"></el-button>
<el-button
link
type="info"
icon="List"
@click="handleViewHistory(scope.row)"
v-hasPermi="['rs:itineraryChange:query']"
></el-button>
</el-tooltip>
<!-- 待审核状态:显示撤回按钮 -->
<el-tooltip content="撤回申请" placement="top" v-if="scope.row.status === '0'">
<el-button link type="warning" icon="RefreshLeft" @click="handleCancel(scope.row)" v-hasPermi="['rs:itineraryChange:cancel']"></el-button>
<el-button
link
type="warning"
icon="RefreshLeft"
@click="handleCancel(scope.row)"
v-hasPermi="['rs:itineraryChange:cancel']"
></el-button>
</el-tooltip>
<!-- 已拒绝状态:显示重新提交按钮 -->
<el-tooltip content="重新提交" placement="top" v-if="scope.row.status === '2'">
<el-button link type="success" icon="RefreshRight" @click="handleResubmit(scope.row)" v-hasPermi="['rs:itineraryChange:edit']"></el-button>
<!-- 已拒绝或已撤回状态:显示重新提交按钮 -->
<el-tooltip content="重新提交" placement="top" v-if="scope.row.status === '2' || scope.row.status === '3'">
<el-button
link
type="success"
icon="RefreshRight"
@click="handleResubmit(scope.row)"
v-hasPermi="['rs:itineraryChange:edit']"
></el-button>
</el-tooltip>
<!-- 删除按钮 -->
<el-tooltip content="删除" placement="top" v-if="scope.row.status !== '1'">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['rs:itineraryChange:remove']"></el-button>
<el-button
link
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['rs:itineraryChange:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
@@ -131,15 +139,6 @@
<el-input v-model="myQueryParams.destination" placeholder="请输入目的地国家" clearable />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="变更状态" prop="status">
<el-select v-model="myQueryParams.status" placeholder="请选择变更状态" clearable style="width: 100%">
<el-option label="待审核" value="0" />
<el-option label="已通过" value="1" />
<el-option label="已拒绝" value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
@@ -169,14 +168,10 @@
<el-table-column label="原目的地" align="center" prop="originalDestination" min-width="120" />
<el-table-column label="新目的地" align="center" prop="newDestination" min-width="120" />
<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="statusText" min-width="100">
<template #default="scope">
@@ -190,19 +185,43 @@
<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="info" icon="List" @click="handleViewHistory(scope.row)" v-hasPermi="['rs:itineraryChange:query']"></el-button>
<el-button
link
type="info"
icon="List"
@click="handleViewHistory(scope.row)"
v-hasPermi="['rs:itineraryChange:query']"
></el-button>
</el-tooltip>
<!-- 待审核状态:显示撤回按钮 -->
<el-tooltip content="撤回申请" placement="top" v-if="scope.row.status === '0'">
<el-button link type="warning" icon="RefreshLeft" @click="handleCancel(scope.row)" v-hasPermi="['rs:itineraryChange:cancel']"></el-button>
<el-button
link
type="warning"
icon="RefreshLeft"
@click="handleCancel(scope.row)"
v-hasPermi="['rs:itineraryChange:cancel']"
></el-button>
</el-tooltip>
<!-- 已拒绝状态:显示重新提交按钮 -->
<el-tooltip content="重新提交" placement="top" v-if="scope.row.status === '2'">
<el-button link type="success" icon="RefreshRight" @click="handleResubmit(scope.row)" v-hasPermi="['rs:itineraryChange:edit']"></el-button>
<!-- 已拒绝或已撤回状态:显示重新提交按钮 -->
<el-tooltip content="重新提交" placement="top" v-if="scope.row.status === '2' || scope.row.status === '3'">
<el-button
link
type="success"
icon="RefreshRight"
@click="handleResubmit(scope.row)"
v-hasPermi="['rs:itineraryChange:edit']"
></el-button>
</el-tooltip>
<!-- 删除按钮 -->
<el-tooltip content="删除" placement="top" v-if="scope.row.status !== '1'">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['rs:itineraryChange:remove']"></el-button>
<el-button
link
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['rs:itineraryChange:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
@@ -223,24 +242,23 @@
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body :close-on-click-modal="false" destroy-on-close>
<div style="max-height: 70vh; overflow-y: auto; padding-right: 10px;">
<div style="max-height: 70vh; overflow-y: auto; padding-right: 10px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<!-- 选择原出国申请 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="选择原出国申请" prop="applicationId">
<el-select
v-model="form.applicationId"
placeholder="请选择已审批通过的出国申请"
<el-select
v-model="form.applicationId"
placeholder="请选择已审批通过的出国申请"
style="width: 100%"
@change="handleApplicationChange"
:disabled="!!form.changeId"
filterable
>
<el-option
v-for="application in availableApplicationList"
:key="application.applicationId"
<el-option
v-for="application in availableApplicationList"
:key="application.applicationId"
:label="`${application.applicantName} - ${application.destination} (${formatDate(application.startDate)}至${formatDate(application.endDate)})`"
:value="application.applicationId"
/>
@@ -336,13 +354,7 @@
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="行程安排">
<el-input
v-model="form.originalItinerary"
type="textarea"
:rows="3"
disabled
placeholder="原行程安排"
/>
<el-input v-model="form.originalItinerary" type="textarea" :rows="3" disabled placeholder="原行程安排" />
</el-form-item>
</el-col>
</el-row>
@@ -384,14 +396,7 @@
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="行程安排" prop="newItinerary">
<el-input
v-model="form.newItinerary"
type="textarea"
:rows="3"
placeholder="请输入实际行程安排"
maxlength="1000"
show-word-limit
/>
<el-input v-model="form.newItinerary" type="textarea" :rows="3" placeholder="请输入实际行程安排" maxlength="1000" show-word-limit />
</el-form-item>
</el-col>
</el-row>
@@ -401,10 +406,10 @@
<el-row :gutter="20">
<el-col :span="24">
<el-form-item prop="changeReason">
<el-input
v-model="form.changeReason"
type="textarea"
:rows="4"
<el-input
v-model="form.changeReason"
type="textarea"
:rows="4"
placeholder="请详细说明行程变更的原因(相关证明材料可附后)"
maxlength="500"
show-word-limit
@@ -418,21 +423,13 @@
<el-row :gutter="20">
<el-col :span="24">
<el-form-item>
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="请填写有关认识和态度"
maxlength="200"
show-word-limit
/>
<div style="margin-top: 12px; color: #303133; font-size: 14px; font-weight: bold;">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请填写有关认识和态度" maxlength="200" show-word-limit />
<div style="margin-top: 12px; color: #303133; font-size: 14px; font-weight: bold">
我承诺,以上情况说明完全属实。如有不实行为,愿意接受组织处理。
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
@@ -450,14 +447,14 @@
<!-- 基本信息 -->
<el-descriptions-item label="申请人">{{ viewData.applicantName || '-' }}</el-descriptions-item>
<el-descriptions-item label="部门">{{ viewData.department || '-' }}</el-descriptions-item>
<!-- 原申请信息 -->
<el-descriptions-item label="原目的地">{{ viewData.originalDestination || '-' }}</el-descriptions-item>
<el-descriptions-item label="原出境事由">{{ viewData.originalExitReason || '-' }}</el-descriptions-item>
<el-descriptions-item label="原出发日期">{{ formatDate(viewData.originalStartDate) }}</el-descriptions-item>
<el-descriptions-item label="原返回日期">{{ formatDate(viewData.originalEndDate) }}</el-descriptions-item>
<el-descriptions-item label="原出国天数" :span="2">{{ viewData.originalDuration || 0 }}天</el-descriptions-item>
<!-- 变更信息 -->
<el-descriptions-item label="变更类型" :span="2">{{ viewData.changeType || '-' }}</el-descriptions-item>
<el-descriptions-item label="新目的地" v-if="viewData.newDestination">{{ viewData.newDestination }}</el-descriptions-item>
@@ -468,7 +465,7 @@
<el-descriptions-item label="新行程安排" :span="2" v-if="viewData.newItinerary">{{ viewData.newItinerary }}</el-descriptions-item>
<el-descriptions-item label="变更原因" :span="2">{{ viewData.changeReason || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2" v-if="viewData.remark">{{ viewData.remark }}</el-descriptions-item>
<!-- 审批信息 -->
<el-descriptions-item label="变更状态">
<el-tag :type="getStatusTagType(viewData.status)">{{ viewData.statusText }}</el-tag>
@@ -513,7 +510,6 @@
</div>
</template>
</el-dialog>
</div>
</template>
@@ -522,7 +518,8 @@ import { ref, reactive, toRefs, onMounted, computed, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import animate from '@/animate';
import { parseTime } from '@/utils/ruoyi';
import {
import { useUserStore } from '@/store/modules/user';
import {
listItineraryChange,
listMyItineraryChange,
getItineraryChange,
@@ -535,6 +532,7 @@ import {
getOverseasApplication,
getItineraryChangeHistory,
getOverseasRecordByApplication,
listOverseasApplication,
type ItineraryChangeQuery,
type ItineraryChangeVO,
type ItineraryChangeForm,
@@ -553,7 +551,7 @@ const formRef = ref();
const state = reactive({
// Tab切换
activeTab: 'all',
// 全部变更列表
loading: false,
showSearch: true,
@@ -565,9 +563,10 @@ const state = reactive({
pageSize: 10,
applicantName: '',
destination: '',
status: ''
status: '',
applicantId: undefined as number | undefined
} as ItineraryChangeQuery,
// 我的变更列表
myLoading: false,
showMySearch: true,
@@ -577,9 +576,10 @@ const state = reactive({
pageNum: 1,
pageSize: 10,
destination: '',
status: ''
status: '',
applicantId: undefined as number | undefined
} as ItineraryChangeQuery,
// 对话框
dialog: {
visible: false,
@@ -591,21 +591,21 @@ const state = reactive({
historyDialog: {
visible: false
},
// 表单数据
form: {} as ItineraryChangeForm,
viewData: null as ItineraryChangeVO | null,
// 变更历史
historyLoading: false,
historyList: [] as ItineraryChangeVO[],
// 可用的出国申请列表(已审批通过)
availableApplicationList: [] as OverseasApplicationVO[],
// 变更类型(多选)
changeTypes: [] as string[],
// 表单验证规则
rules: {
applicationId: [{ required: true, message: '请选择原出国申请', trigger: 'change' }],
@@ -690,7 +690,7 @@ onMounted(() => {
const handleTabClick = (tab: any) => {
const tabName = tab.props?.name || tab.paneName;
state.activeTab = tabName;
if (tabName === 'all') {
getList();
} else if (tabName === 'my') {
@@ -698,18 +698,36 @@ const handleTabClick = (tab: any) => {
}
};
// 查询变更申请列表(只显示待审核的数据)
// 查询变更申请列表(只显示当前登录人的待审核和已撤回的数据)
const getList = async () => {
state.loading = true;
try {
// 如果用户没有手动选择状态,默认只显示待审核的申请
const params = { ...state.queryParams };
if (!params.status) {
params.status = '0'; // 默认只显示待审核状态
}
const userStore = useUserStore();
// 先获取所有当前登录人的数据(不分页),然后在前端过滤和分页
const params = {
pageNum: 1,
pageSize: 10000, // 设置一个较大的值,获取所有数据
applicantId: Number(userStore.userId),
applicantName: state.queryParams.applicantName,
destination: state.queryParams.destination
};
const response = await listItineraryChange(params);
state.changeList = response.rows;
state.total = response.total;
// 前端过滤:只显示状态为 0(待审核)和 3(已撤回)的数据
const filteredRows = response.rows.filter((item: ItineraryChangeVO) => {
return item.status === '0' || item.status === '3';
});
// 前端分页
const pageNum = state.queryParams.pageNum;
const pageSize = state.queryParams.pageSize;
const total = filteredRows.length;
const start = (pageNum - 1) * pageSize;
const end = Math.min(start + pageSize, total);
state.changeList = filteredRows.slice(start, end);
state.total = total;
} catch (error) {
console.error('获取变更行程列表失败:', error);
} finally {
@@ -717,13 +735,35 @@ const getList = async () => {
}
};
// 查询我的变更记录列表(只显示已通过的变更申请)
// 查询我的变更记录列表(只显示当前登录人的已通过和已拒绝的数据)
const getMyList = async () => {
state.myLoading = true;
try {
const response = await listMyItineraryChange(state.myQueryParams);
state.myChangeList = response.rows;
state.myTotal = response.total;
const userStore = useUserStore();
// 先获取所有当前登录人的数据(不分页),然后在前端过滤和分页
const params = {
pageNum: 1,
pageSize: 10000, // 设置一个较大的值,获取所有数据
applicantId: Number(userStore.userId),
destination: state.myQueryParams.destination
};
const response = await listItineraryChange(params);
// 前端过滤:只显示状态为 1(已通过)和 2(已拒绝)的数据
const filteredRows = response.rows.filter((item: ItineraryChangeVO) => {
return item.status === '1' || item.status === '2';
});
// 前端分页
const pageNum = state.myQueryParams.pageNum;
const pageSize = state.myQueryParams.pageSize;
const total = filteredRows.length;
const start = (pageNum - 1) * pageSize;
const end = Math.min(start + pageSize, total);
state.myChangeList = filteredRows.slice(start, end);
state.myTotal = total;
} catch (error) {
console.error('获取我的变更记录失败:', error);
} finally {
@@ -731,13 +771,25 @@ const getMyList = async () => {
}
};
// 获取已审批通过的出国申请列表
// 获取已审批通过的出国申请列表(只显示当前登录人的)
const getApprovedApplicationList = async () => {
try {
const response = await getApprovedOverseasApplications();
state.availableApplicationList = response.data;
const userStore = useUserStore();
// 使用listOverseasApplication查询当前登录人的已通过申请
const response = await listOverseasApplication({
pageNum: 1,
pageSize: 1000, // 设置一个较大的值,获取所有数据
applicantId: Number(userStore.userId), // 只查询当前登录人的申请
status: '1' // 只查询已通过的状态
});
// 确保只返回已通过审批的申请
state.availableApplicationList = response.rows.filter((item: OverseasApplicationVO) => {
return item.status === '1';
});
} catch (error) {
console.error('获取已审批通过的出国申请列表失败:', error);
ElMessage.error('获取已审批通过的出国申请列表失败');
}
};
@@ -798,30 +850,30 @@ const handleApplicationChange = async (applicationId: number) => {
// 1. 获取出国申请详情
const response = await getOverseasApplication(applicationId);
const selectedApplication = response.data;
// 2. 根据申请ID获取出国记录(包含recordId)
const recordResponse = await getOverseasRecordByApplication(applicationId);
const recordData = recordResponse.data;
if (selectedApplication && recordData) {
// 设置recordId(关键字段,必填)
state.form.recordId = recordData.recordId;
// 设置申请人ID(关键字段,必填)
state.form.applicantId = selectedApplication.applicantId;
// 填充本人基本情况
state.form.applicantName = selectedApplication.applicantName;
state.form.gender = selectedApplication.gender;
state.form.birthDate = selectedApplication.birthDate;
// 政治面貌:将字典值转换为文本标签
state.form.politicalStatus = getPoliticalStatusLabel(selectedApplication.politicalStatus);
state.form.politicalStatus = selectedApplication.politicalStatus ? getPoliticalStatusLabel(selectedApplication.politicalStatus) : '';
state.form.professionalTitle = selectedApplication.professionalTitle;
state.form.position = selectedApplication.position;
state.form.documentName = selectedApplication.documentName;
state.form.documentNumber = selectedApplication.documentNumber;
state.form.department = selectedApplication.department;
// 填充申请行程情况
// 前往国家:优先使用destinationCountry,其次使用destination
state.form.originalDestination = selectedApplication.destinationCountry || selectedApplication.destination;
@@ -833,7 +885,7 @@ const handleApplicationChange = async (applicationId: number) => {
state.form.originalStartDate = selectedApplication.startDate;
state.form.originalEndDate = selectedApplication.endDate;
state.form.originalDuration = selectedApplication.duration;
// 默认将原信息复制到新信息
state.form.newDestination = selectedApplication.destinationCountry || selectedApplication.destination;
state.form.newDestinationCity = selectedApplication.destinationCity;
@@ -867,22 +919,24 @@ const handleCancel = (row: ItineraryChangeVO) => {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入撤回原因'
}).then(async ({ value }) => {
try {
await cancelItineraryChange(row.changeId, value);
ElMessage.success('撤回成功');
// 根据当前Tab刷新列表
if (state.activeTab === 'all') {
getList();
} else if (state.activeTab === 'my') {
getMyList();
})
.then(async ({ value }) => {
try {
await cancelItineraryChange(row.changeId, value);
ElMessage.success('撤回成功');
// 根据当前Tab刷新列表
if (state.activeTab === 'all') {
getList();
} else if (state.activeTab === 'my') {
getMyList();
}
} catch (error) {
console.error('撤回失败:', error);
ElMessage.error('撤回失败');
}
} catch (error) {
console.error('撤回失败:', error);
ElMessage.error('撤回失败');
}
}).catch(() => {});
})
.catch(() => {});
};
// 重新提交
@@ -890,12 +944,12 @@ const handleResubmit = async (row: ItineraryChangeVO) => {
try {
const response = await getItineraryChange(row.changeId);
Object.assign(state.form, response.data);
// 解析变更类型
if (response.data.changeType) {
state.changeTypes = response.data.changeType.split(',');
}
state.dialog.title = '重新提交行程变更申请';
state.dialog.visible = true;
} catch (error) {
@@ -910,30 +964,32 @@ const handleDelete = (row: ItineraryChangeVO) => {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await delItineraryChange(row.changeId);
ElMessage.success('删除成功');
// 根据当前Tab刷新列表(如果删除的是当前页最后一条且不是第1页,返回上一页)
if (state.activeTab === 'all') {
// 如果删除的是当前页最后一条且不是第1页,返回上一页
if (state.changeList.length === 1 && state.queryParams.pageNum > 1) {
state.queryParams.pageNum--;
})
.then(async () => {
try {
await delItineraryChange(row.changeId);
ElMessage.success('删除成功');
// 根据当前Tab刷新列表(如果删除的是当前页最后一条且不是第1页,返回上一页)
if (state.activeTab === 'all') {
// 如果删除的是当前页最后一条且不是第1页,返回上一页
if (state.changeList.length === 1 && state.queryParams.pageNum > 1) {
state.queryParams.pageNum--;
}
getList();
} else if (state.activeTab === 'my') {
// 如果删除的是当前页最后一条且不是第1页,返回上一页
if (state.myChangeList.length === 1 && state.myQueryParams.pageNum > 1) {
state.myQueryParams.pageNum--;
}
getMyList();
}
getList();
} else if (state.activeTab === 'my') {
// 如果删除的是当前页最后一条且不是第1页,返回上一页
if (state.myChangeList.length === 1 && state.myQueryParams.pageNum > 1) {
state.myQueryParams.pageNum--;
}
getMyList();
} catch (error) {
console.error('删除失败:', error);
ElMessage.error('删除失败');
}
} catch (error) {
console.error('删除失败:', error);
ElMessage.error('删除失败');
}
}).catch(() => {});
})
.catch(() => {});
};
// 查看变更历史
@@ -963,15 +1019,14 @@ const submitForm = () => {
if (state.form.newExitReason !== state.form.originalExitReason) {
changeTypeList.push('出境事由变更');
}
if (state.form.newStartDate !== state.form.originalStartDate ||
state.form.newEndDate !== state.form.originalEndDate) {
if (state.form.newStartDate !== state.form.originalStartDate || state.form.newEndDate !== state.form.originalEndDate) {
changeTypeList.push('时间变更');
}
if (state.form.newItinerary && state.form.newItinerary !== state.form.originalItinerary) {
changeTypeList.push('行程变更');
}
state.form.changeType = changeTypeList.length > 0 ? changeTypeList.join(',') : '其他变更';
state.submitLoading = true;
try {
if (state.form.changeId) {
@@ -982,7 +1037,7 @@ const submitForm = () => {
ElMessage.success('提交成功!变更申请已进入审批流程。');
}
state.dialog.visible = false;
// 根据当前Tab刷新列表(重置到第1页)
if (state.activeTab === 'all') {
state.queryParams.pageNum = 1;
@@ -1056,11 +1111,11 @@ const getStatusTagType = (status: string): 'primary' | 'success' | 'info' | 'war
const typeMap: Record<string, 'primary' | 'success' | 'info' | 'warning' | 'danger'> = {
'0': 'warning', // 待审核
'1': 'success', // 已通过
'2': 'danger' // 已拒绝
'2': 'danger', // 已拒绝
'3': 'info' // 已撤回
};
return typeMap[status] || 'info';
};
</script>
<style scoped>
@@ -1081,6 +1136,8 @@ const getStatusTagType = (status: string): 'primary' | 'success' | 'info' | 'war
.table-card {
margin-bottom: 10px;
}
:global(.app-container) {
padding: 0 !important;
}
</style>
+190 -47
View File
@@ -49,24 +49,24 @@
</el-card>
</div>
<!-- 操作按钮区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Refresh" @click="handleRefreshPending">刷新</el-button>
</el-col>
<el-col :span="1.5">
<div class="info-text">
<el-icon><InfoFilled /></el-icon>
<span>这里显示所有待审批申请</span>
</div>
</el-col>
</el-row>
</template>
<!-- 操作按钮区域 -->
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Refresh" @click="handleRefreshPending">刷新</el-button>
</el-col>
<el-col :span="1.5">
<div class="info-text">
<el-icon><InfoFilled /></el-icon>
<span>这里显示所有待审批申请</span>
</div>
</el-col>
</el-row>
</template>
<!-- 表格 -->
<el-table v-loading="pendingLoading" :data="pendingList" border>
<!-- 表格 -->
<el-table v-loading="pendingLoading" :data="pendingList" border>
<el-table-column label="申请人" prop="applicantName" min-width="100" align="center" :show-overflow-tooltip="true" />
<el-table-column label="所属部门" prop="department" min-width="140" align="center" :show-overflow-tooltip="true" />
<el-table-column label="前往国家" prop="destinationCountry" min-width="110" align="center" :show-overflow-tooltip="true" />
@@ -350,8 +350,16 @@
<template #footer>
<div style="display: flex; justify-content: space-between; width: 100%">
<div>
<!-- 只有审批完成的申请才显示下载PDF按钮 -->
<el-button v-if="currentDetail && currentDetail.status === '1'" type="primary" icon="Download" @click="handleDownloadPdf" v-hasPermi="['rs:overseas:query']">下载审批表PDF</el-button>
<!-- 只有审批完成的申请才显示下载Word按钮 -->
<el-button
v-if="currentDetail && currentDetail.status === '1'"
type="primary"
icon="Download"
@click="handleDownloadApprovalWord"
v-hasPermi="['rs:overseas:query']"
>
下载审批表Word
</el-button>
</div>
<div>
<el-button @click="detailDialogVisible = false">关闭</el-button>
@@ -362,14 +370,15 @@
</div>
</template>
<script setup name="OverseasApprovalMonitor">
<script setup lang="ts" name="OverseasApprovalMonitor">
import { ref, reactive, onMounted, watch } from 'vue';
import dayjs from 'dayjs';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useRouter } from 'vue-router';
import { Clock, Search, Refresh, View, EditPen, InfoFilled, Download } from '@element-plus/icons-vue';
// API请求
import { getPendingList as apiGetPendingList, getCompletedList as apiGetCompletedList, getDetail, downloadApprovalPdf } from '@/api/rs/overseas';
import { getPendingList as apiGetPendingList, getCompletedList as apiGetCompletedList, getDetail } from '@/api/rs/overseas';
// 导入变更历史API接口
import { getItineraryChangeHistory } from '@/api/rs/overseas';
@@ -405,6 +414,8 @@ const completedQueryParams = reactive({
department: undefined,
destination: undefined,
applyTimeRange: undefined,
applyTimeStart: undefined as string | undefined,
applyTimeEnd: undefined as string | undefined,
status: '1'
});
@@ -691,48 +702,180 @@ const getChangeStatusTagType = (status) => {
};
/**
* 下载审批表PDF
* 下载审批表Word
*/
const handleDownloadPdf = async () => {
if (!currentDetail.value || !currentDetail.value.applicationId) {
const handleDownloadApprovalWord = async () => {
const detail = currentDetail.value;
if (!detail || !detail.applicationId) {
ElMessage.error('无法获取申请ID');
return;
}
try {
ElMessage.info('正在生成PDF,请稍候...');
// 调用API获取blob数据
const response = await downloadApprovalPdf(currentDetail.value.applicationId);
// 创建带MIME类型的Blob对象
const blob = response instanceof Blob
? new Blob([response], { type: 'application/pdf' })
: new Blob([response.data], { type: 'application/pdf' });
// 创建下载链接
ElMessage.info('正在生成审批表,请稍候...');
const templateData = buildApprovalTemplateData(detail);
const [Docxtemplater, PizZip, ImageModule] = await Promise.all([
import('docxtemplater'),
import('pizzip'),
import('docxtemplater-image-module-free')
]);
const templateResponse = await fetch('/chuguojingshenpibiao.docx', { cache: 'no-store' });
if (!templateResponse.ok) {
throw new Error('模板文件加载失败,请联系管理员');
}
const arrayBuffer = await templateResponse.arrayBuffer();
const zip = new PizZip.default(arrayBuffer);
const imageOpts = {
centered: false,
getImage: (base64: string) => {
if (!base64) {
return null;
}
const pure = base64.split(',')[1] || base64;
const bytes = Uint8Array.from(atob(pure), (char) => char.charCodeAt(0));
return bytes.buffer;
},
getSize: () => [60, 20]
};
const doc = new Docxtemplater.default(zip, {
modules: [new ImageModule.default(imageOpts)],
paragraphLoop: true,
linebreaks: true,
delimiters: { start: '{{', end: '}}' },
parser: (tag: string) => {
return {
get: (scope: Record<string, any>) => {
const cleanTag = tag.trim().replace(/^%+/, '');
return scope[cleanTag];
}
};
}
});
doc.render(templateData);
const blob = doc.getZip().generate({
type: 'blob',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
});
const fileName = `出国申请审批表-${detail.applicantName || dayjs().format('YYYYMMDDHHmmss')}.docx`;
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// 设置下载文件名(使用申请人姓名和时间戳)
const fileName = `出国申请审批表_${currentDetail.value.applicantName}_${currentDetail.value.applicationId}.pdf`;
link.download = fileName;
// 触发下载
document.body.appendChild(link);
link.click();
// 清理资源
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
ElMessage.success('PDF下载成功');
} catch (error) {
console.error('下载PDF失败', error);
ElMessage.error('下载PDF失败,请稍后重试');
ElMessage.success('审批表下载成功');
} catch (error: any) {
console.error('下载审批表失败', error);
ElMessage.error(error?.message || '下载审批表失败,请稍后重试');
}
};
/**
* 构建审批表模板数据
*/
const buildApprovalTemplateData = (detail) => {
const startParts = getDateParts(detail.startDate);
const endParts = getDateParts(detail.endDate);
const applyParts = getDateParts(detail.applyTime || detail.createTime);
const durationDays = detail.duration || calculateDuration(detail.startDate, detail.endDate);
const templateData: Record<string, any> = {
applicantName: detail.applicantName || '',
genderText: detail.genderText || '',
birthYear: getDateParts(detail.birthDate).year,
birthMonth: getDateParts(detail.birthDate).month,
birthDay: getDateParts(detail.birthDate).day,
politicalStatusText: detail.politicalStatusText || '-',
department: detail.department || '-',
professionalTitle: detail.professionalTitle || '-',
position: detail.position || '-',
documentName: detail.documentName || '-',
documentNumber: detail.documentNumber || '-',
destinationCountry: detail.destinationCountry || detail.destination || '-',
isTourismChecked: detail.exitReason === '旅游' ? '√' : '',
isVisitChecked: detail.exitReason === '探亲' ? '√' : '',
visitRelationship: detail.visitRelationship || '',
remarks: detail.remarks || '',
startYear: startParts.year,
startMonth: startParts.month,
startDay: startParts.day,
endYear: endParts.year,
endMonth: endParts.month,
endDay: endParts.day,
durationDays: durationDays || '',
applicantSignature: detail.signature || '',
mobilePhone: detail.mobilePhone || detail.contactPhone || '',
emergencyContact: detail.emergencyContact || '',
applicantSignYear: applyParts.year,
applicantSignMonth: applyParts.month,
applicantSignDay: applyParts.day
};
fillApprovalNodeData(templateData, detail.approvalHistory || []);
return templateData;
};
/**
* 计算出国天数
*/
const calculateDuration = (startDate, endDate) => {
if (!startDate || !endDate) return '';
const start = dayjs(startDate);
const end = dayjs(endDate);
if (!start.isValid() || !end.isValid()) return '';
return Math.max(end.diff(start, 'day') + 1, 1);
};
/**
* 获取日期拆分
*/
const getDateParts = (date) => {
if (!date) {
return { year: '', month: '', day: '' };
}
const parsed = dayjs(date);
if (!parsed.isValid()) {
return { year: '', month: '', day: '' };
}
return {
year: parsed.format('YYYY'),
month: parsed.format('MM'),
day: parsed.format('DD')
};
};
/**
* 填充审批节点数据
*/
const fillApprovalNodeData = (templateData, approvalHistory) => {
const approvalNodes = [
{ level: '1', prefix: 'deptLeader' },
{ level: '2', prefix: 'hr' },
{ level: '3', prefix: 'discipline' },
{ level: '4', prefix: 'viceDean' },
{ level: '5', prefix: 'secretary' }
];
approvalNodes.forEach((node) => {
const record = approvalHistory.find((item) => item.approvalLevel === node.level);
const dateParts = getDateParts(record?.approvalTime);
templateData[`${node.prefix}Signature`] = record?.approvalSignature || record?.signature || '';
templateData[`${node.prefix}Opinion`] = record?.approvalOpinion || '';
templateData[`${node.prefix}Year`] = dateParts.year;
templateData[`${node.prefix}Month`] = dateParts.month;
templateData[`${node.prefix}Day`] = dateParts.day;
});
};
</script>
<style scoped lang="scss">
@@ -1,88 +0,0 @@
# 出国申请进度查询功能修改总结 - 快速参考
## ✅ 修改完成
**修改时间**:2025-10-29
**修改范围**:仅修改monitor相关文件,未影响其他模块
---
## 📋 核心修改
### 1. 功能变化
#### 修改前
- ❌ 显示工作流迁移提示
- ❌ 无实际查询功能
- ❌ 表格被隐藏
#### 修改后
- ✅ 显示当前用户提交的待审批申请
- ✅ 显示当前用户提交的已完成审批申请(抄送)
- ✅ 支持搜索和查看审批历史
---
## 🎯 核心逻辑
### 数据过滤
```java
// 后端Service层添加过滤条件
Long currentUserId = LoginHelper.getUserId();
wrapper.eq("applicant_id", currentUserId); // 🔴 只查询当前用户提交的申请
```
### 功能说明
- **待审批中**:用户提交申请后,在这里查看审批进度
- **审批完成**:审批流程完成后,抄送一份到这里
---
## 📄 修改文件
| 文件 | 类型 | 修改内容 |
|------|------|---------|
| `RsOverseasApprovalMonitorServiceImpl.java` | 后端 | 添加当前用户过滤 |
| `RsOverseasApprovalMonitorController.java` | 后端 | 恢复pending接口 |
| `monitor/index.vue` | 前端 | 删除提示,恢复表格 |
---
## 🧪 快速测试
### 测试步骤
1. 登录系统
2. 访问"出国申请进度查询"
3. 查看"待审批中"和"审批完成"选项卡
### 预期结果
- ✅ 只显示当前用户提交的申请
- ✅ 不显示其他用户的申请
- ✅ 可以查看审批历史
---
## 🚀 部署命令
```bash
# 后端
cd backend
mvn clean package -pl ruoyi-admin -am -DskipTests
./stop-main.sh && ./start-main.sh
# 前端(如需要)
cd web
npm run build
```
---
## 📝 详细文档
完整修改说明请参考:`出国申请进度查询功能修改说明.md`
---
**状态**:✅ 已完成
**版本**:v1.0
@@ -1,365 +0,0 @@
# 出国申请进度查询功能优化说明
**日期**: 2025-01-29
**模块**: 出国申请进度查询 (`/rs/overseas/monitor`)
## 📋 功能需求
### 1. 变更历史记录条件显示
- **需求**: 变更历史记录字段只在有变更记录时显示,没有变更记录时不显示该区域
- **实现位置**: `web/src/views/rs/overseas/monitor/index.vue`
### 2. PDF下载功能
- **需求**: 在审批完成选项卡的详情弹窗中新增PDF下载按钮
- **PDF模板**: 杭州第一技师学院登记报备人员因私事出国(境)审批表
- **显示条件**: 仅当申请状态为"已通过"(status='1')时显示下载按钮
## 🎯 实现内容
### 一、前端修改
#### 1. 变更历史记录条件显示
**文件**: `web/src/views/rs/overseas/monitor/index.vue`
**修改内容**:
- 原条件: `v-if="currentDetail.recordId"`
- 新条件: `v-if="currentDetail.recordId && changeHistoryList.length > 0"`
- 移除空状态提示组件 `<el-empty>`
**代码位置**: 第287行
```vue
<!-- 变更历史记录(仅已完成且有变更记录时显示) -->
<div v-if="currentDetail.recordId && changeHistoryList.length > 0" style="margin-top: 20px">
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold; color: #409eff">
<el-icon style="vertical-align: middle; margin-right: 5px"><EditPen /></el-icon>
变更历史记录
</span>
</el-divider>
<!-- 变更历史列表 -->
<div v-loading="changeHistoryLoading">
<el-table :data="changeHistoryList" border style="width: 100%" max-height="400px">
<!-- 表格列定义 -->
</el-table>
</div>
</div>
```
#### 2. PDF下载按钮
**文件**: `web/src/views/rs/overseas/monitor/index.vue`
**新增内容**:
1. **导入图标**: 添加 `Download` 图标
2. **导入API**: 添加 `downloadApprovalPdf` 方法
3. **弹窗footer布局**: 改为左右布局,左侧显示下载按钮,右侧显示关闭按钮
4. **下载方法**: 添加 `handleDownloadPdf` 方法
**代码位置**: 第350-360行
```vue
<template #footer>
<div style="display: flex; justify-content: space-between; width: 100%">
<div>
<!-- 只有审批完成的申请才显示下载PDF按钮 -->
<el-button
v-if="currentDetail && currentDetail.status === '1'"
type="primary"
icon="Download"
@click="handleDownloadPdf"
v-hasPermi="['rs:overseas:query']">
下载审批表PDF
</el-button>
</div>
<div>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</div>
</div>
</template>
```
**JavaScript方法** (第693-710行):
```javascript
/**
* 下载审批表PDF
*/
const handleDownloadPdf = async () => {
if (!currentDetail.value || !currentDetail.value.applicationId) {
ElMessage.error('无法获取申请ID');
return;
}
try {
ElMessage.info('正在生成PDF,请稍候...');
await downloadApprovalPdf(currentDetail.value.applicationId);
ElMessage.success('PDF下载成功');
} catch (error) {
console.error('下载PDF失败', error);
ElMessage.error('下载PDF失败,请稍后重试');
}
};
```
#### 3. API接口定义
**文件**: `web/src/api/rs/overseas.ts`
**新增内容**: Monitor相关API接口(第1036-1085行)
```typescript
// ==================== 出国申请进度查询(监控)相关API ====================
// 审批监控查询参数类型
export interface OverseasMonitorQuery {
pageNum: number;
pageSize: number;
applicantName?: string;
department?: string;
destination?: string;
approvalLevel?: string;
currentApprover?: string;
status?: string;
applyTimeStart?: string;
applyTimeEnd?: string;
}
// 查询待审批列表(监控)
export function getPendingList(query: OverseasMonitorQuery) {
return request({
url: '/rs/overseas/monitor/pending/list',
method: 'get',
params: query
});
}
// 查询已完成审批列表(监控)
export function getCompletedList(query: OverseasMonitorQuery) {
return request({
url: '/rs/overseas/monitor/completed/list',
method: 'get',
params: query
});
}
// 查询申请详情(监控)
export function getDetail(applicationId: string | number) {
return request({
url: `/rs/overseas/monitor/detail/${applicationId}`,
method: 'get'
});
}
// 下载审批表PDF
export function downloadApprovalPdf(applicationId: string | number) {
return request({
url: `/rs/overseas/monitor/downloadApprovalPdf/${applicationId}`,
method: 'get',
responseType: 'blob'
});
}
```
### 二、后端修改
#### 1. Service接口
**文件**: `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApprovalMonitorService.java`
**新增方法**:
```java
/**
* 下载出国申请审批表PDF
*
* @param applicationId 申请ID
* @param response HTTP响应对象
*/
void downloadApprovalPdf(Long applicationId, HttpServletResponse response);
```
#### 2. Controller接口
**文件**: `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java`
**新增接口** (第119-135行):
```java
/**
* 下载出国申请审批表PDF
* <p>
* 生成并下载"杭州第一技师学院登记报备人员因私事出国(境)审批表"PDF文件
*
* @param applicationId 申请ID
* @param response HTTP响应对象
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadApprovalPdf/{applicationId}")
public void downloadApprovalPdf(
@PathVariable @NotNull(message = "申请ID不能为空") Long applicationId,
HttpServletResponse response) {
log.info("下载出国申请审批表PDF,申请ID:{}", applicationId);
monitorService.downloadApprovalPdf(applicationId, response);
}
```
#### 3. Service实现
**文件**: `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java`
**新增依赖导入**:
```java
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.core.exception.ServiceException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
```
**新增方法** (第521-808行):
1. **`downloadApprovalPdf`**: 主方法,负责查询数据和设置响应头
2. **`generateApprovalPdf`**: PDF生成核心方法
3. **`addCell`**: 添加表格单元格辅助方法
4. **`addApprovalRow`**: 添加审批意见行辅助方法
5. **`getApprovalOpinion`**: 获取指定级别审批意见
6. **`formatDate`**: 格式化日期(yyyy-MM-dd)
7. **`formatBirthDate`**: 格式化出生日期(yyyy年MM月)
**PDF文档结构**:
1. 标题:
- "杭州第一技师学院"
- "登记报备人员因私事出国(境)审批表"
2. 个人信息表格(8列):
- 姓名、性别、出生年月、政治面貌
- 现工作部门(教学系)、职称、职务
- 申领证件名称、证件号码
- 前往国家或地区、出境事由(旅游/探亲)
- 探望对象姓名及与申请者关系
- 出国时间、备注
- 承诺声明、联系电话、应急联系人
3. 审批意见表格(2列):
- 部门(教学系)意见
- 人事(组织)处审核意见
- 纪委意见
- 学院分管领导意见
- 学院党委书记意见
4. 备注说明:
- 因私出境时间说明
- 证件号码填写说明
## 🔧 技术实现细节
### PDF生成技术
- **库**: iText 5.5.13.3
- **中文字体**: STSong-Light (UniGB-UCS2-H)
- **页面尺寸**: A4
- **页边距**: 40pt (上下左右)
### 字体大小
- **标题字体**: 16pt, 加粗
- **副标题字体**: 14pt, 加粗
- **正文字体**: 10pt, 常规
- **小字体**: 9pt, 常规
### 表格设计
- **个人信息表**: 8列,列宽比例 [8:12:6:10:10:12:16:6]
- **审批意见表**: 2列,列宽比例 [20:80]
- **单元格最小高度**: 25-50pt
- **表头背景**: 浅灰色 (BaseColor.LIGHT_GRAY)
### 数据处理
- **性别翻译**: 0→男, 1→女
- **政治面貌翻译**: 支持多种编码格式(01-13, 1-13, 英文代码)
- **出境事由**: 旅游/探亲,用√标记
- **日期格式化**: yyyy-MM-dd / yyyy年MM月
- **审批意见**: 优先显示审批意见,无意见时通过状态显示"同意"
## 📝 权限要求
- **查看权限**: `rs:overseas:query`
- **列表权限**: `rs:overseas:list`
## 🎨 UI/UX优化
1. **按钮位置**: 弹窗footer左右布局,下载按钮在左侧,关闭按钮在右侧
2. **按钮样式**: 主色调按钮(type="primary")+ 下载图标
3. **显示条件**: 仅审批完成状态显示下载按钮
4. **用户提示**:
- 开始下载:"正在生成PDF,请稍候..."
- 下载成功:"PDF下载成功"
- 下载失败:"下载PDF失败,请稍后重试"
## ✅ 测试要点
### 前端测试
1. ✅ 有变更记录时,变更历史区域正常显示
2. ✅ 无变更记录时,变更历史区域不显示
3. ✅ 审批完成状态显示下载PDF按钮
4. ✅ 待审批状态不显示下载PDF按钮
5. ✅ 点击下载按钮能触发PDF下载
6. ✅ 下载过程中显示加载提示
### 后端测试
1. ✅ PDF文件名包含申请人姓名和时间戳
2. ✅ PDF包含完整的个人信息
3. ✅ PDF包含所有审批历史记录
4. ✅ 中文字体正确显示
5. ✅ 表格格式符合模板要求
6. ✅ 审批意见正确填充
7. ✅ 日期格式化正确
## 📊 文件清单
### 前端文件
- ✅ `web/src/views/rs/overseas/monitor/index.vue` (修改)
- ✅ `web/src/api/rs/overseas.ts` (修改,新增monitor相关API)
### 后端文件
- ✅ `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasApprovalMonitorService.java` (修改)
- ✅ `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java` (修改)
- ✅ `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java` (修改)
### 文档文件
- ✅ `web/src/views/rs/overseas/monitor/出国申请进度查询功能优化说明.md` (新增)
## 🚀 部署说明
### 前端部署
```bash
cd web
npm run build
# 部署dist目录到Nginx
```
### 后端部署
```bash
cd backend
mvn clean package -DskipTests
# 重启ruoyi-admin服务
```
### 依赖检查
后端已包含iText PDF生成库依赖:
- `com.itextpdf:itextpdf:5.5.13.3`
- `com.itextpdf:itext-asian:5.2.0` (中文字体支持)
## 📖 相关文档
- **项目规范**: `.cursor/cursorrules.md`
- **开发文档**: `docs/开发文档/API/`
- **出国申请API**: `docs/开发文档/API/出国申请管理API.md`
## 🔗 相关模块
- **出国申请管理**: `/rs/overseas/application`
- **出国记录管理**: `/rs/overseas/record`
- **行程变更管理**: `/rs/itinerary/change`
---
**实施人**: Cursor AI
**审核状态**: ✅ 已完成
**更新日期**: 2025-01-29
@@ -1,394 +0,0 @@
# 出国申请进度查询功能修改说明
## 📋 修改概述
**修改时间**:2025-10-29
**修改目的**:按照审批流程要求,修改出国申请进度查询功能,实现申请人查看自己提交的申请进度
---
## 🎯 功能需求
### 1. 待审批选项卡
- **功能**:用户提交申请后,在这里查看自己的待审批记录
- **显示内容**:
- 当前用户提交的所有待审批申请
- 当前审批环节和审批人
- 审批历史记录
### 2. 审批完成选项卡
- **功能**:审批完成后,抄送一份到这里
- **显示内容**:
- 当前用户提交的所有已完成审批申请
- 完整的审批历史记录
- 关联的出国记录信息
---
## 📝 修改内容
### 1. 后端Service修改
**文件**:`backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApprovalMonitorServiceImpl.java`
#### 修改点1:待审批列表查询
```java
/**
* 查询审批中的申请列表(待审核状态)
* 🔴 只查询当前用户提交的待审批申请
*/
@Override
public TableDataInfo<RsOverseasApprovalMonitorVo> queryPendingList(RsOverseasApprovalMonitorQuery query) {
// 🔴 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
// 构建查询条件
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
wrapper.eq("status", "0"); // 只查询待审核状态
wrapper.eq("applicant_id", currentUserId); // 🔴 只查询当前用户提交的申请
// ... 分页查询和数据处理
}
```
**修改说明**:
- ✅ 添加当前用户ID过滤条件
- ✅ 只查询申请人ID = 当前用户ID的记录
- ✅ 添加日志记录便于排查问题
#### 修改点2:已完成列表查询
```java
/**
* 查询已完成审批的申请列表(已通过状态)
* 🔴 只查询当前用户提交的已完成审批申请(抄送功能)
*/
@Override
public TableDataInfo<RsOverseasApprovalMonitorVo> queryCompletedList(RsOverseasApprovalMonitorQuery query) {
// 🔴 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
// 构建查询条件
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
wrapper.eq("status", "1"); // 只查询已通过状态
wrapper.eq("applicant_id", currentUserId); // 🔴 只查询当前用户提交的申请
// ... 分页查询和数据处理
}
```
**修改说明**:
- ✅ 实现抄送功能:审批完成后,申请人可以在这里查看
- ✅ 添加当前用户ID过滤条件
- ✅ 只显示当前用户提交且已完成审批的申请
---
### 2. 后端Controller修改
**文件**:`backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalMonitorController.java`
#### 修改点:恢复pending接口功能
```java
/**
* 查询待审批的申请列表(出国申请进度查询)
* 🔴 功能说明:查询当前用户提交的待审批申请
*/
@SaCheckPermission("rs:overseas:list")
@GetMapping("/pending/list")
public TableDataInfo<RsOverseasApprovalMonitorVo> pendingList(RsOverseasApprovalMonitorQuery query) {
log.info("查询当前用户的待审批申请列表");
query.setStatus("0");
return monitorService.queryPendingList(query);
}
```
**修改说明**:
- ❌ 删除了工作流迁移提示
- ✅ 恢复正常的查询功能
- ✅ 返回实际数据而不是错误提示
---
### 3. 前端页面修改
**文件**:`web/src/views/rs/overseas/monitor/index.vue`
#### 修改点1:删除工作流迁移提示
```vue
<!-- 🔴 删除的内容 -->
<el-alert
title="功能迁移通知"
description="出国申请监控功能已标准化..."
...
>
<el-button @click="navigateToTaskWaiting">前往待办任务页面</el-button>
</el-alert>
```
**修改说明**:
- ❌ 删除整个Alert提示组件
- ❌ 删除跳转到工作流页面的按钮
#### 修改点2:恢复待审批表格显示
```vue
<!-- 表格 -->
<el-table v-loading="pendingLoading" :data="pendingList" border>
<!-- 之前是 style="display: none" 隐藏的,现在恢复显示 -->
<el-table-column label="申请人" prop="applicantName" ... />
<el-table-column label="所属部门" prop="department" ... />
<!-- ... 其他列 -->
</el-table>
```
**修改说明**:
- ✅ 恢复表格正常显示
- ✅ 删除 `style="display: none"`
- ✅ 显示待审批申请数据
#### 修改点3:恢复查询功能
```javascript
// 🔴 修改前:显示提示信息
const getPendingList = async () => {
ElMessage.info('请使用标准工作流待办页面查看审批任务');
pendingList.value = [];
};
// ✅ 修改后:正常查询数据
const getPendingList = async () => {
pendingLoading.value = true;
try {
const response = await apiGetPendingList(pendingQueryParams);
if (response.code === 200) {
pendingList.value = response.rows || [];
pendingTotal.value = response.total || 0;
} else {
ElMessage.error(response.msg || '获取数据失败');
}
} catch (error) {
console.error('获取待审批列表失败', error);
ElMessage.error('获取数据失败,请稍后重试');
} finally {
pendingLoading.value = false;
}
};
```
**修改说明**:
- ✅ 调用真实API接口
- ✅ 处理loading状态
- ✅ 显示查询结果或错误提示
#### 修改点4:恢复查询和重置按钮功能
```javascript
// 查询按钮
const handlePendingQuery = () => {
pendingQueryParams.pageNum = 1;
getPendingList();
};
// 重置按钮
const resetPendingQuery = () => {
pendingQueryParams.pageNum = 1;
pendingQueryParams.applicantName = undefined;
pendingQueryParams.department = undefined;
pendingQueryParams.destination = undefined;
pendingQueryParams.approvalLevel = undefined;
getPendingList();
};
```
**修改说明**:
- ✅ 恢复正常的查询功能
- ✅ 恢复正常的重置功能
- ❌ 删除工作流跳转提示
#### 修改点5:添加功能说明
```vue
<el-col :span="1.5">
<div class="info-text">
<el-icon><InfoFilled /></el-icon>
<span>这里显示您提交的待审批申请</span>
</div>
</el-col>
```
**修改说明**:
- ✅ 在操作按钮区域添加功能说明
- ✅ 明确告知用户这里只显示自己提交的申请
- ✅ 添加对应的CSS样式
---
## 🎨 UI变化
### 修改前
```
[Alert提示框:功能迁移通知]
[前往待办任务页面按钮]
[Empty: 请使用标准工作流待办页面]
```
### 修改后
```
[搜索条件]
[刷新按钮] [ℹ️ 这里显示您提交的待审批申请]
[数据表格:显示待审批申请列表]
[分页组件]
```
---
## 🔍 数据过滤逻辑
### 待审批选项卡
```sql
SELECT * FROM rs_overseas_application
WHERE status = '0' -- 待审核状态
AND applicant_id = {当前用户ID} -- 🔴 只查询当前用户提交的
AND deleted = '0'
AND tenant_id = {租户ID}
ORDER BY apply_time DESC
```
### 审批完成选项卡
```sql
SELECT * FROM rs_overseas_application
WHERE status = '1' -- 已通过状态
AND applicant_id = {当前用户ID} -- 🔴 只查询当前用户提交的
AND deleted = '0'
AND tenant_id = {租户ID}
ORDER BY apply_time DESC
```
---
## 🧪 测试验证
### 测试场景1:待审批选项卡
**测试步骤**:
1. 使用用户A登录系统
2. 提交一个出国申请
3. 访问"出国申请进度查询"菜单
4. 切换到"待审批中"选项卡
**预期结果**:
- ✅ 能看到刚才提交的申请记录
- ✅ 显示当前审批环节和审批人
- ✅ 点击"查看详情"能查看审批历史
**测试步骤2**:
1. 使用用户B登录系统
2. 访问"出国申请进度查询"菜单
3. 切换到"待审批中"选项卡
**预期结果**:
- ✅ 只能看到用户B提交的申请
- ❌ 看不到用户A提交的申请
### 测试场景2:审批完成选项卡
**测试步骤**:
1. 使用用户A登录系统
2. 提交一个出国申请
3. 完成全部审批流程
4. 访问"出国申请进度查询"菜单
5. 切换到"审批完成"选项卡
**预期结果**:
- ✅ 能看到刚才审批完成的申请(抄送)
- ✅ 显示完整的审批历史记录
- ✅ 能查看关联的出国记录信息
### 测试场景3:搜索功能
**测试步骤**:
1. 在"待审批中"选项卡输入搜索条件
2. 点击"搜索"按钮
**预期结果**:
- ✅ 能正常搜索并返回结果
- ✅ 结果仍然只包含当前用户提交的申请
---
## 📊 修改文件清单
| 序号 | 文件类型 | 文件路径 | 修改内容 | 状态 |
|------|---------|---------|---------|------|
| 1 | 后端Service | `RsOverseasApprovalMonitorServiceImpl.java` | 添加当前用户过滤条件 | ✅ |
| 2 | 后端Controller | `RsOverseasApprovalMonitorController.java` | 恢复pending接口功能 | ✅ |
| 3 | 前端页面 | `monitor/index.vue` | 删除工作流提示,恢复表格显示 | ✅ |
| 4 | 前端脚本 | `monitor/index.vue` | 恢复查询、重置功能 | ✅ |
---
## ⚠️ 注意事项
### 1. 权限要求
- 用户需要有 `rs:overseas:list` 权限才能访问此菜单
- 用户需要有 `rs:overseas:query` 权限才能查看详情
### 2. 数据范围
- **待审批选项卡**:只显示当前用户提交的待审核申请
- **审批完成选项卡**:只显示当前用户提交的已完成申请
- 不会显示其他用户提交的申请
### 3. 与其他模块的关系
- **出国申请**:用户在这里提交申请
- **出国审批**:审批人在这里审批申请
- **出国记录**:审批完成后,申请人在这里查看出国记录
- **出国申请进度查询**:申请人在这里查看申请进度(当前模块)
---
## 🚀 部署说明
### 后端部署
```bash
cd backend
mvn clean package -pl ruoyi-admin -am -DskipTests
./stop-main.sh
./start-main.sh
```
### 前端部署
```bash
cd web
npm run build
# 将dist目录部署到nginx
```
### 验证步骤
1. 清除浏览器缓存
2. 重新登录系统
3. 访问"出国申请进度查询"菜单
4. 确认功能正常
---
## ✅ 验收标准
- [ ] 待审批选项卡只显示当前用户提交的待审批申请
- [ ] 审批完成选项卡只显示当前用户提交的已完成审批申请
- [ ] 搜索功能正常工作
- [ ] 重置功能正常工作
- [ ] 查看详情能显示审批历史
- [ ] 不会显示其他用户的申请
- [ ] UI显示正常,无工作流迁移提示
---
## 📞 问题反馈
如有问题,请检查:
1. 后端日志:`backend/logs/sys-info.log`
2. 浏览器控制台:F12查看网络请求和错误
3. 确认用户权限配置正确
---
**文档版本**:v1.0
**最后更新**:2025-10-29
**修改人员**:Cursor AI
@@ -1,411 +0,0 @@
# 出国申请审批环节命名标准分析报告
## 📋 问题发现
在分析代码时,发现系统中**存在两套不同的审批级别命名标准**:
1. **出国申请进度查询模块**:使用**3级审批**命名
2. **出国申请审批模块**:使用**5级审批**命名
这导致**命名不一致**的问题。
---
## 🔍 详细分析
### 一、出国申请进度查询模块(monitor)
**文件位置**:`RsOverseasApprovalMonitorServiceImpl.java`
**当前定义**(第390-404行):
```java
private String getApprovalLevelText(String level) {
if (level == null) {
return "未开始";
}
switch (level) {
case "1":
return "部门主管审批";
case "2":
return "分管领导审批";
case "3":
return "校领导审批";
default:
return "未知";
}
}
```
**问题**:
- ❌ 只定义了3个级别
- ❌ Level 4 和 Level 5 会显示为"未知"
- ❌ 命名与实际审批流程不符
---
### 二、出国申请审批模块(application)
**文件位置**:`RsOverseasApplicationServiceImpl.java`
**当前定义**(第1269-1283行):
```java
private String getApprovalLevelDescription(String approvalLevel) {
switch (approvalLevel) {
case "1":
return "待部门负责人审批";
case "2":
return "待人事处审批";
case "3":
return "待纪委审批";
case "4":
return "待学院分管领导审批";
case "5":
return "待学院党委书记审批";
default:
return "待审批";
}
}
```
**流转逻辑**(第683-698行):
```java
/**
* 获取下一级审批级别
* 五级审批制度:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
*/
private String getNextApprovalLevel(String currentLevel) {
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;
}
}
```
---
## 📊 标准审批级别定义
### 完整的五级审批制度
| 审批级别 | 数据库值 | 权限标识 | 审批环节名称 | 当前审批人显示 | 说明 |
|---------|---------|---------|------------|--------------|------|
| **Level 1** | `"1"` | `rs:overseas:approve1` | **部门负责人审批** | 待部门负责人审批 | 只能看到本部门的申请 |
| **Level 2** | `"2"` | `rs:overseas:approve2` | **人事处审批** | 待人事处审批 | 可以看到所有部门的申请 |
| **Level 3** | `"3"` | `rs:overseas:approve3` | **纪委审批** | 待纪委审批 | 可以看到所有部门的申请 |
| **Level 4** | `"4"` | `rs:overseas:approve4` | **学院分管领导审批** | 待学院分管领导审批 | 可以看到所有部门的申请 |
| **Level 5** | `"5"` | `rs:overseas:approve5` | **学院党委书记审批** | 待学院党委书记审批 | 可以看到所有部门的申请 |
---
## 🎯 审批流程完整流转
### 流程图
```
申请提交
↓
┌─────────────────────────────────┐
│ Level 1: 部门负责人审批 │
│ - 审批人:各部门负责人 │
│ - 权限:rs:overseas:approve1 │
│ - 范围:仅本部门申请 │
└─────────────────────────────────┘
↓ (审批通过)
┌─────────────────────────────────┐
│ Level 2: 人事处审批 │
│ - 审批人:人事处工作人员 │
│ - 权限:rs:overseas:approve2 │
│ - 范围:所有部门申请 │
└─────────────────────────────────┘
↓ (审批通过)
┌─────────────────────────────────┐
│ Level 3: 纪委审批 │
│ - 审批人:纪委工作人员 │
│ - 权限:rs:overseas:approve3 │
│ - 范围:所有部门申请 │
└─────────────────────────────────┘
↓ (审批通过)
┌─────────────────────────────────┐
│ Level 4: 学院分管领导审批 │
│ - 审批人:学院分管领导 │
│ - 权限:rs:overseas:approve4 │
│ - 范围:所有部门申请 │
└─────────────────────────────────┘
↓ (审批通过)
┌─────────────────────────────────┐
│ Level 5: 学院党委书记审批 │
│ - 审批人:学院党委书记 │
│ - 权限:rs:overseas:approve5 │
│ - 范围:所有部门申请 │
└─────────────────────────────────┘
↓ (审批通过)
申请完成 → 生成出国记录
```
---
## 📝 各字段的具体命名标准
### 1. **当前审批环节**(approval_level字段)
**数据库字段**:`approval_level VARCHAR(10)`
**标准命名**(用于显示):
```java
switch (approvalLevel) {
case "1": return "部门负责人审批";
case "2": return "人事处审批";
case "3": return "纪委审批";
case "4": return "学院分管领导审批";
case "5": return "学院党委书记审批";
default: return "未知";
}
```
### 2. **当前审批人**(current_approver字段)
**数据库字段**:`current_approver VARCHAR(100)`
**标准命名格式**:
```java
switch (approvalLevel) {
case "1": return "待部门负责人审批";
case "2": return "待人事处审批";
case "3": return "待纪委审批";
case "4": return "待学院分管领导审批";
case "5": return "待学院党委书记审批";
default: return "待审批";
}
```
**特殊情况**:
- 如果是部门负责人级别,可能显示:`"待指定部门负责人"` 或 `"待{具体审批人姓名}审批"`
- 其他级别通常显示:`"待{审批环节}审批"`
### 3. **审批进度**(用于进度查询)
**显示格式**:`"已审批级别数/总级别数"`
**示例**:
- `"0/5"` - 未开始审批
- `"1/5"` - 部门负责人已审批
- `"2/5"` - 人事处已审批
- `"3/5"` - 纪委已审批
- `"4/5"` - 学院分管领导已审批
- `"5/5"` - 全部审批完成
---
## 🔧 数据库定义
### rs_overseas_application表字段
```sql
-- 审批级别字段
`approval_level` varchar(10) DEFAULT '1'
COMMENT '当前审批级别:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记'
-- 当前审批人字段
`current_approver` varchar(100) DEFAULT NULL
COMMENT '当前审批人姓名或环节描述'
-- 当前审批人ID字段(冗余字段,仅供参考)
`current_approver_id` bigint DEFAULT NULL
COMMENT '当前审批人ID(冗余字段)'
```
---
## 🎨 前端显示示例
### 待审批列表显示
| 申请人 | 所属部门 | 当前审批环节 | 当前审批人 | 审批进度 |
|--------|---------|-------------|-----------|---------|
| 张三 | 教务处 | 部门负责人审批 | 待部门负责人审批 | 0% (0/5) |
| 李四 | 人事处 | 人事处审批 | 待人事处审批 | 20% (1/5) |
| 王五 | 学工处 | 纪委审批 | 待纪委审批 | 40% (2/5) |
| 赵六 | 后勤处 | 学院分管领导审批 | 待学院分管领导审批 | 60% (3/5) |
| 钱七 | 财务处 | 学院党委书记审批 | 待学院党委书记审批 | 80% (4/5) |
### 审批历史记录显示
```
📋 审批历史
✅ 第1级:部门负责人审批
审批人:张主任
审批时间:2025-10-27 10:30:25
审批结果:通过
审批意见:同意出国交流
✅ 第2级:人事处审批
审批人:李处长
审批时间:2025-10-27 14:20:15
审批结果:通过
审批意见:人事档案无问题,同意
⏳ 第3级:纪委审批
状态:待审批
⏸️ 第4级:学院分管领导审批
状态:未到审批阶段
⏸️ 第5级:学院党委书记审批
状态:未到审批阶段
```
---
## ⚠️ 当前存在的问题
### 问题1:命名不统一
**问题描述**:
- `RsOverseasApprovalMonitorServiceImpl.java` 中只定义了3个级别
- 与实际的5级审批流程不符
**影响范围**:
- 出国申请进度查询菜单的"当前审批环节"显示
- Level 4 和 Level 5 会显示为"未知"
**解决方案**:
需要修改 `getApprovalLevelText()` 方法,添加完整的5级定义
### 问题2:总审批级别数设置错误
**问题位置**:`RsOverseasApprovalMonitorServiceImpl.java` 第345-346行
```java
// 总审批级别数(假设最多3级:部门主管、分管领导、校领导)
vo.setTotalLevels(3);
```
**问题**:
- 实际是5级审批,但这里设置为3级
- 导致审批进度显示错误(如:2/3 而不是 2/5)
**影响**:
- 审批进度百分比计算错误
- 进度条显示不准确
---
## ✅ 建议的标准命名
### 统一的命名标准
| 级别 | 简称 | 完整名称 | 待审批显示 | 已审批显示 |
|------|------|---------|-----------|-----------|
| 1 | 部门负责人 | 部门负责人审批 | 待部门负责人审批 | 部门负责人已审批 |
| 2 | 人事处 | 人事处审批 | 待人事处审批 | 人事处已审批 |
| 3 | 纪委 | 纪委审批 | 待纪委审批 | 纪委已审批 |
| 4 | 学院分管领导 | 学院分管领导审批 | 待学院分管领导审批 | 学院分管领导已审批 |
| 5 | 学院党委书记 | 学院党委书记审批 | 待学院党委书记审批 | 学院党委书记已审批 |
### 代码实现建议
```java
/**
* 获取审批级别完整名称(用于"当前审批环节"列)
*/
private String getApprovalLevelText(String level) {
if (level == null) {
return "未开始";
}
switch (level) {
case "1":
return "部门负责人审批";
case "2":
return "人事处审批";
case "3":
return "纪委审批";
case "4":
return "学院分管领导审批";
case "5":
return "学院党委书记审批";
default:
return "未知";
}
}
/**
* 获取当前审批人显示文本(用于"当前审批人"列)
*/
private String getCurrentApproverText(String level) {
if (level == null) {
return "未指定";
}
switch (level) {
case "1":
return "待部门负责人审批";
case "2":
return "待人事处审批";
case "3":
return "待纪委审批";
case "4":
return "待学院分管领导审批";
case "5":
return "待学院党委书记审批";
default:
return "待审批";
}
}
```
---
## 📋 总结
### 当前审批级别定义
**完整的五级审批体系**:
1. **Level 1 - 部门负责人审批**
- 权限:`rs:overseas:approve1`
- 范围:仅本部门申请
- 显示:部门负责人审批
2. **Level 2 - 人事处审批**
- 权限:`rs:overseas:approve2`
- 范围:所有部门申请
- 显示:人事处审批
3. **Level 3 - 纪委审批**
- 权限:`rs:overseas:approve3`
- 范围:所有部门申请
- 显示:纪委审批
4. **Level 4 - 学院分管领导审批**
- 权限:`rs:overseas:approve4`
- 范围:所有部门申请
- 显示:学院分管领导审批
5. **Level 5 - 学院党委书记审批**
- 权限:`rs:overseas:approve5`
- 范围:所有部门申请
- 显示:学院党委书记审批
### 关键注意事项
- ✅ 系统使用**5级审批制度**
- ✅ 只有**部门负责人(Level 1)**需要部门过滤
- ✅ **Level 2-5** 可以看到所有部门的申请
- ⚠️ 出国申请进度查询模块存在命名不一致问题
- ⚠️ 总审批级别数应该是5而不是3
---
**分析完成时间**:2025-10-29
**分析人员**:Cursor AI
**文档版本**:v1.0
@@ -1,286 +0,0 @@
# 出国申请进度查询 - 审批级别修复说明
## 🔧 修复时间
2025-10-29
## 🎯 修复目标
将出国申请进度查询模块从3级审批改为完整的5级审批,与实际审批流程保持一致。
---
## 📝 修复内容
### 修复1:更新审批级别文本定义
**文件**:`RsOverseasApprovalMonitorServiceImpl.java`
**方法**:`getApprovalLevelText(String level)`
#### 修复前
```java
switch (level) {
case "1":
return "部门主管审批";
case "2":
return "分管领导审批";
case "3":
return "校领导审批";
default:
return "未知";
}
```
**问题**:
- ❌ Level 4 和 Level 5 显示为"未知"
- ❌ 命名与实际流程不符
#### 修复后
```java
/**
* 获取审批级别文本
* 五级审批制度:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
*/
switch (level) {
case "1":
return "部门负责人审批";
case "2":
return "人事处审批";
case "3":
return "纪委审批";
case "4":
return "学院分管领导审批";
case "5":
return "学院党委书记审批";
default:
return "未知";
}
```
**改进**:
- ✅ 支持完整的5级审批
- ✅ 命名与实际流程一致
- ✅ 添加了详细的注释说明
---
### 修复2:更新总审批级别数
**文件**:`RsOverseasApprovalMonitorServiceImpl.java`
**方法**:`fillApprovalProgress()`
#### 修复前
```java
// 总审批级别数(假设最多3级:部门主管、分管领导、校领导)
vo.setTotalLevels(3);
// 未开始审批
vo.setApprovedLevels(0);
vo.setTotalLevels(3);
vo.setApprovalProgress("0/3");
```
**问题**:
- ❌ 总级别数设置为3
- ❌ 审批进度显示错误(如:2/3 而不是 2/5)
- ❌ 进度百分比计算错误
#### 修复后
```java
// 总审批级别数(五级审批制度:部门负责人、人事处、纪委、学院分管领导、学院党委书记)
vo.setTotalLevels(5);
// 未开始审批
vo.setApprovedLevels(0);
vo.setTotalLevels(5);
vo.setApprovalProgress("0/5");
```
**改进**:
- ✅ 总级别数正确设置为5
- ✅ 审批进度显示正确
- ✅ 进度百分比计算准确
---
### 修复3:更新统计信息定义
**文件**:`RsOverseasApprovalMonitorServiceImpl.java`
**方法**:`getApprovalStatistics()`
#### 修复前
```java
// 2. 各审批级别的待审批数量
Map<String, Long> pendingByLevel = new HashMap<>();
pendingByLevel.put("level1", countByStatusAndLevel("0", "1", tenantId)); // 部门主管
pendingByLevel.put("level2", countByStatusAndLevel("0", "2", tenantId)); // 分管领导
pendingByLevel.put("level3", countByStatusAndLevel("0", "3", tenantId)); // 校领导
statistics.put("pendingByLevel", pendingByLevel);
```
**问题**:
- ❌ 只统计3个级别
- ❌ Level 4 和 Level 5 的统计缺失
#### 修复后
```java
// 2. 各审批级别的待审批数量(五级审批制度)
Map<String, Long> pendingByLevel = new HashMap<>();
pendingByLevel.put("level1", countByStatusAndLevel("0", "1", tenantId)); // 部门负责人
pendingByLevel.put("level2", countByStatusAndLevel("0", "2", tenantId)); // 人事处
pendingByLevel.put("level3", countByStatusAndLevel("0", "3", tenantId)); // 纪委
pendingByLevel.put("level4", countByStatusAndLevel("0", "4", tenantId)); // 学院分管领导
pendingByLevel.put("level5", countByStatusAndLevel("0", "5", tenantId)); // 学院党委书记
statistics.put("pendingByLevel", pendingByLevel);
```
**改进**:
- ✅ 统计所有5个级别
- ✅ 统计数据完整准确
- ✅ 命名规范一致
---
## 📊 修复效果对比
### 1. 当前审批环节显示
| 审批级别 | 修复前 | 修复后 | 状态 |
|---------|--------|--------|------|
| Level 1 | 部门主管审批 | 部门负责人审批 | ✅ 已修正 |
| Level 2 | 分管领导审批 | 人事处审批 | ✅ 已修正 |
| Level 3 | 校领导审批 | 纪委审批 | ✅ 已修正 |
| Level 4 | **未知** ❌ | 学院分管领导审批 | ✅ 已修复 |
| Level 5 | **未知** ❌ | 学院党委书记审批 | ✅ 已修复 |
### 2. 审批进度显示
| 已审批级别 | 修复前 | 修复后 | 进度百分比(修复前) | 进度百分比(修复后) |
|-----------|--------|--------|-------------------|-------------------|
| 0级 | 0/3 | 0/5 | 0% | 0% |
| 1级 | 1/3 | 1/5 | 33% ❌ | 20% ✅ |
| 2级 | 2/3 | 2/5 | 67% ❌ | 40% ✅ |
| 3级 | 3/3 | 3/5 | 100% ❌ | 60% ✅ |
| 4级 | - | 4/5 | - | 80% ✅ |
| 5级 | - | 5/5 | - | 100% ✅ |
### 3. 统计信息
| 统计项 | 修复前 | 修复后 |
|--------|--------|--------|
| 统计级别数 | 3个 | 5个 ✅ |
| Level 4统计 | ❌ 缺失 | ✅ 包含 |
| Level 5统计 | ❌ 缺失 | ✅ 包含 |
---
## 🎨 前端显示效果
### 待审批列表示例
#### 修复前
| 申请人 | 当前审批环节 | 审批进度 |
|--------|-------------|---------|
| 张三 | 部门主管审批 | 0/3 (0%) |
| 李四 | 分管领导审批 | 1/3 (33%) |
| 王五 | 校领导审批 | 2/3 (67%) |
| 赵六 | **未知** ❌ | 3/3 (100%) ❌ |
| 钱七 | **未知** ❌ | - |
#### 修复后
| 申请人 | 当前审批环节 | 审批进度 |
|--------|-------------|---------|
| 张三 | 部门负责人审批 | 0/5 (0%) |
| 李四 | 人事处审批 | 1/5 (20%) |
| 王五 | 纪委审批 | 2/5 (40%) |
| 赵六 | 学院分管领导审批 ✅ | 3/5 (60%) ✅ |
| 钱七 | 学院党委书记审批 ✅ | 4/5 (80%) ✅ |
---
## ✅ 修复验证
### 验证步骤
1. **重新编译后端**
```bash
cd backend
mvn clean package -pl ruoyi-admin -am -DskipTests
```
2. **重启后端服务**
```bash
./stop-main.sh
./start-main.sh
```
3. **登录系统测试**
- 访问"出国申请进度查询"菜单
- 查看待审批中和审批完成选项卡
- 验证以下内容:
#### 验证项1:当前审批环节显示
- [ ] Level 1 显示为"部门负责人审批"
- [ ] Level 2 显示为"人事处审批"
- [ ] Level 3 显示为"纪委审批"
- [ ] Level 4 显示为"学院分管领导审批"(不再是"未知")
- [ ] Level 5 显示为"学院党委书记审批"(不再是"未知")
#### 验证项2:审批进度显示
- [ ] 未审批显示:0/5 (0%)
- [ ] 部门负责人已审批:1/5 (20%)
- [ ] 人事处已审批:2/5 (40%)
- [ ] 纪委已审批:3/5 (60%)
- [ ] 学院分管领导已审批:4/5 (80%)
- [ ] 学院党委书记已审批:5/5 (100%)
#### 验证项3:统计信息
- [ ] 统计信息包含所有5个级别
- [ ] Level 4 和 Level 5 的统计数据正常显示
---
## 📋 修改文件清单
| 序号 | 文件 | 修改内容 | 行数 | 状态 |
|------|------|---------|------|------|
| 1 | `RsOverseasApprovalMonitorServiceImpl.java` | 更新审批级别文本定义 | 387-404 | ✅ |
| 2 | `RsOverseasApprovalMonitorServiceImpl.java` | 更新总审批级别数(已审批) | 345-346 | ✅ |
| 3 | `RsOverseasApprovalMonitorServiceImpl.java` | 更新总审批级别数(未审批) | 358-360 | ✅ |
| 4 | `RsOverseasApprovalMonitorServiceImpl.java` | 更新统计信息定义 | 152-157 | ✅ |
---
## 🎯 修复总结
### 修复前的问题
- ❌ 只支持3级审批,Level 4和5显示"未知"
- ❌ 总审批级别数错误(3而不是5)
- ❌ 审批进度百分比计算错误
- ❌ 统计信息不完整
### 修复后的改进
- ✅ 完整支持5级审批流程
- ✅ 所有审批级别正确显示
- ✅ 审批进度计算准确
- ✅ 统计信息完整
- ✅ 与实际业务流程一致
### 影响范围
- ✅ 仅修改出国申请进度查询模块
- ✅ 不影响其他模块
- ✅ 向后兼容
---
## 📞 相关文档
- 详细分析报告:`审批环节命名标准分析报告.md`
- 功能修改说明:`出国申请进度查询功能修改说明.md`
- 权限修改说明:`权限修改说明.md`
---
**修复状态**:✅ 已完成
**需要操作**:重新编译和重启后端服务
**版本**:v1.2
**最后更新**:2025-10-29
@@ -1,183 +0,0 @@
# 出国申请进度查询权限修改说明
## 📋 修改时间
2025-10-29
## 🎯 修改目的
将"出国申请进度查询"从"只能查看自己提交的申请"改为"拥有权限的用户可以查看所有申请"
---
## 📝 修改内容
### 1. 后端Service层修改
**文件**:`RsOverseasApprovalMonitorServiceImpl.java`
#### 修改前
```java
// 只查询当前用户提交的申请
Long currentUserId = LoginHelper.getUserId();
wrapper.eq("applicant_id", currentUserId);
```
#### 修改后
```java
// 不再限制申请人,拥有权限的用户可以看到所有数据
// 已删除 applicant_id 过滤条件
```
**影响方法**:
- ✅ `queryPendingList()` - 待审批列表
- ✅ `queryCompletedList()` - 已完成列表
---
### 2. 后端Controller层修改
**文件**:`RsOverseasApprovalMonitorController.java`
#### 修改点
- 更新注释说明:从"当前用户提交的"改为"所有"
- 更新日志输出:从"当前用户的"改为"所有"
---
### 3. 前端页面修改
**文件**:`monitor/index.vue`
#### 修改点
- 提示文字:从"这里显示您提交的"改为"这里显示所有"
---
## 🔍 权限控制
### 访问控制
- **权限标识**:`rs:overseas:list`
- **控制级别**:拥有此权限的用户可以查看所有数据
- **数据范围**:不再限制申请人,显示所有待审批和已完成的申请
### 查询逻辑
```sql
-- 待审批列表
SELECT * FROM rs_overseas_application
WHERE status = '0' -- 待审核状态
AND deleted = '0'
AND tenant_id = {租户ID}
ORDER BY apply_time DESC
-- 已完成列表
SELECT * FROM rs_overseas_application
WHERE status = '1' -- 已通过状态
AND deleted = '0'
AND tenant_id = {租户ID}
ORDER BY apply_time DESC
```
---
## ✅ 修改对比
| 项目 | 修改前 | 修改后 |
|------|--------|--------|
| 数据范围 | 当前用户提交的申请 | 所有申请 |
| 权限要求 | rs:overseas:list | rs:overseas:list(不变) |
| 查询条件 | applicant_id = 当前用户ID | 无申请人限制 |
| 用户体验 | 只能看到自己的 | 可以看到所有 |
---
## 🧪 测试验证
### 测试场景1:查看待审批列表
**测试步骤**:
1. 用户A登录系统(拥有 rs:overseas:list 权限)
2. 访问"出国申请进度查询"菜单
3. 切换到"待审批中"选项卡
**预期结果**:
- ✅ 能看到所有用户提交的待审批申请
- ✅ 包括用户A自己提交的和其他用户提交的
### 测试场景2:查看已完成列表
**测试步骤**:
1. 用户A登录系统(拥有 rs:overseas:list 权限)
2. 访问"出国申请进度查询"菜单
3. 切换到"审批完成"选项卡
**预期结果**:
- ✅ 能看到所有已完成审批的申请
- ✅ 不限制申请人
### 测试场景3:无权限用户
**测试步骤**:
1. 用户B登录系统(没有 rs:overseas:list 权限)
2. 尝试访问"出国申请进度查询"菜单
**预期结果**:
- ❌ 无法访问该菜单(权限控制在菜单级别)
---
## 📊 修改文件清单
| 序号 | 文件类型 | 文件路径 | 修改内容 | 状态 |
|------|---------|---------|---------|------|
| 1 | 后端Service | `RsOverseasApprovalMonitorServiceImpl.java` | 删除申请人ID过滤 | ✅ |
| 2 | 后端Controller | `RsOverseasApprovalMonitorController.java` | 更新注释和日志 | ✅ |
| 3 | 前端页面 | `monitor/index.vue` | 更新提示文字 | ✅ |
---
## ⚠️ 注意事项
### 1. 权限配置
- 确保只有需要查看所有数据的用户才有 `rs:overseas:list` 权限
- 如果需要区分不同权限级别,建议创建新的权限标识
### 2. 数据安全
- 所有查询仍然受租户ID限制
- 不会跨租户显示数据
### 3. 性能考虑
- 查询所有数据时,建议使用搜索条件过滤
- 分页显示可以减轻服务器压力
---
## 🚀 部署说明
### 后端部署
```bash
cd backend
mvn clean package -pl ruoyi-admin -am -DskipTests
./stop-main.sh
./start-main.sh
```
### 前端(如已运行无需重启)
```bash
# 前端代码修改后会自动热更新
# 如需打包部署:
cd web
npm run build
```
---
## 📞 相关文档
- 详细修改说明:`出国申请进度查询功能修改说明.md`
- 快速参考:`修改总结-快速参考.md`
---
**修改状态**:✅ 已完成
**生效方式**:需要重启后端服务
**版本**:v1.1