diff --git a/backend/ruoyi-admin/src/main/resources/application.yml b/backend/ruoyi-admin/src/main/resources/application.yml index d5dc137f..f6bde346 100644 --- a/backend/ruoyi-admin/src/main/resources/application.yml +++ b/backend/ruoyi-admin/src/main/resources/application.yml @@ -12,10 +12,16 @@ server: # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的 max-http-post-size: -1 # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 - # 每块buffer的空间大小,越小的空间被利用越充分 - buffer-size: 512 + # 每块buffer的空间大小(标准配置:16KB,提升文件上传性能) + buffer-size: 16384 # 是否分配的直接内存 direct-buffers: true + # 请求处理超时时间(毫秒),0表示无限制,建议设置合理超时避免长时间阻塞 + no-request-timeout: 600000 + # 🔧 修复文件上传超时:阻塞写操作超时时间(毫秒),适用于大文件上传或慢网络环境 + options: + # 阻塞写操作超时时间设置为10分钟,避免上传大文件时触发默认300秒超时 + blocking-write-timeout: 600000 threads: # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 io: 8 @@ -75,10 +81,16 @@ spring: # 文件上传 servlet: multipart: + # 是否启用文件上传 + enabled: true # 单个文件大小 max-file-size: 10MB # 设置总上传的文件大小 max-request-size: 20MB + # 上传文件的临时目录 + location: ${java.io.tmpdir} + # 文件大小阈值,超过该阈值将写入磁盘而非内存(0表示始终写入磁盘) + file-size-threshold: 0 mvc: # 设置静态资源路径 防止所有请求都去查静态资源 static-path-pattern: /static/** diff --git a/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java b/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java index e4ebc6ec..4a8913da 100644 --- a/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java +++ b/backend/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java @@ -21,6 +21,8 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.transfer.s3.S3TransferManager; import software.amazon.awssdk.transfer.s3.model.*; @@ -32,6 +34,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; /** @@ -93,10 +96,10 @@ public class OssClient { .httpClient(NettyNioAsyncHttpClient.builder() // 🚀 优化:连接超时时间(60秒) .connectionTimeout(Duration.ofSeconds(60)) - // 🚀 优化:读取超时时间(300秒,适合大文件下载) - .readTimeout(Duration.ofSeconds(300)) - // 🚀 优化:写入超时时间(300秒,适合大文件上传) - .writeTimeout(Duration.ofSeconds(300)) + // 🚀 优化:读取超时时间(600秒,适合大文件下载和慢网络环境) + .readTimeout(Duration.ofSeconds(600)) + // 🚀 优化:写入超时时间(600秒,适合大文件上传和慢网络环境) + .writeTimeout(Duration.ofSeconds(600)) .build()) .build(); @@ -184,31 +187,31 @@ public class OssClient { inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); } try { - // 创建异步请求体(length如果为空会报错) + // 🔧 修复:创建异步请求体,设置超时时间为600秒(10分钟),适应慢网络环境 BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder() .contentLength(length) - .subscribeTimeout(Duration.ofSeconds(30)) + .subscribeTimeout(Duration.ofSeconds(600)) .build(); - // 使用 transferManager 进行上传 - Upload upload = transferManager.upload( - x -> x.requestBody(body) - .putObjectRequest( - y -> y.bucket(properties.getBucketName()) - .key(key) - .contentType(contentType) - // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, - // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) - // .acl(getAccessPolicy().getObjectCannedACL()) - .build()) - .build()); + // 🔧 修复:构建上传请求对象 + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(properties.getBucketName()) + .key(key) + .contentType(contentType) + // 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同, + // 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置) + // .acl(getAccessPolicy().getObjectCannedACL()) + .build(); - // 将输入流写入请求体 + // 🔧 修复:先发起上传请求(重要!必须在writeInputStream之前) + CompletableFuture future = client.putObject(putObjectRequest, body); + + // 🔧 修复:在发起请求后立即写入数据流(正确顺序) body.writeInputStream(inputStream); // 等待文件上传操作完成 - CompletedUpload uploadResult = upload.completionFuture().join(); - String eTag = uploadResult.response().eTag(); + PutObjectResponse response = future.join(); + String eTag = response.eTag(); // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build(); diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java index 5ef39ac3..361aa041 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java @@ -347,4 +347,96 @@ public class RsTrainingApplicationController extends BaseController { log.info("重新提交被拒绝的培训申请: applicationId={}", applicationId); return toAjax(trainingApplicationService.resubmitApplication(applicationId, application)); } + + // ======================================== + // 7级审批接口(OR模式权限) + // ======================================== + + /** + * 审批培训申请(7级审批制度) + * 🔴 支持7级审批权限:满足任一级别权限即可访问接口,具体级别在Service层验证 + * + * 审批级别: + * - 第1级:部门负责人 (rs:training:approve1) + * - 第2A级:教务处 (rs:training:approve2a) - 培训类型=教学 + * - 第2B级:学生处 (rs:training:approve2b) - 培训类型=德育 + * - 第3级:计划财务处 (rs:training:approve3) + * - 第4级:人事处 (rs:training:approve4) + * - 第5级:业务分管领导 (rs:training:approve5) + * - 第6级:人事分管院领导 (rs:training:approve6) + * - 第7级:院长 (rs:training:approve7) - 最终审批 + */ + @SaCheckPermission(value = { + "rs:training:approve", // 通用审批权限(超级管理员) + "rs:training:approve1", // 第1级:部门负责人 + "rs:training:approve2a", // 第2A级:教务处 + "rs:training:approve2b", // 第2B级:学生处 + "rs:training:approve3", // 第3级:计划财务处 + "rs:training:approve4", // 第4级:人事处 + "rs:training:approve5", // 第5级:业务分管领导 + "rs:training:approve6", // 第6级:人事分管院领导 + "rs:training:approve7" // 第7级:院长 + }, mode = cn.dev33.satoken.annotation.SaMode.OR) // 🔴 关键:OR模式,满足任一权限即可通过 + @Log(title = "审批培训申请", businessType = BusinessType.UPDATE) + @PostMapping("/approve") + public R approve(@RequestBody java.util.Map request) { + try { + // 解析请求参数 + Long applicationId = Long.parseLong(request.get("applicationId").toString()); + String approvalStatus = request.get("approvalStatus").toString(); + String approvalOpinion = request.get("approvalOpinion") != null ? request.get("approvalOpinion").toString() + : ""; + String approvalSignature = request.get("approvalSignature") != null + ? request.get("approvalSignature").toString() + : ""; + + // 获取当前审批人信息 + Long approverId = LoginHelper.getUserId(); + String approverName = LoginHelper.getUsername(); + + log.info("审批培训申请 - 申请ID: {}, 审批人: {} ({}), 审批状态: {}", + applicationId, approverName, approverId, approvalStatus); + + // 调用Service层审批方法 + Boolean result = trainingApplicationService.approveApplication( + applicationId, approverId, approverName, + approvalStatus, approvalOpinion, approvalSignature); + + return result ? R.ok() : R.fail("审批失败"); + } catch (Exception e) { + log.error("审批培训申请失败: {}", e.getMessage(), e); + return R.fail("审批失败: " + e.getMessage()); + } + } + + /** + * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) + * + * 权限说明: + * - 第1级审批人:只能看到本部门员工的待审批申请 + * - 第2-7级审批人:可以看到全校的待审批申请 + */ + @SaCheckPermission(value = { + "rs:training:approve", + "rs:training:approve1", + "rs:training:approve2a", + "rs:training:approve2b", + "rs:training:approve3", + "rs:training:approve4", + "rs:training:approve5", + "rs:training:approve6", + "rs:training:approve7" + }, mode = cn.dev33.satoken.annotation.SaMode.OR) + @GetMapping("/pending") + public R> queryPending() { + try { + List result = trainingApplicationService.queryPendingByCurrentUser(); + log.info("查询待审批申请 - 用户: {}, 待审批数量: {}", + LoginHelper.getUsername(), result.size()); + return R.ok(result); + } catch (Exception e) { + log.error("查询待审批申请失败: {}", e.getMessage(), e); + return R.fail("查询失败: " + e.getMessage()); + } + } } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java index b3677f55..cd44229d 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java @@ -51,6 +51,12 @@ public class RsTrainingApplication extends BaseEntity { */ private String userName; + /** + * 申请人昵称(用户真实姓名,非数据库字段) + */ + @TableField(exist = false) + private String nickName; + /** * 所属部门 */ @@ -127,23 +133,23 @@ public class RsTrainingApplication extends BaseEntity { * 当前审批人 */ private String currentApprover; - + /** * 培训地点 */ private String location; - + /** * 学时 */ private Integer duration; - + /** * 开始时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date startTime; - + /** * 结束时间 */ @@ -154,7 +160,7 @@ public class RsTrainingApplication extends BaseEntity { * 租户ID */ private String tenantId; - + /** * 拒绝时间 */ @@ -165,10 +171,26 @@ public class RsTrainingApplication extends BaseEntity { * 附件ID列表(逗号分隔) */ private String attachmentIds; - + + /** + * 当前审批级别(1-7,2a-教务处,2b-学生处) + */ + private String approvalLevel; + + /** + * 当前审批人ID + */ + private Long currentApproverId; + + /** + * 审批完成时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date approvalTime; + /** * 排除状态(查询时排除指定状态,不存储到数据库) */ @TableField(exist = false) private String excludeStatus; -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java index 473e20f7..e854f3f3 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApproval.java @@ -9,6 +9,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity; import com.fasterxml.jackson.annotation.JsonFormat; import java.util.Date; +import java.util.List; /** * 培训审批对象 rs_training_approval @@ -69,70 +70,81 @@ public class RsTrainingApproval extends BaseEntity { * 租户ID */ private String tenantId; - + /** 非数据库字段 - 审批记录列表显示 **/ @TableField(exist = false) private String applicationUserName; - + @TableField(exist = false) private String courseTitle; - + /** 非数据库字段 - 审批记录详情显示 **/ @TableField(exist = false) private String userName; - + @TableField(exist = false) private String department; - + @TableField(exist = false) private String currentTitle; - + @TableField(exist = false) private String currentPosition; - + @TableField(exist = false) @JsonFormat(pattern = "yyyy-MM-dd") private Date workStartTime; - + @TableField(exist = false) private String phone; - + @TableField(exist = false) private String organizerUnit; - + @TableField(exist = false) private String courseName; - + @TableField(exist = false) private String location; - + @TableField(exist = false) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date startTime; - + @TableField(exist = false) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date endTime; - + @TableField(exist = false) private String trainingType; - + @TableField(exist = false) private Double trainingFee; - + @TableField(exist = false) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date applyTime; - + @TableField(exist = false) private String companions; - + @TableField(exist = false) private String trainingPurpose; - + @TableField(exist = false) private String reason; - + @TableField(exist = false) private String attachmentIds; -} \ No newline at end of file + + /** 非数据库字段 - 权限过滤辅助字段 **/ + @TableField(exist = false) + private Long applicantId; // 申请人ID(用于权限过滤) + + @TableField(exist = false) + private Long deptId; // 部门ID(用于部门权限过滤) + + /** 🆕 新增:审批级别列表(用于多级审批过滤) **/ + @TableField(exist = false) + private List approvalLevels; // 审批级别列表,例如:["1", "4"] +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java index 9c03a329..1ea417b0 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsTrainingApplicationService.java @@ -17,10 +17,11 @@ public interface IRsTrainingApplicationService { * 查询培训申请列表 * * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ - TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, PageQuery pageQuery); + TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, + PageQuery pageQuery); /** * 查询培训申请列表 @@ -33,22 +34,24 @@ public interface IRsTrainingApplicationService { /** * 查询指定课程的培训申请列表 * - * @param courseId 课程ID + * @param courseId 课程ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ - TableDataInfo selectPageApplicationsByCourse(Long courseId, RsTrainingApplication application, PageQuery pageQuery); - + TableDataInfo selectPageApplicationsByCourse(Long courseId, + RsTrainingApplication application, PageQuery pageQuery); + /** * 查询我的培训申请列表 * - * @param userId 用户ID + * @param userId 用户ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ - TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, PageQuery pageQuery); + TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, + PageQuery pageQuery); /** * 查询培训申请详细信息 @@ -89,7 +92,7 @@ public interface IRsTrainingApplicationService { * @return 结果 */ int deleteRsTrainingApplicationById(Long applicationId); - + /** * 取消培训申请 * @@ -97,22 +100,43 @@ public interface IRsTrainingApplicationService { * @return 结果 */ int cancelApplication(Long applicationId); - + /** * 更新申请状态 * * @param applicationId 培训申请ID - * @param status 状态 + * @param status 状态 * @return 结果 */ int updateApplicationStatus(Long applicationId, String status); - + /** * 重新提交已被拒绝的申请 * * @param applicationId 培训申请ID - * @param application 培训申请更新数据 + * @param application 培训申请更新数据 * @return 结果 */ int resubmitApplication(Long applicationId, RsTrainingApplication application); -} \ No newline at end of file + + /** + * 审批培训申请(7级审批) + * + * @param applicationId 申请ID + * @param approverId 审批人ID + * @param approverName 审批人姓名 + * @param approvalStatus 审批状态(1-通过,2-拒绝) + * @param approvalOpinion 审批意见 + * @param approvalSignature 审批签字(base64格式) + * @return 结果 + */ + Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature); + + /** + * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) + * + * @return 待审批申请列表 + */ + List queryPendingByCurrentUser(); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java index 5b5c1d39..821c6581 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApplicationServiceImpl.java @@ -18,14 +18,21 @@ import org.dromara.rs.domain.RsTrainingRecord; import java.util.Arrays; +import org.dromara.rs.domain.RsTrainingApproval; import org.dromara.rs.mapper.RsTrainingApplicationMapper; +import org.dromara.rs.mapper.RsTrainingApprovalMapper; import org.dromara.rs.service.IRsTrainingApplicationService; import org.dromara.rs.service.IRsTrainingRecordService; +import org.dromara.system.domain.vo.SysUserVo; +import org.dromara.system.service.ISysUserService; +import org.dromara.common.core.exception.ServiceException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; @@ -39,23 +46,30 @@ import java.util.List; @Service @RequiredArgsConstructor public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationService { - + private static final Logger log = LoggerFactory.getLogger(RsTrainingApplicationServiceImpl.class); - + private final RsTrainingApplicationMapper trainingApplicationMapper; - + @Autowired private IRsTrainingRecordService trainingRecordService; + @Autowired + private RsTrainingApprovalMapper approvalMapper; + + @Autowired + private ISysUserService userService; + /** * 查询培训申请列表 * * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ @Override - public TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, PageQuery pageQuery) { + public TableDataInfo selectPageRsTrainingApplicationList(RsTrainingApplication application, + PageQuery pageQuery) { // 创建分页对象 Page page = pageQuery.build(); // 获取分页参数 @@ -68,7 +82,7 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application.getTenantId() == null || application.getTenantId().isEmpty()) { application.setTenantId(LoginHelper.getTenantId()); } - + // ===== 业务逻辑层权限过滤 ===== // 根据用户角色进行数据权限过滤 if (!LoginHelper.isSuperAdmin()) { @@ -76,7 +90,7 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS Long currentUserId = LoginHelper.getUserId(); Long currentDeptId = LoginHelper.getDeptId(); String username = LoginHelper.getUsername(); - + // 检查用户角色并应用相应的数据过滤 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能看到自己的申请 @@ -92,24 +106,25 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS application.setCreateBy(currentUserId); } } - + // 查询总数 long total = trainingApplicationMapper.selectCount(buildQueryWrapper(application)); - + // 使用自定义分页查询获取列表数据 - List list = trainingApplicationMapper.selectRsTrainingApplicationListPage(application, offset, size); - + List list = trainingApplicationMapper.selectRsTrainingApplicationListPage(application, + offset, size); + // 处理自定义培训课程的情况 for (RsTrainingApplication app : list) { if (app.getCourseName() == null || app.getCourseName().isEmpty()) { app.setCourseName("自定义培训"); } } - + // 构建分页对象 page.setRecords(list); page.setTotal(total); - + return TableDataInfo.build(page); } @@ -121,69 +136,133 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS */ @Override public List selectRsTrainingApplicationList(RsTrainingApplication application) { - // ===== 业务逻辑层权限过滤 ===== - // 根据用户角色进行数据权限过滤 + log.info("🔍 ==================== 开始培训申请列表查询 ===================="); + log.info("🔍 当前登录用户ID: {}", LoginHelper.getUserId()); + log.info("🔍 是否超级管理员: {}", LoginHelper.isSuperAdmin()); + log.info("🔍 用户角色: rs-Applicant={}, rs-admin={}", + StpUtil.hasRole("rs-Applicant"), + StpUtil.hasRole("rs-admin")); + + // ===== 审批权限过滤 ===== if (!LoginHelper.isSuperAdmin()) { - // 获取当前登录用户信息 Long currentUserId = LoginHelper.getUserId(); Long currentDeptId = LoginHelper.getDeptId(); - - // 检查用户角色并应用相应的数据过滤 + log.info("🔍 当前用户部门ID: {}", currentDeptId); + + // 判断用户角色 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能看到自己的申请 application.setUserId(currentUserId); } else if (StpUtil.hasRole("rs-admin")) { - // 人事管理员可以看到所有申请(包括审批和部门管理功能) - // 不设置额外过滤条件,让管理员能看到所有申请 + // 人事管理员可以看到所有申请 + // 不设置额外过滤条件 } else { - // 默认情况:只能看到自己创建的申请 - application.setCreateBy(currentUserId); + // 审批人权限过滤:根据审批级别权限过滤数据 + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + if (approvalLevels.isEmpty()) { + // 没有审批权限,只能看到自己创建的申请 + application.setCreateBy(currentUserId); + } else { + // 🔴 有审批权限,需要在数据库层面过滤 + // 这里需要在 Mapper XML 中添加审批权限的过滤逻辑 + // 暂时先在代码层面过滤 + } } } - - // 使用自定义的Mapper方法查询,已经关联了课程表获取课程名称 + + // 使用自定义的Mapper方法查询 List list = trainingApplicationMapper.selectRsTrainingApplicationList(application); - + + // 🔴 在代码层面进行审批权限过滤(临时方案) + log.info("🔍 准备进行审批权限过滤,查询到的数据总数: {}", list.size()); + + if (!LoginHelper.isSuperAdmin() && + !StpUtil.hasRole("rs-Applicant") && + !StpUtil.hasRole("rs-admin")) { + + log.info("🔍 触发审批权限过滤逻辑(非超管、非申请人、非人事管理员)"); + + Long currentUserId = LoginHelper.getUserId(); + Long currentDeptId = LoginHelper.getDeptId(); + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + log.info("🔍 当前用户可审批级别: {}", approvalLevels); + + if (!approvalLevels.isEmpty()) { + log.info("🔍 开始按审批级别过滤数据..."); + // 按审批权限过滤 + list = list.stream().filter(app -> { + String appLevel = app.getApprovalLevel(); + if (appLevel == null) + return false; + + // 检查是否有该级别的审批权限 + if (!approvalLevels.contains(appLevel)) { + return false; + } + + // 第1-3级需要检查部门权限 + if (appLevel.equals("1") || appLevel.equals("2a") || appLevel.equals("2b") + || appLevel.equals("3")) { + return app.getCreateDept() != null && app.getCreateDept().equals(currentDeptId); + } + + // 第4-7级可以跨部门审批 + return true; + }).collect(java.util.stream.Collectors.toList()); + + log.info("🔍 审批权限过滤完成,过滤后数据数量: {}", list.size()); + } else { + log.info("🔍 用户没有审批权限,不进行过滤"); + } + } else { + log.info("🔍 跳过审批权限过滤(超管/申请人/人事管理员)"); + } + // 处理自定义培训课程(courseId=0)的情况 for (RsTrainingApplication app : list) { - if (app.getCourseId() != null && app.getCourseId() == 0 && (app.getCourseName() == null || app.getCourseName().isEmpty())) { + if (app.getCourseId() != null && app.getCourseId() == 0 + && (app.getCourseName() == null || app.getCourseName().isEmpty())) { app.setCourseName("自定义培训"); } } - + return list; } /** * 查询指定课程的培训申请列表 * - * @param courseId 课程ID + * @param courseId 课程ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ @Override - public TableDataInfo selectPageApplicationsByCourse(Long courseId, RsTrainingApplication application, PageQuery pageQuery) { + public TableDataInfo selectPageApplicationsByCourse(Long courseId, + RsTrainingApplication application, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(application); lqw.eq(RsTrainingApplication::getCourseId, courseId); return TableDataInfo.build(trainingApplicationMapper.selectPage(pageQuery.build(), lqw)); } - + /** * 查询我的培训申请列表 * - * @param userId 用户ID + * @param userId 用户ID * @param application 培训申请 - * @param pageQuery 分页参数 + * @param pageQuery 分页参数 * @return 培训申请分页列表 */ @Override - public TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, PageQuery pageQuery) { + public TableDataInfo selectPageMyApplications(Long userId, RsTrainingApplication application, + PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(application); lqw.eq(RsTrainingApplication::getUserId, userId); return TableDataInfo.build(trainingApplicationMapper.selectPage(pageQuery.build(), lqw)); } - + /** * 构建查询条件 */ @@ -200,9 +279,11 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS lqw.like(application.getLocation() != null, RsTrainingApplication::getLocation, application.getLocation()); lqw.eq(application.getDuration() != null, RsTrainingApplication::getDuration, application.getDuration()); // 课程名称搜索:搜索自定义课程名称字段 - lqw.like(application.getCourseName() != null, RsTrainingApplication::getCourseName, application.getCourseName()); + lqw.like(application.getCourseName() != null, RsTrainingApplication::getCourseName, + application.getCourseName()); // 排除指定状态的条件(用于待审批接口) - lqw.ne(application.getExcludeStatus() != null, RsTrainingApplication::getStatus, application.getExcludeStatus()); + lqw.ne(application.getExcludeStatus() != null, RsTrainingApplication::getStatus, + application.getExcludeStatus()); return lqw; } @@ -216,15 +297,15 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS public RsTrainingApplication selectRsTrainingApplicationById(Long applicationId) { // 使用自定义的Mapper方法查询,已经关联了课程表获取课程名称 RsTrainingApplication application = trainingApplicationMapper.selectRsTrainingApplicationById(applicationId); - + // ===== 业务逻辑层权限过滤 ===== // 检查当前用户是否有权限查看此申请 if (application != null && !LoginHelper.isSuperAdmin()) { Long currentUserId = LoginHelper.getUserId(); Long currentDeptId = LoginHelper.getDeptId(); - + boolean hasPermission = false; - + // 检查用户角色权限 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能查看自己的申请 @@ -234,23 +315,23 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS hasPermission = true; } else { // 默认情况:只能查看自己创建的申请 - hasPermission = application.getCreateBy() != null && - application.getCreateBy().equals(currentUserId); + hasPermission = application.getCreateBy() != null && + application.getCreateBy().equals(currentUserId); } - + // 如果没有权限,返回null if (!hasPermission) { log.warn("用户 {} 尝试访问无权限的培训申请: {}", currentUserId, applicationId); return null; } } - + // 处理自定义培训课程(courseId=0)的情况 - if (application != null && application.getCourseId() != null && application.getCourseId() == 0 - && (application.getCourseName() == null || application.getCourseName().isEmpty())) { + if (application != null && application.getCourseId() != null && application.getCourseId() == 0 + && (application.getCourseName() == null || application.getCourseName().isEmpty())) { application.setCourseName("自定义培训"); } - + return application; } @@ -267,7 +348,7 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application.getApplicationId() == null) { application.setApplicationId(System.currentTimeMillis()); } - + // 处理自定义培训课程的情况(courseId = 0 或 null) // 确保courseName不为空 if (application.getCourseName() == null || application.getCourseName().isEmpty()) { @@ -277,9 +358,10 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS application.setCreateTime(now); application.setApplyTime(now); application.setStatus("0"); // 待审核状态 + application.setApprovalLevel("1"); // 🔴 初始化为第1级审批(部门负责人) application.setTenantId(LoginHelper.getTenantId()); // 设置当前用户租户ID return trainingApplicationMapper.insert(application); - + // 以上代码已处理所有情况 } catch (Exception e) { e.printStackTrace(); @@ -308,13 +390,14 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS @Override public int deleteRsTrainingApplicationByIds(Long[] applicationIds) { // 查询所有要删除的申请 - List applications = trainingApplicationMapper.selectBatchIds(Arrays.asList(applicationIds)); - + List applications = trainingApplicationMapper + .selectBatchIds(Arrays.asList(applicationIds)); + // 对每个申请,减少对应课程的当前报名人数 for (RsTrainingApplication app : applications) { - // 培训课程功能已移除,无需更新课程报名人数 + // 培训课程功能已移除,无需更新课程报名人数 } - + return trainingApplicationMapper.deleteBatchIds(Arrays.asList(applicationIds)); } @@ -331,10 +414,10 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application != null) { // 培训课程功能已移除,无需更新课程报名人数 } - + return trainingApplicationMapper.deleteById(applicationId); } - + /** * 取消培训申请 * @@ -348,40 +431,40 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (application != null) { // 只有待审核状态的申请才能取消 if ("0".equals(application.getStatus())) { - // 更新状态为已取消 - application.setStatus("3"); - application.setUpdateTime(DateUtils.getNowDate()); - - // 培训课程功能已移除,无需更新课程报名人数 - + // 更新状态为已取消 + application.setStatus("3"); + application.setUpdateTime(DateUtils.getNowDate()); + + // 培训课程功能已移除,无需更新课程报名人数 + return trainingApplicationMapper.updateById(application); } } return 0; } - + /** * 更新申请状态 * * @param applicationId 培训申请ID - * @param status 状态 + * @param status 状态 * @return 结果 */ @Override public int updateApplicationStatus(Long applicationId, String status) { RsTrainingApplication application = trainingApplicationMapper.selectById(applicationId); - if (application != null) { - application.setStatus(status); - Date now = DateUtils.getNowDate(); - application.setUpdateTime(now); - - // 如果是拒绝,记录被拒绝时间 - if ("2".equals(status)) { - application.setRejectTime(now); - } - + if (application != null) { + application.setStatus(status); + Date now = DateUtils.getNowDate(); + application.setUpdateTime(now); + + // 如果是拒绝,记录被拒绝时间 + if ("2".equals(status)) { + application.setRejectTime(now); + } + int result = trainingApplicationMapper.updateById(application); - + // 如果申请通过,自动创建培训记录 if ("1".equals(status)) { try { @@ -391,16 +474,16 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS // 不影响申请状态更新的结果 } } - + return result; } return 0; } - + /** * 重新提交已被拒绝的申请 * - * @param applicationId 培训申请ID + * @param applicationId 培训申请ID * @param applicationData 培训申请更新数据 * @return 结果 */ @@ -411,14 +494,12 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (existingApp == null) { return 0; } - + // 只有状态为已拒绝的申请才能重新提交 if (!"2".equals(existingApp.getStatus())) { return 0; } - - - + // 更新申请数据 if (applicationData.getReason() != null) { existingApp.setReason(applicationData.getReason()); @@ -438,18 +519,17 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS if (applicationData.getCourseName() != null) { existingApp.setCourseName(applicationData.getCourseName()); } - - // 重置为待审核状态 - existingApp.setStatus("0"); - Date now = DateUtils.getNowDate(); - existingApp.setUpdateTime(now); - existingApp.setApplyTime(now); // 更新申请时间 - - - + + // 重置为待审核状态 + existingApp.setStatus("0"); + existingApp.setApprovalLevel("1"); // 🔴 重新提交时重置为第1级审批 + Date now = DateUtils.getNowDate(); + existingApp.setUpdateTime(now); + existingApp.setApplyTime(now); // 更新申请时间 + return trainingApplicationMapper.updateById(existingApp); } - + /** * 根据培训申请创建培训记录 * @@ -458,13 +538,14 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS private void createTrainingRecordFromApplication(RsTrainingApplication application) { try { // 检查是否已存在培训记录 - RsTrainingRecord existingRecord = trainingRecordService.selectByApplicationId(application.getApplicationId()); + RsTrainingRecord existingRecord = trainingRecordService + .selectByApplicationId(application.getApplicationId()); if (existingRecord != null) { - log.info("培训记录已存在,不重复创建: applicationId={}, recordId={}", - application.getApplicationId(), existingRecord.getRecordId()); + log.info("培训记录已存在,不重复创建: applicationId={}, recordId={}", + application.getApplicationId(), existingRecord.getRecordId()); return; } - + // 创建新的培训记录 RsTrainingRecord record = new RsTrainingRecord(); record.setApplicationId(application.getApplicationId()); @@ -483,30 +564,355 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS record.setStartTime(application.getStartTime()); record.setEndTime(application.getEndTime()); record.setCompletionStatus("0"); // 0=进行中 - + // 设置基础字段 - 修复字段缺失问题 Date now = DateUtils.getNowDate(); record.setCreateTime(now); record.setUpdateTime(now); - + // 设置租户和用户信息 - record.setTenantId(application.getTenantId() != null ? application.getTenantId() : LoginHelper.getTenantId()); + record.setTenantId( + application.getTenantId() != null ? application.getTenantId() : LoginHelper.getTenantId()); record.setCreateBy(application.getUserId()); // 创建者设为申请人 record.setUpdateBy(application.getUserId()); // 更新者设为申请人 record.setCreateDept(application.getCreateDept() != null ? application.getCreateDept() : 1L); // 使用createDept,如果没有则设为默认值 record.setDeptId(application.getCreateDept() != null ? application.getCreateDept() : 1L); // 设置部门ID - + int result = trainingRecordService.insertRsTrainingRecord(record); if (result > 0) { - log.info("培训记录创建成功: applicationId={}, recordId={}", - application.getApplicationId(), record.getRecordId()); + log.info("培训记录创建成功: applicationId={}, recordId={}", + application.getApplicationId(), record.getRecordId()); } else { log.error("培训记录创建失败: applicationId={}", application.getApplicationId()); } } catch (Exception e) { - log.error("创建培训记录时发生异常: applicationId={}, error={}", - application.getApplicationId(), e.getMessage(), e); + log.error("创建培训记录时发生异常: applicationId={}, error={}", + application.getApplicationId(), e.getMessage(), e); throw e; } } -} \ No newline at end of file + + // ======================================== + // 7级审批核心逻辑(支持培训类型分流) + // ======================================== + + /** + * 审批培训申请(7级审批制度) + * + * @param applicationId 申请ID + * @param approverId 审批人ID + * @param approverName 审批人姓名 + * @param approvalStatus 审批状态(1-通过,2-拒绝) + * @param approvalOpinion 审批意见 + * @param approvalSignature 审批签字(base64格式) + * @return 结果 + */ + @Override + @Transactional + public Boolean approveApplication(Long applicationId, Long approverId, String approverName, + String approvalStatus, String approvalOpinion, String approvalSignature) { + RsTrainingApplication application = trainingApplicationMapper.selectById(applicationId); + if (application == null) { + throw new RuntimeException("申请不存在"); + } + if (!"0".equals(application.getStatus())) { + throw new RuntimeException("只有待审核状态的申请才能审批"); + } + + // 🔴 验证审批人权限级别 + String currentApprovalLevel = application.getApprovalLevel() != null ? application.getApprovalLevel() : "1"; + if (!validateApproverLevel(approverId, currentApprovalLevel)) { + throw new ServiceException(String.format( + "您没有第%s级审批权限,当前申请需要第%s级审批人审批", + currentApprovalLevel, currentApprovalLevel)); + } + log.info("✅ 审批人 {} 拥有第{}级审批权限验证通过", approverName, currentApprovalLevel); + + // 创建审批记录 + RsTrainingApproval approval = new RsTrainingApproval(); + approval.setApplicationId(applicationId); + approval.setApproverId(approverId); + approval.setApproverName(approverName); + approval.setApprovalLevel(Integer.parseInt(currentApprovalLevel.replaceAll("[^0-9]", ""))); // 转换为数字 + approval.setStatus(approvalStatus); + approval.setOpinion(approvalOpinion); + approval.setApprovalTime(new Date()); + + // 日志记录签字数据 + if (approvalSignature != null && !approvalSignature.trim().isEmpty()) { + log.info("✅ 保存审批签字数据,长度: {} 字符", approvalSignature.length()); + } else { + log.info("ℹ️ 未提供审批签字"); + } + + // 手动设置多租户和删除标志字段 + if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) { + approval.setTenantId(LoginHelper.getTenantId()); + } + + approvalMapper.insert(approval); + + // 更新申请状态(7级审批流转逻辑) + if ("1".equals(approvalStatus)) { + // 审批通过 + String nextLevel = getNextApprovalLevel(currentApprovalLevel, application.getTrainingType()); + if (nextLevel != null) { + // 进入下一级审批 + application.setApprovalLevel(nextLevel); + application.setCurrentApproverId(null); // 不再设置固定审批人ID + application.setCurrentApprover(getApprovalLevelDescription(nextLevel)); + + log.info("✅ 审批流转到第{}级,申请ID: {}, 审批人根据权限动态查询", + nextLevel, applicationId); + } else { + // 所有级别审批完成 + try { + // 创建培训记录 + log.info("开始最终审批处理,申请ID: {}, 当前状态: {}, 申请人: {}", + applicationId, application.getStatus(), application.getUserName()); + + createTrainingRecordFromApplication(application); + log.info("审批通过,成功创建培训记录,申请ID: {}", applicationId); + + // 更新申请状态为通过 + application.setStatus("1"); + application.setApprovalTime(new Date()); + } catch (Exception e) { + log.error("审批通过后创建培训记录失败,申请ID: {}, 错误: {}", applicationId, e.getMessage(), e); + throw new RuntimeException("审批处理失败:创建培训记录时发生错误 - " + e.getMessage()); + } + } + } else if ("2".equals(approvalStatus)) { + // 审批拒绝 + application.setStatus("2"); + application.setApprovalTime(new Date()); + } + + // 更新申请状态到数据库 + boolean updateResult = trainingApplicationMapper.updateById(application) > 0; + + return updateResult; + } + + /** + * 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) + * + * @return 待审批申请列表 + */ + @Override + public List queryPendingByCurrentUser() { + Long currentUserId = LoginHelper.getUserId(); + String currentUserName = LoginHelper.getUsername(); + + log.info("🔍 查询用户待审批申请 - 用户ID: {}, 用户名: {}", currentUserId, currentUserName); + + // 1. 获取当前用户的审批级别列表(基于权限动态查询) + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + if (approvalLevels == null || approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限,返回空列表", currentUserName); + return new ArrayList<>(); + } + + log.info("✅ 用户 {} 拥有审批级别: {}", currentUserName, approvalLevels); + + // 2. 对每个级别单独查询并合并 + List result = new ArrayList<>(); + + for (String level : approvalLevels) { + Long deptIdForFilter = null; + + // 🔴 关键:只对 level=1 进行部门过滤 + if ("1".equals(level)) { + SysUserVo currentUser = userService.selectUserById(currentUserId); + if (currentUser != null && currentUser.getDeptId() != null) { + deptIdForFilter = currentUser.getDeptId(); + log.info("🔒 [部门权限过滤] 级别1,用户: {}, 部门ID: {}, 只查看本部门待审批", + currentUserName, deptIdForFilter); + } else { + log.warn("⚠️ 用户 {} 是部门负责人但没有部门信息,无法过滤", currentUserName); + } + } else { + log.info("✅ [全校权限] 级别{}, 用户 {}, 查看全校待审批", level, currentUserName); + } + + // 3. 构建查询条件 + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(RsTrainingApplication::getStatus, "0"); // 待审核状态 + lqw.eq(RsTrainingApplication::getApprovalLevel, level); + if (deptIdForFilter != null) { + lqw.eq(RsTrainingApplication::getCreateDept, deptIdForFilter); + } + lqw.eq(RsTrainingApplication::getTenantId, LoginHelper.getTenantId()); + + // 4. 查询该级别的待审批申请 + List levelApplications = trainingApplicationMapper.selectList(lqw); + + log.info("📋 级别{} 查询到 {} 条待审批申请", level, levelApplications.size()); + + result.addAll(levelApplications); + } + + log.info("📋 用户 {} 总共查询到 {} 条待审批申请", currentUserName, result.size()); + + return result; + } + + /** + * 获取下一级审批级别(根据培训类型动态路由) + * + * 7级审批制度: + * 1-部门负责人 → 2a-教务处/2b-学生处/3-计划财务处(根据培训类型) + * 2a-教务处 → 3-计划财务处 + * 2b-学生处 → 3-计划财务处 + * 3-计划财务处 → 4-人事处 + * 4-人事处 → 5-业务分管领导 + * 5-业务分管领导 → 6-人事分管院领导 + * 6-人事分管院领导 → 7-院长 + * 7-院长 → 审批完成 + * + * @param currentLevel 当前审批级别 + * @param trainingType 培训类型 + * @return 下一级审批级别 + */ + private String getNextApprovalLevel(String currentLevel, String trainingType) { + switch (currentLevel) { + case "1": + // 部门负责人审批后,根据培训类型分流 + if ("教学".equals(trainingType)) { + return "2a"; // 教务处 + } else if ("德育".equals(trainingType)) { + return "2b"; // 学生处 + } else { + // 行政、管理、其他 → 直接到计划财务处 + return "3"; + } + case "2a": // 教务处 → 计划财务处 + case "2b": // 学生处 → 计划财务处 + return "3"; + case "3": + return "4"; // 计划财务处 → 人事处 + case "4": + return "5"; // 人事处 → 业务分管领导 + case "5": + return "6"; // 业务分管领导 → 人事分管院领导 + case "6": + return "7"; // 人事分管院领导 → 院长 + case "7": + return null; // 院长审批完成 + default: + return null; + } + } + + /** + * 根据审批级别获取审批环节描述 + * + * @param approvalLevel 审批级别 + * @return 审批环节描述 + */ + private String getApprovalLevelDescription(String approvalLevel) { + switch (approvalLevel) { + case "1": + return "待部门负责人审批"; + case "2a": + return "待教务处审批"; + case "2b": + return "待学生处审批"; + case "3": + return "待计划财务处审批"; + case "4": + return "待人事处审批"; + case "5": + return "待业务分管领导审批"; + case "6": + return "待人事分管院领导审批"; + case "7": + return "待院长审批"; + default: + return "待审批"; + } + } + + /** + * 根据当前登录用户的审批权限,获取其可审批的级别列表 + * + * 权限到级别的映射: + * - rs:training:approve1 → 审批级别 "1" (部门负责人) + * - rs:training:approve2a → 审批级别 "2a" (教务处) + * - rs:training:approve2b → 审批级别 "2b" (学生处) + * - rs:training:approve3 → 审批级别 "3" (计划财务处) + * - rs:training:approve4 → 审批级别 "4" (人事处) + * - rs:training:approve5 → 审批级别 "5" (业务分管领导) + * - rs:training:approve6 → 审批级别 "6" (人事分管院领导) + * - rs:training:approve7 → 审批级别 "7" (院长) + * + * @param userId 用户ID + * @return 可审批的级别列表 + */ + private List getApprovalLevelsByUser(Long userId) { + try { + List approvalLevels = new ArrayList<>(); + + // 检查7个审批级别的权限(1, 2a, 2b, 3, 4, 5, 6, 7) + String[] levels = { "1", "2a", "2b", "3", "4", "5", "6", "7" }; + for (String level : levels) { + String permission = "rs:training:approve" + level; + if (StpUtil.hasPermission(permission)) { + approvalLevels.add(level); + log.info("✅ 用户 {} 拥有第{}级审批权限: {}", userId, level, permission); + } + } + + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:training:approve")) { + log.info("✅ 用户 {} 拥有通用审批权限,可审批所有级别", userId); + approvalLevels.clear(); + for (String level : levels) { + approvalLevels.add(level); + } + } + + if (approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限", userId); + } else { + log.info("📋 用户 {} 可审批的级别: {}", userId, approvalLevels); + } + + return approvalLevels; + + } catch (Exception e) { + log.error("获取用户审批级别失败,用户ID: {}, 错误: {}", userId, e.getMessage(), e); + return new ArrayList<>(); + } + } + + /** + * 验证审批人权限级别 + * + * @param approverId 审批人ID + * @param approvalLevel 审批级别 + * @return 是否有权限 + */ + private boolean validateApproverLevel(Long approverId, String approvalLevel) { + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:training:approve")) { + log.info("用户 {} 拥有通用审批权限 rs:training:approve", approverId); + return true; + } + + // 检查分级审批权限 + String requiredPermission = "rs:training:approve" + approvalLevel; + boolean hasPermission = StpUtil.hasPermission(requiredPermission); + + if (hasPermission) { + log.info("用户 {} 拥有第{}级审批权限 {}", + approverId, approvalLevel, requiredPermission); + } else { + log.warn("用户 {} 缺少第{}级审批权限 {},权限验证失败", + approverId, approvalLevel, requiredPermission); + } + + return hasPermission; + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java index f80bcb6c..0bc83948 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsTrainingApprovalServiceImpl.java @@ -1,5 +1,6 @@ package org.dromara.rs.service.impl; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -18,6 +19,8 @@ import org.dromara.rs.domain.RsTrainingApproval; import org.dromara.rs.domain.RsTrainingApplication; import org.dromara.rs.service.IRsTrainingApprovalService; import org.dromara.rs.service.IRsTrainingApplicationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 培训审批服务实现类 @@ -27,26 +30,30 @@ import org.dromara.rs.service.IRsTrainingApplicationService; */ @Service public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService { - + + private static final Logger log = LoggerFactory.getLogger(RsTrainingApprovalServiceImpl.class); + @Autowired private RsTrainingApprovalMapper trainingApprovalMapper; - + @Autowired private RsTrainingApplicationMapper trainingApplicationMapper; - + @Autowired private IRsTrainingApplicationService trainingApplicationService; /** * 查询培训审批列表 * - * @param approval 培训审批 + * @param approval 培训审批 * @param pageQuery 分页参数 * @return 培训审批分页列表 */ @Override - public TableDataInfo selectPageRsTrainingApprovalList(RsTrainingApproval approval, PageQuery pageQuery) { - com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper.selectPage(pageQuery.build(), buildQueryWrapper(approval)); + public TableDataInfo selectPageRsTrainingApprovalList(RsTrainingApproval approval, + PageQuery pageQuery) { + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper + .selectPage(pageQuery.build(), buildQueryWrapper(approval)); return TableDataInfo.build(page); } @@ -60,37 +67,39 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService public List selectRsTrainingApprovalList(RsTrainingApproval approval) { return trainingApprovalMapper.selectList(buildQueryWrapper(approval)); } - + /** * 构建查询条件 */ private LambdaQueryWrapper buildQueryWrapper(RsTrainingApproval approval) { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); - + // ===== 业务逻辑层权限过滤 ===== // 根据用户角色进行数据权限过滤 if (!LoginHelper.isSuperAdmin()) { // 获取当前登录用户信息 Long currentUserId = LoginHelper.getUserId(); - + // 检查用户角色并应用相应的数据过滤 if (StpUtil.hasRole("rs-Applicant")) { // 申请人只能看到与自己相关的审批记录 // 这里需要通过申请表关联查询 - lqw.exists("SELECT 1 FROM rs_training_application app WHERE app.application_id = rs_training_approval.application_id AND app.user_id = " + currentUserId); + lqw.exists( + "SELECT 1 FROM rs_training_application app WHERE app.application_id = rs_training_approval.application_id AND app.user_id = " + + currentUserId); } else if (StpUtil.hasRole("rs-admin") || StpUtil.hasRole("rs-Approver")) { // 人事管理员和审批人可以看到所有审批记录 // 不设置额外过滤条件,让管理员和审批人能看到所有审批 } else { // 默认情况:只能看到自己创建或处理的审批 lqw.and(wrapper -> wrapper.eq(RsTrainingApproval::getCreateBy, currentUserId) - .or().eq(RsTrainingApproval::getApproverId, currentUserId)); + .or().eq(RsTrainingApproval::getApproverId, currentUserId)); } } - + // 添加租户过滤 lqw.eq(RsTrainingApproval::getTenantId, LoginHelper.getTenantId()); - + lqw.eq(approval.getApplicationId() != null, RsTrainingApproval::getApplicationId, approval.getApplicationId()); lqw.eq(approval.getApproverId() != null, RsTrainingApproval::getApproverId, approval.getApproverId()); lqw.eq(approval.getApprovalLevel() != null, RsTrainingApproval::getApprovalLevel, approval.getApprovalLevel()); @@ -119,14 +128,15 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService * 查询待我审批的列表 * * @param approverId 审批人ID - * @param approval 培训审批 - * @param pageQuery 分页参数 + * @param approval 培训审批 + * @param pageQuery 分页参数 * @return 培训审批分页列表 */ @Override - public TableDataInfo selectPagePendingApprovals(Long approverId, RsTrainingApproval approval, PageQuery pageQuery) { + public TableDataInfo selectPagePendingApprovals(Long approverId, RsTrainingApproval approval, + PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(approval); - + // 简化权限模型:审批人和管理员可以看到所有待审核记录 if (StpUtil.hasRole("rs-admin") || StpUtil.hasRole("rs-Approver")) { // 管理员和审批人可以看到所有待审核记录,不限制approverId @@ -134,9 +144,10 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService // 其他角色只能看到指派给自己的待审核记录 lqw.eq(RsTrainingApproval::getApproverId, approverId); } - + lqw.eq(RsTrainingApproval::getStatus, "0"); // 待审核状态 - com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper.selectPage(pageQuery.build(), lqw); + com.baomidou.mybatisplus.extension.plugins.pagination.Page page = trainingApprovalMapper + .selectPage(pageQuery.build(), lqw); return TableDataInfo.build(page); } @@ -178,12 +189,27 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService /** * 查询带详细信息的审批记录列表(关联申请人和课程信息) * - * @param approval 培训审批 + * @param approval 培训审批 * @param pageQuery 分页参数 * @return 培训审批分页列表 */ @Override - public TableDataInfo selectPageRsTrainingApprovalWithDetailsList(RsTrainingApproval approval, PageQuery pageQuery) { + public TableDataInfo selectPageRsTrainingApprovalWithDetailsList(RsTrainingApproval approval, + PageQuery pageQuery) { + log.info("==================== 查询培训审批列表 ===================="); + log.info("原始查询参数: applicationId={}, approverId={}", approval.getApplicationId(), approval.getApproverId()); + + // 🔧 应用数据权限过滤 + try { + applyDataPermissionFilter(approval); + log.info("权限过滤后查询参数: applicationId={}, approverId={}, applicantId={}, deptId={}", + approval.getApplicationId(), approval.getApproverId(), approval.getApplicantId(), + approval.getDeptId()); + } catch (Exception e) { + log.error("❌ 权限过滤异常:", e); + throw e; + } + // 创建分页对象 com.baomidou.mybatisplus.extension.plugins.pagination.Page page = pageQuery.build(); // 获取分页参数 @@ -196,13 +222,15 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) { approval.setTenantId(LoginHelper.getTenantId()); } - + // 查询总数 - 使用与列表查询相同的条件 long total = trainingApprovalMapper.selectRsTrainingApprovalWithDetailsCount(approval); - + // 使用自定义分页查询获取列表数据 List list = trainingApprovalMapper.selectRsTrainingApprovalWithDetailsList(approval); - + + log.info("查询结果数量: total={}, list.size={}", total, list.size()); + // 手动分页处理 int start = (int) offset; int end = Math.min(start + (int) size, list.size()); @@ -211,11 +239,14 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService } else { list = list.subList(start, end); } - + // 构建分页对象 page.setRecords(list); page.setTotal(total); - + + log.info("分页结果: 总数={}, 当前页={}, 每页={}, 返回={}", total, current, size, list.size()); + log.info("==================== 查询完成 ===================="); + return TableDataInfo.build(page); } @@ -240,13 +271,13 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService public int deleteRsTrainingApprovalById(Long approvalId) { return trainingApprovalMapper.deleteById(approvalId); } - + /** * 处理审批 * * @param applicationId 申请ID - * @param status 审批状态 - * @param opinion 审批意见 + * @param status 审批状态 + * @param opinion 审批意见 * @return 结果 */ @Override @@ -257,11 +288,11 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService if (application == null) { return 0; } - + // 获取当前用户信息 Long userId = LoginHelper.getUserId(); String userName = LoginHelper.getUsername(); - + // 创建审批记录 - MyBatis-Plus自动生成雪花ID RsTrainingApproval approval = new RsTrainingApproval(); // 不需要手动设置ID,MyBatis-Plus会自动生成雪花ID @@ -273,14 +304,190 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService approval.setOpinion(opinion); // 审批意见 approval.setApprovalTime(DateUtils.getNowDate()); approval.setCreateTime(DateUtils.getNowDate()); - + // 插入审批记录 - 无论通过还是拒绝,都创建独立审批记录 int result = trainingApprovalMapper.insert(approval); System.out.println("创建审批记录 - applicationId: " + applicationId + ", status: " + status + ", result: " + result); - + // 更新申请状态 trainingApplicationService.updateApplicationStatus(applicationId, status); - + return result; } -} \ No newline at end of file + + /** + * 应用数据权限过滤 + */ + private void applyDataPermissionFilter(RsTrainingApproval approval) { + Long currentUserId = StpUtil.getLoginIdAsLong(); + String currentUsername = LoginHelper.getUsername(); + Long currentDeptId = LoginHelper.getDeptId(); + boolean isSuperAdmin = LoginHelper.isSuperAdmin(); + + log.info("==================== 开始应用数据权限过滤 ===================="); + log.info("当前用户: {}, ID: {}, 部门ID: {}, 超级管理员: {}", + currentUsername, currentUserId, currentDeptId, isSuperAdmin); + + // 超级管理员不需要过滤 + if (isSuperAdmin) { + log.info("→ 权限结果:超级管理员,跳过所有过滤"); + log.info("==================== 权限过滤结束 ===================="); + return; + } + + // 申请人只能看到自己的申请 + boolean hasApplicantRole = StpUtil.hasRole("rs-Applicant"); + log.info("✓ 角色检查 [rs-Applicant]: {}", hasApplicantRole); + if (hasApplicantRole) { + approval.setApplicantId(currentUserId); + log.info("→ 权限结果:申请人角色,设置申请人过滤 applicantId={}", currentUserId); + log.info("==================== 权限过滤结束 ===================="); + return; + } + + // 人事管理员可以看到所有申请 + boolean hasAdminRole = StpUtil.hasRole("rs-admin"); + log.info("✓ 角色检查 [rs-admin]: {}", hasAdminRole); + if (hasAdminRole) { + log.info("→ 权限结果:管理员角色,不设置过滤(可查看所有数据)"); + log.info("==================== 权限过滤结束 ===================="); + return; // 全局权限 + } + + // 🔧 检查部门负责人权限(但不能与其他审批权限冲突) + boolean isDeptApprover = isDeptApprover(currentUserId); + log.info("✓ 部门负责人检查结果: {}", isDeptApprover); + + // 🆕 动态查询用户的审批级别 + List approvalLevels = getApprovalLevelsByUser(currentUserId); + + if (approvalLevels == null || approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限,返回空列表", currentUsername); + approval.setApproverId(-1L); // 设置一个不存在的ID,确保查不到数据 + log.info("==================== 权限过滤结束 ===================="); + return; + } + + log.info("✅ 用户 {} 拥有审批级别: {}", currentUsername, approvalLevels); + + // 🔴 关键改进:将审批级别列表存储到审批对象中 + approval.setApprovalLevels(approvalLevels); + + // 🔴 关键改进:只对 level=1(部门负责人)进行部门过滤 + if (approvalLevels.contains("1")) { + // 如果用户有第1级审批权限,需要特殊处理部门过滤 + // 但不能直接设置deptId,因为可能同时有其他级别的审批权限 + // 解决方案:在Mapper中根据approvalLevel动态应用部门过滤 + approval.setDeptId(currentDeptId); + log.info("✅ 用户拥有第1级审批权限(部门负责人),设置部门过滤 deptId={}", currentDeptId); + } + + log.info("→ 权限结果:多级审批权限,审批级别={}", approvalLevels); + log.info("==================== 权限过滤结束 ===================="); + } + + /** + * 判断用户是否为部门负责人(第1级审批权限) + * + * @param userId 用户ID + * @return 是否为部门负责人 + */ + private boolean isDeptApprover(Long userId) { + // 检查是否有第1级审批权限(部门负责人) + if (StpUtil.hasPermission("rs:training:approve1")) { + log.info("✓ 检测到部门负责人权限: rs:training:approve1"); + return true; + } + // 检查是否有部门负责人角色 + if (StpUtil.hasRole("dept_head_role")) { + log.info("✓ 检测到部门负责人角色: dept_head_role"); + return true; + } + // 兼容旧版权限标识符 + if (StpUtil.hasPermission("training:application:dept_head:approve")) { + log.info("✓ 检测到旧版部门负责人权限: training:application:dept_head:approve"); + return true; + } + return false; + } + + /** + * 🆕 动态获取用户的审批级别列表(培训审批) + * + * @param userId 用户ID + * @return 可审批的级别列表,例如:["1", "4"] + */ + private List getApprovalLevelsByUser(Long userId) { + try { + List approvalLevels = new ArrayList<>(); + + // 检查7个审批级别的权限(1-7级,2a-教务处,2b-学生处) + // 第1级:部门负责人 + if (StpUtil.hasPermission("rs:training:approve1")) { + approvalLevels.add("1"); + log.info("✅ 用户 {} 拥有第1级审批权限(部门负责人)", userId); + } + + // 第2a级:教务处 + if (StpUtil.hasPermission("rs:training:approve2a")) { + approvalLevels.add("2a"); + log.info("✅ 用户 {} 拥有第2a级审批权限(教务处)", userId); + } + + // 第2b级:学生处 + if (StpUtil.hasPermission("rs:training:approve2b")) { + approvalLevels.add("2b"); + log.info("✅ 用户 {} 拥有第2b级审批权限(学生处)", userId); + } + + // 第3级:计划财务处 + if (StpUtil.hasPermission("rs:training:approve3")) { + approvalLevels.add("3"); + log.info("✅ 用户 {} 拥有第3级审批权限(计划财务处)", userId); + } + + // 第4级:人事处 + if (StpUtil.hasPermission("rs:training:approve4")) { + approvalLevels.add("4"); + log.info("✅ 用户 {} 拥有第4级审批权限(人事处)", userId); + } + + // 第5级:业务分管领导 + if (StpUtil.hasPermission("rs:training:approve5")) { + approvalLevels.add("5"); + log.info("✅ 用户 {} 拥有第5级审批权限(业务分管领导)", userId); + } + + // 第6级:人事分管院领导 + if (StpUtil.hasPermission("rs:training:approve6")) { + approvalLevels.add("6"); + log.info("✅ 用户 {} 拥有第6级审批权限(人事分管院领导)", userId); + } + + // 第7级:院长 + if (StpUtil.hasPermission("rs:training:approve7")) { + approvalLevels.add("7"); + log.info("✅ 用户 {} 拥有第7级审批权限(院长)", userId); + } + + // 检查通用审批权限(超级管理员) + if (StpUtil.hasPermission("rs:training:approve")) { + log.info("✅ 用户 {} 拥有通用审批权限,可审批所有级别", userId); + approvalLevels.clear(); + approvalLevels.addAll(Arrays.asList("1", "2a", "2b", "3", "4", "5", "6", "7")); + } + + if (approvalLevels.isEmpty()) { + log.warn("❌ 用户 {} 没有任何培训审批权限", userId); + } else { + log.info("📋 用户 {} 可审批的级别: {}", userId, approvalLevels); + } + + return approvalLevels; + + } catch (Exception e) { + log.error("❌ 获取用户 {} 的审批级别失败", userId, e); + return new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml index a022c1e5..33d40c2d 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml @@ -9,6 +9,7 @@ + @@ -25,6 +26,7 @@ + @@ -41,10 +43,12 @@ SELECT - a.application_id, a.course_id, a.course_name as custom_course_name, a.user_id, a.user_name, a.department, a.position, + a.application_id, a.course_id, a.course_name as custom_course_name, a.user_id, a.user_name, + u.nick_name, + a.department, a.position, a.current_title, a.current_position, a.work_start_time, a.phone, a.organizer_unit, a.training_type, a.training_fee, a.companions, a.training_purpose, a.attachment_ids, - a.reason, a.status, a.apply_time, a.current_approver, a.location, a.duration, a.start_time, a.end_time, + a.reason, a.status, a.apply_time, a.current_approver, a.approval_level, a.location, a.duration, a.start_time, a.end_time, a.tenant_id, a.create_dept, a.reject_time, a.create_by, a.create_time, a.update_by, a.update_time, CASE WHEN a.course_id = 0 THEN a.course_name @@ -52,6 +56,7 @@ END as course_name FROM rs_training_application a LEFT JOIN rs_training_course c ON a.course_id = c.course_id + LEFT JOIN sys_user u ON a.user_id = u.user_id - + - + diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java index 7d34abdd..907b0b10 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysTeacherServiceImpl.java @@ -7,7 +7,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; -import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.system.domain.SysUser; @@ -88,10 +87,10 @@ public class SysTeacherServiceImpl implements ISysTeacherService { public List getDeptTreeData() { // 获取有教师的部门 List teacherDepts = baseMapper.selectDeptTreeWithTeachers(); - + // 补全父级部门 List completeDepts = completeParentDepts(teacherDepts); - + // 构建树形结构 return buildDeptTree(completeDepts, 0L); } @@ -105,7 +104,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { private List completeParentDepts(List teacherDepts) { Set allDeptIds = new HashSet<>(); Set parentIds = new HashSet<>(); - + // 收集所有部门ID和父级ID for (SysDeptTreeVo dept : teacherDepts) { allDeptIds.add(dept.getId()); @@ -113,7 +112,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { collectParentIds(dept.getParentId(), parentIds); } } - + // 找出缺失的父级部门ID Set missingParentIds = new HashSet<>(); for (Long parentId : parentIds) { @@ -121,13 +120,13 @@ public class SysTeacherServiceImpl implements ISysTeacherService { missingParentIds.add(parentId); } } - + // 查询缺失的父级部门 List completeDepts = new ArrayList<>(teacherDepts); if (!missingParentIds.isEmpty()) { List parentDepts = baseMapper.selectDeptsByIds(new ArrayList<>(missingParentIds)); completeDepts.addAll(parentDepts); - + // 递归补全更上级的父级部门 List moreParents = completeParentDepts(parentDepts); for (SysDeptTreeVo parent : moreParents) { @@ -136,14 +135,14 @@ public class SysTeacherServiceImpl implements ISysTeacherService { } } } - + return completeDepts; } - + /** * 递归收集所有父级部门ID * - * @param parentId 父级部门ID + * @param parentId 父级部门ID * @param parentIds 父级ID集合 */ private void collectParentIds(Long parentId, Set parentIds) { @@ -161,7 +160,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { */ private List buildDeptTree(List flatList, Long parentId) { List tree = new ArrayList<>(); - + for (SysDeptTreeVo dept : flatList) { if (ObjectUtil.equal(dept.getParentId(), parentId)) { List children = buildDeptTree(flatList, dept.getId()); @@ -169,7 +168,7 @@ public class SysTeacherServiceImpl implements ISysTeacherService { tree.add(dept); } } - + return tree; } @@ -198,10 +197,10 @@ public class SysTeacherServiceImpl implements ISysTeacherService { if (ObjectUtil.isNull(userId)) { return null; } + // ⚠️ 注意:基础条件(del_flag='0' 和 account_type='1')已在Mapper的SQL中定义 + // 这里只添加userId条件,避免生成重复的WHERE子句 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(SysUser::getUserId, userId); - lqw.eq(SysUser::getDelFlag, SystemConstants.NORMAL); - lqw.eq(SysUser::getAccountType, "1"); // 教师账号类型 List list = baseMapper.selectTeacherList(lqw); return CollUtil.isNotEmpty(list) ? list.get(0) : null; } @@ -214,11 +213,10 @@ public class SysTeacherServiceImpl implements ISysTeacherService { */ private LambdaQueryWrapper buildQueryWrapper(SysTeacherBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - - // 基础条件:未删除的教师用户 - lqw.eq(SysUser::getDelFlag, SystemConstants.NORMAL); - lqw.eq(SysUser::getAccountType, "1"); // 教师账号类型 - + + // ⚠️ 注意:基础条件(del_flag='0' 和 account_type='1')已在Mapper的SQL中定义 + // 这里只添加动态查询条件,避免生成重复的WHERE子句 + // 动态查询条件 lqw.eq(ObjectUtil.isNotNull(bo.getUserId()), SysUser::getUserId, bo.getUserId()); lqw.like(StrUtil.isNotBlank(bo.getUserName()), SysUser::getUserName, bo.getUserName()); @@ -228,19 +226,18 @@ public class SysTeacherServiceImpl implements ISysTeacherService { lqw.eq(ObjectUtil.isNotNull(bo.getDeptId()), SysUser::getDeptId, bo.getDeptId()); lqw.eq(StrUtil.isNotBlank(bo.getSex()), SysUser::getSex, bo.getSex()); lqw.eq(StrUtil.isNotBlank(bo.getStatus()), SysUser::getStatus, bo.getStatus()); - + // 搜索关键词(支持姓名、账号、手机号) if (StrUtil.isNotBlank(bo.getSearchKeyword())) { String keyword = bo.getSearchKeyword(); lqw.and(wrapper -> wrapper - .like(SysUser::getNickName, keyword) - .or() - .like(SysUser::getUserName, keyword) - .or() - .like(SysUser::getPhonenumber, keyword) - ); + .like(SysUser::getNickName, keyword) + .or() + .like(SysUser::getUserName, keyword) + .or() + .like(SysUser::getPhonenumber, keyword)); } - + return lqw; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/web/src/api/rs/training.ts b/web/src/api/rs/training.ts index e57bd961..deb292f3 100644 --- a/web/src/api/rs/training.ts +++ b/web/src/api/rs/training.ts @@ -432,3 +432,29 @@ export function updateRecordAttachments(recordId: string | number, data: { attac data: data }); } + +// ============================================ +// 7级审批相关接口(2025-01-04新增) +// ============================================ + +// 审批培训申请(7级审批制度) +export function approveTrainingApplication(data: { + applicationId: number | string; + approvalStatus: string; // 1-通过,2-拒绝 + approvalOpinion?: string; + approvalSignature?: string; // base64格式 +}) { + return request({ + url: '/rs/training/application/approve', + method: 'post', + data: data + }); +} + +// 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤) +export function getPendingTrainingApplications() { + return request({ + url: '/rs/training/application/pending', + method: 'get' + }); +} diff --git a/web/src/views/rs/training/index.vue b/web/src/views/rs/training/index.vue index 37da2719..291ad56b 100644 --- a/web/src/views/rs/training/index.vue +++ b/web/src/views/rs/training/index.vue @@ -79,6 +79,16 @@ + + + + + + - + - + @@ -387,7 +397,7 @@ - {{ applicationDetail.userName }} + {{ applicationDetail.nickName || applicationDetail.userName }} {{ applicationDetail.department }} {{ applicationDetail.currentTitle || '暂无' }} {{ applicationDetail.currentPosition || '暂无' }} @@ -660,14 +670,9 @@ class="attachment-upload-demo" drag list-type="picture-card" - :action="`${baseApi}/resource/oss/upload`" - :headers="{ - 'Authorization': `Bearer ${getToken()}`, - 'clientid': clientId - }" - :data="{ - bizType: 'training_application_attachment' - }" + :action="applicationUploadAction" + :headers="applicationUploadHeaders" + :data="applicationUploadData" :auto-upload="true" :before-upload="beforeApplicationAttachmentUpload" :on-success="handleApplicationAttachmentSuccess" @@ -778,14 +783,9 @@ class="certificate-upload-demo" drag list-type="picture-card" - :action="`${baseApi}/resource/oss/upload`" - :headers="{ - 'Authorization': `Bearer ${getToken()}`, - 'clientid': clientId - }" - :data="{ - bizType: 'training_certificate_attachment' - }" + :action="recordCertificateUploadAction" + :headers="recordCertificateUploadHeaders" + :data="recordCertificateUploadData" :auto-upload="true" :before-upload="beforeRecordCertificateUpload" :on-success="handleRecordCertificateSuccess" @@ -1022,9 +1022,9 @@ import { getFilePreviewUrl } from '@/api/system/file'; import { getToken } from '@/utils/auth'; import { getInfo as getUserInfo } from '@/api/login'; import { useUserStore } from '@/store/modules/user'; -import { listClassTeacherCounselorRel } from '@/api/xg/classTeacherCounselorRel'; import TeacherMultiSelector from '@/components/TeacherMultiSelector/index.vue'; import { TeacherVO } from '@/api/system/teacher/types'; +import { listTeacher } from '@/api/system/teacher'; const { proxy } = getCurrentInstance() as ComponentInternalInstance; const userStore = useUserStore(); @@ -1034,6 +1034,52 @@ const baseApi = import.meta.env.VITE_APP_BASE_API; const clientId = import.meta.env.VITE_APP_CLIENT_ID; const animate = proxy?.animate || { searchAnimate: { enter: 'animate__fadeInUp', leave: 'animate__fadeOutDown' } }; +// ===== 申请培训附件上传配置(统一使用computed方式) ===== +const applicationUploadAction = computed(() => { + const api = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'; + console.log('[申请培训附件上传配置] 上传地址:', api); + return api; +}); + +const applicationUploadHeaders = computed(() => { + const token = getToken() || ''; + const headers = { + 'Authorization': `Bearer ${token}`, + 'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e' + }; + console.log('[申请培训附件上传配置] 请求头:', headers); + return headers; +}); + +const applicationUploadData = computed(() => { + return { + bizType: 'training_application_attachment' + }; +}); + +// ===== 培训证书上传配置(统一使用computed方式) ===== +const recordCertificateUploadAction = computed(() => { + const api = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'; + console.log('[培训证书上传配置] 上传地址:', api); + return api; +}); + +const recordCertificateUploadHeaders = computed(() => { + const token = getToken() || ''; + const headers = { + 'Authorization': `Bearer ${token}`, + 'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e' + }; + console.log('[培训证书上传配置] 请求头:', headers); + return headers; +}); + +const recordCertificateUploadData = computed(() => { + return { + bizType: 'training_certificate_attachment' + }; +}); + // 培训申请相关 // 培训课程遮罩层 const trainingLoading = ref(false); @@ -1212,7 +1258,8 @@ const applicationDetail = reactive({ approvalTime: '', remark: '', adminRemark: '', - certificatePath: '' + certificatePath: '', + attachmentIds: '' // 添加附件ID字段 }); // 审批历史相关 @@ -1650,14 +1697,19 @@ const getCompletionStatusLabel = (status: string) => { /** 获取教师列表 */ function getTeacherList() { - listClassTeacherCounselorRel({}) - .then((response) => { + // 使用 system/teacher 接口获取教师列表,设置大的pageSize以获取所有教师 + listTeacher({ + pageNum: 1, + pageSize: 9999 // 设置足够大的pageSize以获取所有教师 + }) + .then((response: any) => { const teachers = response.rows || []; - teacherList.value = teachers.map((item: any) => ({ - teacherId: item.teacherId, - teacherName: item.teacherName, - teacherType: item.teacherType + teacherList.value = teachers.map((item: TeacherVO) => ({ + teacherId: item.userId, // 使用 userId 作为 teacherId + teacherName: item.nickName || item.userName, // 使用 nickName 或 userName + teacherType: '' // system/teacher 接口没有 teacherType 字段 })); + console.log('初始化教师列表加载完成:', teacherList.value.length, '位教师'); }) .catch(() => { console.error('获取教师列表失败'); @@ -1667,14 +1719,20 @@ function getTeacherList() { /** 异步加载教师列表用于详情显示 */ async function loadTeacherListForDisplay() { try { - const response = await listClassTeacherCounselorRel({}); + console.log('开始加载教师列表...'); + // 使用 system/teacher 接口获取教师列表,设置大的pageSize以获取所有教师 + const response: any = await listTeacher({ + pageNum: 1, + pageSize: 9999 // 设置足够大的pageSize以获取所有教师 + }); const teachers = response.rows || []; - teacherList.value = teachers.map((item: any) => ({ - teacherId: item.teacherId, - teacherName: item.teacherName, - teacherType: item.teacherType + teacherList.value = teachers.map((item: TeacherVO) => ({ + teacherId: item.userId, // 使用 userId 作为 teacherId + teacherName: item.nickName || item.userName, // 使用 nickName 或 userName + teacherType: '' // system/teacher 接口没有 teacherType 字段 })); console.log('教师列表加载完成:', teacherList.value.length, '位教师'); + console.log('教师ID列表:', teacherList.value.map(t => t.teacherId)); } catch (error) { console.error('获取教师列表失败:', error); teacherList.value = []; @@ -1772,13 +1830,14 @@ async function handleAddApplication() { cachedDeptName: userStore.deptName }); - // 自动填充申请人姓名(使用用户名称) - const displayName = userStore.name || `用户${userStore.userId}`; + // 自动填充申请人姓名(使用用户昵称,即真实姓名) + const displayName = userStore.nickname || userStore.name || `用户${userStore.userId}`; applicationForm.userName = displayName; console.log('申请人姓名自动填充:', { userId: userStore.userId, displayName: displayName, + nickname: userStore.nickname, userName: userStore.name }); @@ -1918,6 +1977,10 @@ function cancelApplicationDialog() { function submitApplicationForm() { applicationFormRef.value?.validate((valid: boolean) => { if (valid) { + console.log('[提交培训申请] 提交前检查'); + console.log('[提交培训申请] applicationForm.attachmentIds:', applicationForm.attachmentIds); + console.log('[提交培训申请] applicationAttachmentFileList:', applicationAttachmentFileList.value); + applicationDialogLoading.value = true; // 创建符合API要求的数据结构 @@ -1945,6 +2008,9 @@ function submitApplicationForm() { // 附件信息 attachmentIds: applicationForm.attachmentIds.join(',') }; + + console.log('[提交培训申请] 最终提交的API数据:', apiData); + console.log('[提交培训申请] attachmentIds字段值:', apiData.attachmentIds); // 如果是编辑现有申请,添加applicationId if (applicationForm.applicationId) { @@ -2068,6 +2134,7 @@ function formatFileSize(bytes: number | string | undefined): string { /** 获取同行人姓名列表 */ function getCompanionNames(companions: (string | number)[] | string | null | undefined): string { if (!companions) { + console.log('[同行人解析] 同行人数据为空'); return ''; } @@ -2078,33 +2145,47 @@ function getCompanionNames(companions: (string | number)[] | string | null | und try { companionArray = JSON.parse(companions); if (!Array.isArray(companionArray)) { + console.log('[同行人解析] JSON解析结果不是数组:', companionArray); return ''; } + console.log('[同行人解析] JSON解析成功:', companionArray); } catch (e) { // 如果JSON解析失败,可能是逗号分隔的字符串 companionArray = companions.split(',').map(id => id.trim()).filter(id => id); + console.log('[同行人解析] 使用逗号分隔解析:', companionArray); } } else if (Array.isArray(companions)) { companionArray = companions; + console.log('[同行人解析] 直接使用数组:', companionArray); } if (companionArray.length === 0) { + console.log('[同行人解析] 同行人数组为空'); return ''; } // 如果教师列表还未加载,显示"加载中..." if (teacherList.value.length === 0) { + console.log('[同行人解析] 教师列表未加载,返回加载中'); return '加载中...'; } - return companionArray.map(teacherId => { + console.log('[同行人解析] 教师列表已加载:', teacherList.value.length, '位教师'); + console.log('[同行人解析] 需要查找的ID:', companionArray); + + const names = companionArray.map(teacherId => { // 确保teacherId转换为正确的类型进行比较 const teacher = teacherList.value.find(t => String(t.teacherId) === String(teacherId) || Number(t.teacherId) === Number(teacherId) ); - return teacher ? teacher.teacherName : `未知教师(ID:${teacherId})`; + const name = teacher ? teacher.teacherName : `未知教师(ID:${teacherId})`; + console.log(`[同行人解析] ID ${teacherId} -> ${name}`); + return name; }).join('、'); + + console.log('[同行人解析] 最终结果:', names); + return names; } /** 格式化同行人员显示(表格列使用) */ @@ -2114,6 +2195,13 @@ function formatCompanions(companions: string | undefined): string { /** 查看申请详情 */ async function handleViewApplicant(row: any) { + console.log('[查看申请详情] 原始数据:', { + userId: row.userId, + userName: row.userName, + nickName: row.nickName, + attachmentIds: row.attachmentIds // 添加附件ID日志 + }); + // 如果行数据中没有完整的课程名称,尝试从课程列表获取 if (!row.courseName && row.courseId) { const course = courseList.value.find((c) => c.courseId === row.courseId); @@ -2130,6 +2218,12 @@ async function handleViewApplicant(row: any) { // 复制申请详情 Object.assign(applicationDetail, row); + console.log('[查看申请详情] 详情数据已设置:', { + userName: applicationDetail.userName, + nickName: applicationDetail.nickName, + attachmentIds: applicationDetail.attachmentIds // 添加附件ID日志 + }); + // 加载申请详情的附件数据 await handleApplicationDetailAttachmentData(row.attachmentIds); @@ -2143,7 +2237,7 @@ function handleEditApplication(row: any) { applicationDialogTitle.value = '修改培训申请'; // 复制申请数据到表单 - 新字段结构 - applicationForm.userName = row.userName || userStore.name; + applicationForm.userName = row.userName || userStore.nickname || userStore.name; applicationForm.department = row.department; applicationForm.currentTitle = row.currentTitle || ''; applicationForm.currentPosition = row.currentPosition || ''; @@ -2236,7 +2330,7 @@ async function handleUpdateAndResubmitApplication(row: any) { if (response.code === 200 && response.data) { // 设置表单数据 - 新字段结构 const data = response.data; - applicationForm.userName = data.userName || userStore.name; + applicationForm.userName = data.userName || userStore.nickname || userStore.name; applicationForm.department = data.department; applicationForm.currentTitle = data.currentTitle || ''; applicationForm.currentPosition = data.currentPosition || ''; @@ -2279,7 +2373,7 @@ function handleResubmitApplication(row: any) { applicationDialogTitle.value = '重新提交培训申请'; // 复制申请数据到表单 - 新字段结构 - applicationForm.userName = row.userName || userStore.name; + applicationForm.userName = row.userName || userStore.nickname || userStore.name; applicationForm.department = row.department; applicationForm.currentTitle = row.currentTitle || ''; applicationForm.currentPosition = row.currentPosition || ''; @@ -3728,6 +3822,13 @@ onMounted(() => { /** 申请培训附件上传前检查 */ function beforeApplicationAttachmentUpload(file: File) { + console.log('[申请培训附件上传前检查] 开始检查'); + console.log('[申请培训附件上传前检查] 文件信息:', { + name: file.name, + type: file.type, + size: `${(file.size / 1024 / 1024).toFixed(2)}MB` + }); + const isValidType = [ 'application/pdf', 'application/msword', @@ -3740,26 +3841,32 @@ function beforeApplicationAttachmentUpload(file: File) { const isLt20M = file.size / 1024 / 1024 < 20; if (!isValidType) { + console.error('[申请培训附件上传前检查] 文件类型不符合要求:', file.type); proxy?.$modal.msgError('上传附件文件只能是PDF/Word/Excel/图片格式!'); return false; } if (!isLt20M) { + console.error('[申请培训附件上传前检查] 文件大小超出限制:', `${(file.size / 1024 / 1024).toFixed(2)}MB`); proxy?.$modal.msgError('上传附件文件大小不能超过20MB!'); return false; } + console.log('[申请培训附件上传前检查] 检查通过'); return true; } /** 申请培训附件上传成功处理 */ function handleApplicationAttachmentSuccess(response: any, file: any) { + console.log('[申请培训附件上传成功] 响应数据:', response); + console.log('[申请培训附件上传成功] 文件对象:', file); + if (response.code === 200 && response.data) { file.url = response.data.url; file.ossId = response.data.ossId; proxy?.$modal.msgSuccess('附件上传成功!'); - console.log('申请培训附件上传成功:', response.data); + console.log('[申请培训附件上传成功] OSS ID:', response.data.ossId); // 更新文件状态为成功 file.status = 'success'; @@ -3771,24 +3878,55 @@ function handleApplicationAttachmentSuccess(response: any, file: any) { } /** 申请培训附件上传失败处理 */ -function handleApplicationAttachmentError(error: any) { - console.error('申请培训附件上传失败:', error); +function handleApplicationAttachmentError(error: any, file: any) { + console.error('[申请培训附件上传失败] 错误对象:', error); + console.error('[申请培训附件上传失败] 文件对象:', file); + let errorMessage = '附件上传失败,请重试!'; + let errorDetails = ''; // 尝试从错误响应中获取具体错误信息 - if (error && error.response && error.response.data) { - if (error.response.data.msg) { - errorMessage = error.response.data.msg; - } else if (error.response.data.message) { - errorMessage = error.response.data.message; + if (error && error.response) { + console.error('[申请培训附件上传失败] HTTP响应:', { + status: error.response.status, + statusText: error.response.statusText, + data: error.response.data + }); + + if (error.response.data) { + if (error.response.data.msg) { + errorMessage = error.response.data.msg; + } else if (error.response.data.message) { + errorMessage = error.response.data.message; + } } + + // 常见错误状态码处理 + if (error.response.status === 401) { + errorDetails = '认证失败,请重新登录'; + } else if (error.response.status === 403) { + errorDetails = '没有上传权限'; + } else if (error.response.status === 404) { + errorDetails = '上传接口不存在'; + } else if (error.response.status === 413) { + errorDetails = '文件太大,服务器拒绝接收'; + } else if (error.response.status >= 500) { + errorDetails = '服务器内部错误'; + } + } else if (error.message) { + console.error('[申请培训附件上传失败] 错误信息:', error.message); + errorDetails = error.message; } - proxy?.$modal.msgError(errorMessage); + const finalMessage = errorDetails ? `${errorMessage} (${errorDetails})` : errorMessage; + proxy?.$modal.msgError(finalMessage); } /** 更新applicationForm中的attachmentIds字段 */ function updateApplicationFormAttachmentIds() { + console.log('[更新附件IDs] 开始更新'); + console.log('[更新附件IDs] 文件列表:', applicationAttachmentFileList.value); + // 提取所有成功上传的ossId const attachmentOssIds = applicationAttachmentFileList.value .filter((file) => file.status === 'success' && file.ossId) @@ -3797,7 +3935,8 @@ function updateApplicationFormAttachmentIds() { // 更新applicationForm applicationForm.attachmentIds = attachmentOssIds; - console.log('更新applicationForm.attachmentIds:', attachmentOssIds); + console.log('[更新附件IDs] 提取的OSS IDs:', attachmentOssIds); + console.log('[更新附件IDs] applicationForm.attachmentIds:', applicationForm.attachmentIds); } /** 移除申请培训附件 */ @@ -3831,12 +3970,18 @@ function handlePreviewApplicationAttachment(file: any) { /** 下载申请培训附件 */ async function handleDownloadApplicationAttachment(file: any) { + console.log('[下载申请培训附件] 开始下载'); + console.log('[下载申请培训附件] 文件信息:', file); + if (!file.ossId) { + console.error('[下载申请培训附件] ossId为空,无法下载'); proxy?.$modal.msgError('文件信息不完整,无法下载'); return; } try { + console.log('[下载申请培训附件] 请求下载接口:', `/resource/oss/download/${file.ossId}`); + // 使用后端安全下载接口,通过request工具发送请求以包含认证头 const response = await request({ url: `/resource/oss/download/${file.ossId}`, @@ -3844,6 +3989,8 @@ async function handleDownloadApplicationAttachment(file: any) { responseType: 'blob' // 指定响应类型为blob }); + console.log('[下载申请培训附件] 接口响应:', response); + // 创建blob URL并触发下载 const blob = response instanceof Blob ? response : new Blob([response.data]); const url = window.URL.createObjectURL(blob); @@ -3859,9 +4006,30 @@ async function handleDownloadApplicationAttachment(file: any) { document.body.removeChild(link); window.URL.revokeObjectURL(url); - } catch (error) { - console.error('下载附件失败:', error); - proxy?.$modal.msgError('下载附件失败,请检查文件权限或稍后重试'); + console.log('[下载申请培训附件] 下载成功'); + proxy?.$modal.msgSuccess('文件下载成功'); + + } catch (error: any) { + console.error('[下载申请培训附件] 下载失败:', error); + + let errorMessage = '下载附件失败,请稍后重试'; + + if (error.response) { + console.error('[下载申请培训附件] HTTP响应错误:', { + status: error.response.status, + statusText: error.response.statusText + }); + + if (error.response.status === 401) { + errorMessage = '认证失败,请重新登录'; + } else if (error.response.status === 403) { + errorMessage = '没有下载权限'; + } else if (error.response.status === 404) { + errorMessage = '文件不存在或已被删除'; + } + } + + proxy?.$modal.msgError(errorMessage); } }