From da8d332fac9e242547e137ab2cd1a27f73cd7d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cwuzhen=E2=80=9D?= Date: Mon, 26 Oct 2026 13:44:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E4=BF=AE=E5=A4=8D=E7=BD=91?= =?UTF-8?q?=E5=AE=89=E6=89=AB=E6=8F=8F=E7=9B=B8=E5=85=B3=E4=B8=A5=E9=87=8D?= =?UTF-8?q?/=E9=AB=98=E5=8D=B1/=E4=B8=AD=E5=8D=B1=E6=BC=8F=E6=B4=9E?= =?UTF-8?q?=E5=B9=B6=E5=8A=A0=E5=9B=BA=E5=AD=A6=E7=94=9F=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E9=87=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关闭工作流 SpEL 命令执行与社团活动 SQL 注入,补齐越权、调试接口、登录喷洒和异常回显等问题;学生密码改为随机临时密码,Web/移动端支持复制。 Constraint: 流程发起/办理接口保持仅登录校验,避免影响请假审批 Rejected: 开启验证码 | Web/移动端登录页均未实现,开启会导致无法登录 Confidence: high Scope-risk: broad Not-tested: 生产环境与社团社长/指导老师视角 Co-authored-by: Cursor --- .../dromara/web/controller/CasController.java | 5 +- .../web/listener/UserActionListener.java | 8 +- .../dromara/web/service/SysLoginService.java | 36 ++++++ .../service/impl/PasswordAuthStrategy.java | 6 +- .../src/main/resources/application-prod.yml | 3 +- .../src/main/resources/application.yml | 9 +- .../main/resources/i18n/messages.properties | 1 + .../resources/i18n/messages_en_US.properties | 1 + .../resources/i18n/messages_zh_CN.properties | 1 + .../common/core/utils/file/MimeTypeUtils.java | 42 +++++++ .../handler/MybatisExceptionHandler.java | 2 +- .../common/sse/controller/SseController.java | 28 ----- .../common/web/config/ResourcesConfig.java | 8 +- .../web/handler/GlobalExceptionHandler.java | 41 ++++-- .../TeacherAssessmentController.java | 15 +-- .../CwApprovalRecordController.java | 5 +- .../HqPmRepairAttachmentController.java | 16 ++- .../hq/controller/HqSatSurveyController.java | 7 +- .../JcbtScholarshipApplicationController.java | 84 +------------ ...udyApplicationCollegeReviewController.java | 48 ------- .../JcbtWorkStudyApplicationController.java | 48 +------ .../controller/RsApplicationController.java | 12 +- .../rs/controller/RsInitController.java | 7 +- ...erseasApplicationAttachmentController.java | 3 +- .../RsOverseasApplicationController.java | 7 +- .../RsOverseasApprovalController.java | 37 +----- .../RsOverseasRecordController.java | 3 +- .../RsTrainingApplicationController.java | 11 +- .../RsTrainingRecordController.java | 27 ---- ...rseasApplicationAttachmentServiceImpl.java | 6 +- .../StglActivityAttendanceController.java | 6 + .../StglActivityRegistrationController.java | 5 +- .../stgl/controller/StglClubController.java | 21 ---- .../StglClubPermissionController.java | 13 +- .../StglVolunteerTeamController.java | 3 +- .../service/impl/StglActivityServiceImpl.java | 118 ++++-------------- .../mapper/stgl/StglActivityMapper.xml | 44 ++++++- .../system/SysBucketPasswordController.java | 9 +- .../system/SysConfigController.java | 16 +++ .../system/SysFileAccessController.java | 31 +++-- .../wechat/SysWeChatController.java | 15 +-- .../service/impl/SysOssServiceImpl.java | 25 +++- .../service/impl/SysWeChatServiceImpl.java | 4 +- .../constant/FlowPermissionConstant.java | 28 +++++ .../workflow/config/WarmFlowConfig.java | 58 ++++++++- .../expression/SafeExpressionStrategies.java | 83 ++++++++++++ .../config/expression/SafeSpelHelper.java | 98 +++++++++++++++ .../controller/FlwDefinitionController.java | 15 +++ .../controller/FlwInstanceController.java | 8 ++ .../controller/FlwTaskController.java | 5 + .../service/impl/FlwInstanceServiceImpl.java | 16 +++ .../XgStuColBatchClassController.java | 13 +- .../controller/XgStudentLeaveController.java | 3 +- .../xg/controller/XgStudentMsgController.java | 8 +- .../xg/service/IXgStudentMsgService.java | 6 +- .../impl/XgStudentMsgBatchServiceImpl.java | 18 +-- .../service/impl/XgStudentMsgServiceImpl.java | 57 ++++++++- ...XljkConsultationAppointmentController.java | 4 + .../XljkConsultationNoShowController.java | 5 + ...khAssessmentApplicationApprovalMapper.java | 14 +++ ...essmentApplicationApprovalServiceImpl.java | 62 +++++++++ .../controller/InternshipInfoController.java | 13 +- .../ZsjyEnrollmentInfoController.java | 5 +- .../ZsjyEnrollmentProcessController.java | 2 + .../impl/ZsjyInternshipDailyServiceImpl.java | 9 +- ...update_20261026_student_pwd_reset_menu.sql | 2 +- .../update_20261026_workflow_permission.sql | 51 ++++++++ docs/开发文档/API/学生密码重置API.md | 20 +-- mobile/src/api/studentPwdReset.js | 2 +- .../pages/xg/xsjcxx/studentPwdReset/index.vue | 36 +++++- .../xg/xsjcxx/studentMsg/studentMsg/index.ts | 2 +- .../components/SocialCallback/index.vue | 7 ++ web/src/utils/htmlSafe.ts | 62 +++++++++ web/src/views/hq/pm/repairOrder/index.vue | 5 +- web/src/views/rs/index.vue | 15 +-- web/src/views/rs/recruitment-portal.vue | 5 +- .../studentMsg/studentPwdReset/index.vue | 50 +++++++- 问题整改/20260922-安全扫描修改.md | 19 +++ 问题整改/20261026-安全扫描整改说明.md | 60 +++++++++ 79 files changed, 1156 insertions(+), 547 deletions(-) create mode 100644 backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowPermissionConstant.java create mode 100644 backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeExpressionStrategies.java create mode 100644 backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeSpelHelper.java create mode 100644 backend/script/sql/update/update_20261026_workflow_permission.sql create mode 100644 web/src/utils/htmlSafe.ts create mode 100644 问题整改/20261026-安全扫描整改说明.md diff --git a/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java b/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java index fbaf5efd..c030d6b0 100644 --- a/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java +++ b/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java @@ -1,5 +1,6 @@ package org.dromara.web.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.domain.R; @@ -178,14 +179,14 @@ public class CasController { return R.fail(ue.getMessage()); } catch (Exception e) { log.error("处理 CAS profile 失败", e); - return R.fail("处理 CAS profile 失败: " + e.getMessage()); + return R.fail("处理 CAS profile 失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } else { return R.fail("无法解析 CAS access_token"); } } catch (Exception e) { log.error("请求 CAS accessToken 失败", e); - return R.fail("请求 CAS accessToken 失败: " + e.getMessage()); + return R.fail("请求 CAS accessToken 失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } diff --git a/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java b/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java index 554c64b3..ba1e88d4 100644 --- a/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java +++ b/backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java @@ -73,7 +73,7 @@ public class UserActionListener implements SaTokenListener { SpringUtils.context().publishEvent(logininforEvent); // 更新登录信息 loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip); - log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue); + log.info("user doLogin, userId:{}", loginId); } /** @@ -85,7 +85,7 @@ public class UserActionListener implements SaTokenListener { TenantHelper.dynamic(tenantId, () -> { RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue); }); - log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue); + log.info("user doLogout, userId:{}", loginId); } /** @@ -97,7 +97,7 @@ public class UserActionListener implements SaTokenListener { TenantHelper.dynamic(tenantId, () -> { RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue); }); - log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue); + log.info("user doKickout, userId:{}", loginId); } /** @@ -109,7 +109,7 @@ public class UserActionListener implements SaTokenListener { TenantHelper.dynamic(tenantId, () -> { RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue); }); - log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue); + log.info("user doReplaced, userId:{}", loginId); } /** diff --git a/backend/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java b/backend/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java index 84b52845..9f5dd37e 100644 --- a/backend/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java +++ b/backend/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java @@ -56,6 +56,9 @@ public class SysLoginService { @Value("${user.password.lockTime}") private Integer lockTime; + @Value("${user.password.ipMaxRetryCount:30}") + private Integer ipMaxRetryCount; + private final ISysTenantService tenantService; private final ISysPermissionService permissionService; private final ISysSocialService sysSocialService; @@ -200,7 +203,23 @@ public class SysLoginService { throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime); } + // 按公网来源 IP 统计失败次数,防止对大量账号逐个试密码;校园内网 / 本机 IP 不计 + String clientIp = ServletUtils.getClientIP(); + String ipErrorKey = null; + if (ipMaxRetryCount > 0 && StringUtils.isNotBlank(clientIp) && isPublicClientIp(clientIp)) { + ipErrorKey = CacheConstants.PWD_ERR_CNT_KEY + "ip:" + clientIp; + int ipErrorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(ipErrorKey), 0); + if (ipErrorNumber >= ipMaxRetryCount) { + recordLogininfor(tenantId, username, loginFail, MessageUtils.message("user.ip.retry.limit.exceed", lockTime)); + throw new UserException("user.ip.retry.limit.exceed", lockTime); + } + } + if (supplier.get()) { + if (ipErrorKey != null) { + int ipErrorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(ipErrorKey), 0); + RedisUtils.setCacheObject(ipErrorKey, ipErrorNumber + 1, Duration.ofMinutes(lockTime)); + } // 错误次数递增 errorNumber++; RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime)); @@ -219,6 +238,23 @@ public class SysLoginService { RedisUtils.deleteObject(errorKey); } + /** + * 是否为需要计入失败锁定的公网 IP。本机、内网 IPv4/IPv6 不计;无法解析时按公网处理,且不得抛异常阻断登录。 + */ + private boolean isPublicClientIp(String ip) { + try { + if (NetUtils.isIPv6(ip)) { + return !NetUtils.isInnerIPv6(ip); + } + if (NetUtils.isIPv4(ip)) { + return !NetUtils.isInnerIP(ip); + } + } catch (Exception e) { + log.warn("无法判断登录来源 IP 是否内网: {}", ip); + } + return true; + } + /** * 校验租户 * diff --git a/backend/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java b/backend/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java index e579f996..704fb8d7 100644 --- a/backend/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java +++ b/backend/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java @@ -64,7 +64,9 @@ public class PasswordAuthStrategy implements IAuthStrategy { } LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> { SysUserVo user = loadUserByUsername(username); - loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword())); + // 用户不存在时按密码错误处理,避免通过提示差异枚举账号 + loginService.checkLogin(LoginType.PASSWORD, tenantId, username, + () -> user == null || !BCrypt.checkpw(password, user.getPassword())); // 此处可根据登录用户的数据不同 自行创建 loginUser return loginService.buildLoginUser(user); }); @@ -112,7 +114,7 @@ public class PasswordAuthStrategy implements IAuthStrategy { SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper().eq(SysUser::getUserName, username)); if (ObjectUtil.isNull(user)) { log.info("登录用户:{} 不存在.", username); - throw new UserException("user.not.exists", username); + return null; } else if (SystemConstants.DISABLE.equals(user.getStatus())) { log.info("登录用户:{} 已被停用.", username); throw new UserException("user.blocked", username); diff --git a/backend/ruoyi-admin/src/main/resources/application-prod.yml b/backend/ruoyi-admin/src/main/resources/application-prod.yml index 2f8daf65..60cf8a72 100644 --- a/backend/ruoyi-admin/src/main/resources/application-prod.yml +++ b/backend/ruoyi-admin/src/main/resources/application-prod.yml @@ -348,7 +348,8 @@ logging: mybatis-plus: configuration: # 生产环境关闭 SQL 日志打印,避免性能影响和敏感信息泄露 - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 生产禁止将完整 SQL 及参数(身份证号、手机号等)打印到控制台,需排查时在 dev 环境开启 + # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启驼峰命名转换 map-underscore-to-camel-case: true # 开启缓存 diff --git a/backend/ruoyi-admin/src/main/resources/application.yml b/backend/ruoyi-admin/src/main/resources/application.yml index 9485988d..f9e21121 100644 --- a/backend/ruoyi-admin/src/main/resources/application.yml +++ b/backend/ruoyi-admin/src/main/resources/application.yml @@ -66,6 +66,8 @@ user: maxRetryCount: 5 # 密码锁定时间(默认10分钟) lockTime: 10 + # 同一公网IP在锁定时间内允许的最大登录失败次数(内网IP不计,0为关闭) + ipMaxRetryCount: 30 # Spring配置 spring: @@ -124,10 +126,12 @@ sa-token: # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) is-share: false # jwt秘钥 - jwt-secret-key: abcdefghijklmnopqrstuvwxyz + jwt-secret-key: 23e9ff71db5bbbb1ccd808d2ec35533ec53ee80740b5185adc8ba9bed7897610 # security配置 security: + # 允许跨域访问的来源(支持 * 通配子域名与 [*] 通配端口);前端均经 nginx 同源访问 /prod-api,新增域名时在此补充 + cors-allowed-origins: "http://*.hzdyjsxy.cn:[*],https://*.hzdyjsxy.cn:[*],http://localhost:[*],http://127.0.0.1:[*]" # 排除路径(P0:已下线 Swagger/api-docs,不再加入白名单) excludes: - /*.html @@ -174,7 +178,8 @@ mybatis-plus: typeAliasesPackage: org.dromara.**.domain # 启用SQL日志输出,用于调试数据库插入问题 configuration: - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 生产禁止将完整 SQL 及参数(身份证号、手机号等)打印到控制台,需排查时在 dev 环境开启 + # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: dbConfig: # 主键类型 diff --git a/backend/ruoyi-admin/src/main/resources/i18n/messages.properties b/backend/ruoyi-admin/src/main/resources/i18n/messages.properties index 2941bb77..6967a294 100644 --- a/backend/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/backend/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -6,6 +6,7 @@ user.not.exists=对不起, 您的账号:{0} 不存在. user.password.not.match=用户不存在/密码错误 user.password.retry.limit.count=密码输入错误{0}次 user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.ip.retry.limit.exceed=登录失败次数过多,请{0}分钟后再试 user.password.delete=对不起,您的账号:{0} 已被删除 user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员 role.blocked=角色已封禁,请联系管理员 diff --git a/backend/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties b/backend/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties index 12df9b46..176a295c 100644 --- a/backend/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties +++ b/backend/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties @@ -6,6 +6,7 @@ user.not.exists=Sorry, your account: {0} does not exist user.password.not.match=User does not exist/Password error user.password.retry.limit.count=Password input error {0} times user.password.retry.limit.exceed=Password input error {0} times, account locked for {1} minutes +user.ip.retry.limit.exceed=Too many failed logins, please try again in {0} minutes user.password.delete=Sorry, your account:{0} has been deleted user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator role.blocked=Role disabled,please contact administrators diff --git a/backend/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties b/backend/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties index 2941bb77..6967a294 100644 --- a/backend/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties +++ b/backend/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties @@ -6,6 +6,7 @@ user.not.exists=对不起, 您的账号:{0} 不存在. user.password.not.match=用户不存在/密码错误 user.password.retry.limit.count=密码输入错误{0}次 user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.ip.retry.limit.exceed=登录失败次数过多,请{0}分钟后再试 user.password.delete=对不起,您的账号:{0} 已被删除 user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员 role.blocked=角色已封禁,请联系管理员 diff --git a/backend/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java b/backend/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java index d8b0621a..ca57baef 100644 --- a/backend/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java +++ b/backend/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java @@ -37,4 +37,46 @@ public class MimeTypeUtils { // pdf "pdf"}; + private static final java.util.Map SAFE_CONTENT_TYPES = java.util.Map.ofEntries( + java.util.Map.entry("bmp", IMAGE_BMP), + java.util.Map.entry("gif", IMAGE_GIF), + java.util.Map.entry("jpg", IMAGE_JPEG), + java.util.Map.entry("jpeg", IMAGE_JPEG), + java.util.Map.entry("png", IMAGE_PNG), + java.util.Map.entry("pdf", "application/pdf"), + java.util.Map.entry("txt", "text/plain;charset=UTF-8"), + java.util.Map.entry("doc", "application/msword"), + java.util.Map.entry("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + java.util.Map.entry("xls", "application/vnd.ms-excel"), + java.util.Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + java.util.Map.entry("ppt", "application/vnd.ms-powerpoint"), + java.util.Map.entry("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), + java.util.Map.entry("mp4", "video/mp4") + ); + + /** + * 扩展名是否在上传白名单内 + */ + public static boolean isAllowedExtension(String extension) { + if (extension == null || extension.isBlank()) { + return false; + } + for (String allowed : DEFAULT_ALLOWED_EXTENSION) { + if (allowed.equalsIgnoreCase(extension)) { + return true; + } + } + return false; + } + + /** + * 按扩展名确定存储时使用的 Content-Type,不信任客户端上报值,防止以 text/html 等类型上传后在浏览器中执行脚本 + */ + public static String getSafeContentType(String extension) { + if (extension == null) { + return "application/octet-stream"; + } + return SAFE_CONTENT_TYPES.getOrDefault(extension.toLowerCase(), "application/octet-stream"); + } + } diff --git a/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java b/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java index 9a572b56..e1354dbf 100644 --- a/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java +++ b/backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java @@ -41,7 +41,7 @@ public class MybatisExceptionHandler { return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认"); } log.error("请求地址'{}', Mybatis系统异常", requestURI, e); - return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message); + return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "数据处理异常,请稍后重试或联系管理员"); } } diff --git a/backend/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java b/backend/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java index 412834cf..3b2aba52 100644 --- a/backend/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java +++ b/backend/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.sse.core.SseEmitterManager; -import org.dromara.common.sse.dto.SseMessageDto; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.MediaType; @@ -14,7 +13,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.List; /** * SSE 控制器 @@ -51,32 +49,6 @@ public class SseController implements DisposableBean { return R.ok(); } - /** - * 向特定用户发送消息 - * - * @param userId 目标用户的 ID - * @param msg 要发送的消息内容 - */ - @GetMapping(value = "${sse.path}/send") - public R send(Long userId, String msg) { - SseMessageDto dto = new SseMessageDto(); - dto.setUserIds(List.of(userId)); - dto.setMessage(msg); - sseEmitterManager.publishMessage(dto); - return R.ok(); - } - - /** - * 向所有用户发送消息 - * - * @param msg 要发送的消息内容 - */ - @GetMapping(value = "${sse.path}/sendAll") - public R send(String msg) { - sseEmitterManager.publishAll(msg); - return R.ok(); - } - /** * 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误 */ diff --git a/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java b/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java index 1371913b..cdfe1867 100644 --- a/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java +++ b/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java @@ -1,5 +1,7 @@ package org.dromara.common.web.config; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; import org.dromara.common.web.handler.GlobalExceptionHandler; import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -33,11 +35,11 @@ public class ResourcesConfig implements WebMvcConfigurer { * 跨域配置 */ @Bean - public CorsFilter corsFilter() { + public CorsFilter corsFilter(@Value("${security.cors-allowed-origins:http://localhost:[*],http://127.0.0.1:[*]}") List allowedOrigins) { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); - // 设置访问源地址 - config.addAllowedOriginPattern("*"); + // 设置访问源地址(仅允许配置的本校域名,禁止回显任意 Origin) + allowedOrigins.forEach(config::addAllowedOriginPattern); // 设置访问源请求头 config.addAllowedHeader("*"); // 设置访问源请求方法 diff --git a/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java b/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java index 28aacbcf..616ae22a 100644 --- a/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java +++ b/backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java @@ -37,6 +37,30 @@ import java.io.IOException; @RestControllerAdvice public class GlobalExceptionHandler { + private static final String DEFAULT_ERROR_MESSAGE = "系统繁忙,请稍后重试或联系管理员"; + + private static final String[] TECHNICAL_MARKERS = { + "###", "sql", "jdbc", "exception", "java.", "javax.", "jakarta.", "org.", "com.", "mapper", + "select ", "insert ", "update ", "delete ", " from ", " where ", "table", "column", "syntax", + "\tat ", "nested", "cause:", "null" + }; + + /** + * 仅返回业务提示类异常信息,包含 SQL、类名、堆栈等技术细节的信息统一替换为通用提示,避免泄露系统内部结构 + */ + public static String safeMessage(String message) { + if (message == null || message.isBlank() || message.length() > 300) { + return DEFAULT_ERROR_MESSAGE; + } + String lower = message.toLowerCase(); + for (String marker : TECHNICAL_MARKERS) { + if (lower.contains(marker)) { + return DEFAULT_ERROR_MESSAGE; + } + } + return message; + } + /** * 请求方式不支持 */ @@ -55,7 +79,8 @@ public class GlobalExceptionHandler { public R handleServiceException(ServiceException e, HttpServletRequest request) { log.error(e.getMessage()); Integer code = e.getCode(); - return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage()); + String message = safeMessage(e.getMessage()); + return ObjectUtil.isNotNull(code) ? R.fail(code, message) : R.fail(message); } /** @@ -76,7 +101,7 @@ public class GlobalExceptionHandler { public R handleServletException(ServletException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求地址'{}',发生未知异常.", requestURI, e); - return R.fail(e.getMessage()); + return R.fail(DEFAULT_ERROR_MESSAGE); } /** @@ -85,7 +110,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(BaseException.class) public R handleBaseException(BaseException e, HttpServletRequest request) { log.error(e.getMessage()); - return R.fail(e.getMessage()); + return R.fail(safeMessage(e.getMessage())); } /** @@ -105,7 +130,7 @@ public class GlobalExceptionHandler { public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI); - return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue())); + return R.fail(String.format("请求参数类型不匹配,参数[%s]格式错误", e.getName())); } /** @@ -139,7 +164,7 @@ public class GlobalExceptionHandler { public R handleRuntimeException(RuntimeException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求地址'{}',发生未知异常.", requestURI, e); - return R.fail(e.getMessage()); + return R.fail(safeMessage(e.getMessage())); } /** @@ -149,7 +174,7 @@ public class GlobalExceptionHandler { public R handleException(Exception e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求地址'{}',发生系统异常.", requestURI, e); - return R.fail(e.getMessage()); + return R.fail(DEFAULT_ERROR_MESSAGE); } /** @@ -190,7 +215,7 @@ public class GlobalExceptionHandler { public R handleJsonParseException(JsonParseException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求地址'{}' 发生 JSON 解析异常: {}", requestURI, e.getMessage()); - return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求数据格式错误(JSON 解析失败):" + e.getMessage()); + return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求数据格式错误(JSON 解析失败)"); } /** @@ -199,7 +224,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(HttpMessageNotReadableException.class) public R handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) { log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage()); - return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误:" + e.getMostSpecificCause().getMessage()); + return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误"); } } diff --git a/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/controller/TeacherAssessmentController.java b/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/controller/TeacherAssessmentController.java index c8b90aeb..acccb3f5 100644 --- a/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/controller/TeacherAssessmentController.java +++ b/backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/controller/TeacherAssessmentController.java @@ -1,5 +1,6 @@ package org.dromara.bzrkh.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.math.BigDecimal; import java.util.List; @@ -79,7 +80,7 @@ public class TeacherAssessmentController extends BaseController { return toAjax(result); } catch (RuntimeException e) { // 返回具体的错误信息 - return R.fail(e.getMessage()); + return R.fail(GlobalExceptionHandler.safeMessage(e.getMessage())); } catch (Exception e) { // 记录详细的异常信息 log.error("提交考核异常", e); @@ -135,7 +136,7 @@ public class TeacherAssessmentController extends BaseController { return toAjax(result); } catch (RuntimeException e) { // 返回具体的错误信息 - return R.fail(e.getMessage()); + return R.fail(GlobalExceptionHandler.safeMessage(e.getMessage())); } catch (Exception e) { // 记录详细的异常信息 log.error("提交考核评分数据异常", e); @@ -143,16 +144,6 @@ public class TeacherAssessmentController extends BaseController { } } - /** - * 测试人数系数计算(临时调试接口) - * 用于验证特定人数的系数计算是否正确 - */ - @GetMapping("/testCoefficient") - public R testStudentCountCoefficient(@RequestParam("studentCount") int studentCount) { - BigDecimal coefficient = teacherAssessmentService.testStudentCountCoefficient(studentCount); - return R.ok("学生人数: " + studentCount + ", 计算系数: " + coefficient); - } - /** * 查询班级学生违纪记录和奖励记录 */ diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwApprovalRecordController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwApprovalRecordController.java index fd9da7a9..35bd73e2 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwApprovalRecordController.java +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwApprovalRecordController.java @@ -1,5 +1,6 @@ package org.dromara.cw.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.domain.R; @@ -298,7 +299,7 @@ public class CwApprovalRecordController extends BaseController { return R.ok(); } catch (Exception e) { log.error("❌ 分管领导审批失败:applicationId={}, error={}", applicationId, e.getMessage(), e); - return R.fail("审批失败:" + e.getMessage()); + return R.fail("审批失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -330,7 +331,7 @@ public class CwApprovalRecordController extends BaseController { return R.ok(); } catch (Exception e) { log.error("❌ 财务审批失败:applicationId={}, error={}", applicationId, e.getMessage(), e); - return R.fail("审批失败:" + e.getMessage()); + return R.fail("审批失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqPmRepairAttachmentController.java b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqPmRepairAttachmentController.java index f7a155b4..4e978e16 100644 --- a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqPmRepairAttachmentController.java +++ b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqPmRepairAttachmentController.java @@ -1,5 +1,7 @@ package org.dromara.hq.controller; +import org.dromara.common.core.utils.file.FileUtils; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; @@ -120,7 +122,7 @@ public class HqPmRepairAttachmentController extends BaseController { HqPmRepairAttachmentVo result = attachmentService.uploadFile(orderId, uploadStage, file, description); return R.ok(result); } catch (Exception e) { - return R.fail("上传失败: " + e.getMessage()); + return R.fail("上传失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -145,7 +147,7 @@ public class HqPmRepairAttachmentController extends BaseController { descriptions); return R.ok(results); } catch (Exception e) { - return R.fail("批量上传失败: " + e.getMessage()); + return R.fail("批量上传失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -172,12 +174,16 @@ public class HqPmRepairAttachmentController extends BaseController { try { // 设置响应头 + String filePath = attachment.getFilePath(); + if (filePath == null || !(filePath.startsWith("https://") || filePath.startsWith("http://"))) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } response.setContentType("application/octet-stream"); - response.setHeader("Content-Disposition", "attachment; filename=\"" + attachment.getFileName() + "\""); + FileUtils.setAttachmentResponseHeader(response, attachment.getFileName()); - // 这里需要根据具体的文件存储方式来实现下载逻辑 // 如果使用OSS,可以直接重定向到OSS的URL - response.sendRedirect(attachment.getFilePath()); + response.sendRedirect(filePath); } catch (Exception e) { log.error("下载文件失败: {}", e.getMessage(), e); } diff --git a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqSatSurveyController.java b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqSatSurveyController.java index 1f556cf2..73d19f7e 100644 --- a/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqSatSurveyController.java +++ b/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqSatSurveyController.java @@ -1,5 +1,6 @@ package org.dromara.hq.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import lombok.RequiredArgsConstructor; @@ -204,7 +205,7 @@ public class HqSatSurveyController extends BaseController { return R.ok(result); } catch (Exception e) { log.error("提交问卷答案失败", e); - return R.fail("提交失败:" + e.getMessage()); + return R.fail("提交失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -229,7 +230,7 @@ public class HqSatSurveyController extends BaseController { return R.ok(result); } catch (Exception e) { log.error("保存问卷答案草稿失败", e); - return R.fail("保存草稿失败:" + e.getMessage()); + return R.fail("保存草稿失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -366,7 +367,7 @@ public class HqSatSurveyController extends BaseController { if (e.getMessage() != null && e.getMessage().contains("未找到有效的答题记录")) { return R.fail(404, "未找到有效的答题记录"); } - return R.fail("获取答题记录失败:" + e.getMessage()); + return R.fail("获取答题记录失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtScholarshipApplicationController.java b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtScholarshipApplicationController.java index 6a12e342..c30f699a 100644 --- a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtScholarshipApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtScholarshipApplicationController.java @@ -1,6 +1,8 @@ package org.dromara.jcbt.controller; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; +import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.core.domain.R; import org.dromara.common.core.validate.AddGroup; import org.dromara.common.core.validate.EditGroup; @@ -145,6 +147,9 @@ public class JcbtScholarshipApplicationController extends BaseController { @Operation(summary = "查询我的申请记录") public R> getMyApplications(@Parameter(description = "申请人ID") @RequestParam("applicantId") Long applicantId, @Parameter(description = "批次ID") @RequestParam(value = "batchId", required = false) Long batchId) { + if (!StpUtil.hasPermission("jcbt:scholarshipApplication:list")) { + applicantId = LoginHelper.getUserId(); + } return R.ok(applicationService.queryApplicationsByApplicant(applicantId, batchId)); } @@ -335,83 +340,4 @@ public class JcbtScholarshipApplicationController extends BaseController { return R.ok(applicationService.confirmClassSelection(teacherId, classId)); } - /** - * 测试接口:获取奖学金类型数据结构 - */ - @SaCheckPermission("jcbt:scholarshipApplication:scholarshipTypes") - @GetMapping("/test/scholarshipTypesStructure") - @Operation(summary = "测试接口:获取奖学金类型数据结构") - public R> testScholarshipTypesStructure(@Parameter(description = "批次ID") @RequestParam("batchId") Long batchId, - @Parameter(description = "班级ID") @RequestParam(value = "classId", required = false) Long classId) { - Map result = new HashMap<>(); - try { - if (classId != null) { - result.put("withQuota", applicationService.getScholarshipTypesWithQuota(batchId, classId)); - } - result.put("withoutQuota", applicationService.getScholarshipTypesByBatch(batchId)); - result.put("batchId", batchId); - result.put("classId", classId); - result.put("timestamp", System.currentTimeMillis()); - } catch (Exception e) { - result.put("error", e.getMessage()); - } - return R.ok(result); - } - - /** - * 调试接口:详细SQL日志输出测试 - */ - @SaCheckPermission("jcbt:scholarshipApplication:scholarshipTypes") - @GetMapping("/debug/sqlLogs") - @Operation(summary = "调试接口:详细SQL日志输出测试") - public R> debugSqlLogs(@Parameter(description = "批次ID") @RequestParam("batchId") Long batchId, - @Parameter(description = "班级ID") @RequestParam("classId") Long classId, - @Parameter(description = "奖学金类型ID(可选,测试单个类型)") @RequestParam(value = "scholarshipTypeId", required = false) Long scholarshipTypeId) { - Map result = new HashMap<>(); - - try { - log.info("🚀 === SQL调试接口开始 ==="); - log.info("📥 接收参数: batchId={}, classId={}, scholarshipTypeId={}", batchId, classId, scholarshipTypeId); - - result.put("requestParams", Map.of( - "batchId", batchId, - "classId", classId, - "scholarshipTypeId", scholarshipTypeId - )); - - // 如果指定了奖学金类型ID,则只测试该类型 - if (scholarshipTypeId != null) { - log.info("🎯 单个奖学金类型测试模式"); - - // 测试班级名额查询 - log.info("📊 开始测试班级名额查询"); - var classQuota = applicationService.testClassQuotaQuery(classId, scholarshipTypeId, batchId); - result.put("classQuotaTest", classQuota); - - // 测试全校名额查询 - log.info("🏫 开始测试全校名额查询"); - var schoolQuota = applicationService.testSchoolQuotaQuery(batchId, scholarshipTypeId); - result.put("schoolQuotaTest", schoolQuota); - - } else { - log.info("📋 完整流程测试模式"); - // 完整流程测试 - var quotaResult = applicationService.getScholarshipTypesWithQuota(batchId, classId); - result.put("fullProcessTest", quotaResult); - } - - result.put("status", "success"); - result.put("timestamp", System.currentTimeMillis()); - - log.info("✅ === SQL调试接口完成 ==="); - - } catch (Exception e) { - log.error("❌ === SQL调试接口异常 ===", e); - result.put("status", "error"); - result.put("error", e.getMessage()); - result.put("stackTrace", e.getStackTrace()); - } - - return R.ok(result); - } } diff --git a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationCollegeReviewController.java b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationCollegeReviewController.java index fa6ab68e..8f17f602 100644 --- a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationCollegeReviewController.java +++ b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationCollegeReviewController.java @@ -269,54 +269,6 @@ public class JcbtWorkStudyApplicationCollegeReviewController extends BaseControl // ========== 工具方法 ========== - /** - * 调试权限信息 - 查看当前用户的学院关联情况 - */ - @Operation(summary = "调试权限信息") - @SaCheckPermission("jcbt:workStudyApplication:collegeReview") - @GetMapping("/debug/permission") - public R> debugPermission() { - Map result = new HashMap<>(); - - try { - Long userId = LoginHelper.getUserId(); - result.put("userId", userId); - result.put("username", LoginHelper.getUsername()); - result.put("deptId", LoginHelper.getDeptId()); - result.put("deptName", LoginHelper.getDeptName()); - - try { - result.put("deptCategory", LoginHelper.getDeptCategory()); - } catch (Exception e) { - result.put("deptCategory", "获取失败: " + e.getMessage()); - } - - // 检查学生信息 - try { - XgStudentMsgVo studentInfo = xgStudentMsgService.queryByUserId(userId); - if (studentInfo != null) { - result.put("isStudent", true); - result.put("studentCollegeId", studentInfo.getCollegeId()); - result.put("studentCollegeName", studentInfo.getCollegeName()); - } else { - result.put("isStudent", false); - } - } catch (Exception e) { - result.put("studentInfoError", e.getMessage()); - } - - // 获取最终的学院ID - String collegeId = getCurrentUserCollegeId(); - result.put("finalCollegeId", collegeId); - result.put("hasCollegeAccess", collegeId != null); - - } catch (Exception e) { - result.put("error", e.getMessage()); - } - - return R.ok(result); - } - /** * 获取当前用户的学院ID * 通过查询学生信息或用户部门信息获取 diff --git a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationController.java b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationController.java index 45deb2e8..d8c03251 100644 --- a/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationController.java @@ -1,5 +1,6 @@ package org.dromara.jcbt.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -120,51 +121,6 @@ public class JcbtWorkStudyApplicationController extends BaseController { ExcelUtil.exportExcel(list, "勤工俭学申请信息", JcbtWorkStudyApplicationVo.class, response); } - /** - * 临时调试接口 - 直接查询数据库记录 - */ - @GetMapping("/debug/{id}") - public R> debugQuery(@PathVariable("id") Long id) { - log.info("调试查询接口,ID: {}", id); - - Map result = new HashMap<>(); - - try { - // 1. 查询实体类 - JcbtWorkStudyApplication entity = baseMapper.selectById(id); - result.put("entity", entity); - result.put("entityExists", entity != null); - if (entity != null) { - result.put("entityDetails", "ID=" + entity.getId() + ", studentName=" + entity.getStudentName() + ", deleted=" + entity.getDeleted()); - } - - // 2. 查询VO类 - JcbtWorkStudyApplicationVo vo = baseMapper.selectVoById(id); - result.put("vo", vo); - result.put("voExists", vo != null); - if (vo != null) { - result.put("voDetails", "ID=" + vo.getId() + ", studentName=" + vo.getStudentName()); - } - - // 3. 手动构建查询条件 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(JcbtWorkStudyApplication::getId, id); - JcbtWorkStudyApplication manualEntity = baseMapper.selectOne(wrapper); - result.put("manualEntity", manualEntity); - result.put("manualEntityExists", manualEntity != null); - - // 4. 查询数据库表中所有记录数量 - Long totalCount = baseMapper.selectCount(new LambdaQueryWrapper<>()); - result.put("totalRecords", totalCount); - - } catch (Exception e) { - log.error("调试查询发生异常", e); - result.put("exception", e.getMessage()); - } - - return R.ok(result); - } - // ========== 业务功能接口(4个) ========== /** @@ -342,7 +298,7 @@ public class JcbtWorkStudyApplicationController extends BaseController { return R.ok(studentInfo); } catch (Exception e) { log.error("获取当前用户学生信息失败", e); - return R.fail("获取学生信息失败:" + e.getMessage()); + return R.fail("获取学生信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsApplicationController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsApplicationController.java index 420fc14c..adee4dc3 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsApplicationController.java @@ -1,5 +1,6 @@ package org.dromara.rs.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; @@ -224,6 +225,7 @@ public class RsApplicationController extends BaseController { @Log(title = "提交职位申请", businessType = BusinessType.INSERT) @PostMapping("/apply") public R apply(@RequestBody RsApplication application) { + application.setApplicantId(LoginHelper.getUserId()); // 检查是否已经申请过 RsApplication existingApp = applicationService.checkApplied(application.getJobId(), application.getApplicantId()); if (existingApp != null) { @@ -241,7 +243,8 @@ public class RsApplicationController extends BaseController { */ @GetMapping("/my/{applicantId}") public TableDataInfo myApplications(@PathVariable("applicantId") Long applicantId, PageQuery pageQuery) { - List list = applicationService.selectApplicationsByApplicant(applicantId); + // 路径参数仅为兼容旧前端保留,只返回当前登录人的申请 + List list = applicationService.selectApplicationsByApplicant(LoginHelper.getUserId()); return TableDataInfo.build(list); } @@ -298,10 +301,7 @@ public class RsApplicationController extends BaseController { @PostMapping("/submit") public R> submitApplication(@RequestBody RsApplication application) { try { - // 设置默认状态 - if (StringUtils.isEmpty(application.getStatus())) { - application.setStatus("pending"); - } + application.setStatus("pending"); // 设置申请时间 application.setApplyTime(new java.util.Date()); @@ -331,7 +331,7 @@ public class RsApplicationController extends BaseController { return R.fail("申请提交失败"); } } catch (Exception e) { - return R.fail("申请提交失败:" + e.getMessage()); + return R.fail("申请提交失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsInitController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsInitController.java index 9e5d1965..f0243f9e 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsInitController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsInitController.java @@ -1,5 +1,6 @@ package org.dromara.rs.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import org.dromara.common.core.domain.R; import org.dromara.common.log.annotation.Log; import org.dromara.common.log.enums.BusinessType; @@ -85,7 +86,7 @@ public class RsInitController extends BaseController { return R.ok("菜单初始化成功"); } catch (Exception e) { e.printStackTrace(); - return R.fail("菜单初始化失败: " + e.getMessage()); + return R.fail("菜单初始化失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -113,7 +114,7 @@ public class RsInitController extends BaseController { return R.ok("菜单清除成功"); } catch (Exception e) { e.printStackTrace(); - return R.fail("菜单清除失败: " + e.getMessage()); + return R.fail("菜单清除失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -157,7 +158,7 @@ public class RsInitController extends BaseController { return R.ok("菜单更新成功"); } catch (Exception e) { e.printStackTrace(); - return R.fail("菜单更新失败: " + e.getMessage()); + return R.fail("菜单更新失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java index 854293e7..2842b712 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java @@ -1,5 +1,6 @@ package org.dromara.rs.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -76,7 +77,7 @@ public class RsOverseasApplicationAttachmentController extends BaseController { return R.ok("上传成功", vo); } catch (Exception e) { log.error("上传附件失败", e); - return R.fail("上传失败:" + e.getMessage()); + return R.fail("上传失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java index 9ec78ad4..e973344e 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java @@ -1,5 +1,6 @@ package org.dromara.rs.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; @@ -392,7 +393,7 @@ public class RsOverseasApplicationController extends BaseController { int fixedCount = applicationService.fixApprovedApplicationsWithoutRecords(); return R.ok("修复完成,共修复 " + fixedCount + " 条记录"); } catch (Exception e) { - return R.fail("修复失败:" + e.getMessage()); + return R.fail("修复失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -545,7 +546,7 @@ public class RsOverseasApplicationController extends BaseController { } catch (Exception e) { e.printStackTrace(); - return R.fail("系统异常:" + e.getMessage()); + return R.fail("系统异常:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -612,7 +613,7 @@ public class RsOverseasApplicationController extends BaseController { } catch (Exception e) { e.printStackTrace(); - return R.fail("系统异常:" + e.getMessage()); + return R.fail("系统异常:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java index 4352d3af..d2eb35c1 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java @@ -1,5 +1,6 @@ package org.dromara.rs.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; @@ -78,40 +79,6 @@ public class RsOverseasApprovalController extends BaseController { return approvalService.queryCombinedPageList(query); } - /** - * 调试接口:查看当前用户的权限过滤情况 - */ - @SaCheckPermission("rs:overseas:list") - @GetMapping("/debug/permission") - public R> debugPermission(RsOverseasApprovalQuery query) { - Map debugInfo = new HashMap<>(); - - // 当前用户信息 - debugInfo.put("currentUserId", LoginHelper.getUserId()); - debugInfo.put("currentUsername", LoginHelper.getUsername()); - debugInfo.put("currentDeptId", LoginHelper.getDeptId()); - debugInfo.put("isSuperAdmin", LoginHelper.isSuperAdmin()); - - // 角色信息 - debugInfo.put("hasRsApplicant", StpUtil.hasRole("rs-Applicant")); - debugInfo.put("hasRsAdmin", StpUtil.hasRole("rs-admin")); - debugInfo.put("hasApprove1Permission", StpUtil.hasPermission("rs:overseas:approve1")); - debugInfo.put("hasDeptHeadRole", StpUtil.hasRole("dept_head_role")); - debugInfo.put("hasOldApprovePermission", StpUtil.hasPermission("overseas:application:dept_head:approve")); - - // 查询参数(应用权限过滤前) - debugInfo.put("originalQuery", query.toString()); - - // 应用权限过滤 - TableDataInfo result = approvalService.queryCombinedPageList(query); - - // 查询参数(应用权限过滤后) - debugInfo.put("filteredQuery", query.toString()); - debugInfo.put("resultCount", result.getTotal()); - - return R.ok(debugInfo); - } - /** * 根据申请ID查询审批记录列表 */ @@ -187,7 +154,7 @@ public class RsOverseasApprovalController extends BaseController { return R.ok("审批记录数据修复成功!"); } catch (Exception e) { log.error("修复审批记录数据失败", e); - return R.fail("修复失败:" + e.getMessage()); + return R.fail("修复失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java index 89c28045..b5881d9d 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java @@ -1,5 +1,6 @@ package org.dromara.rs.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; @@ -254,7 +255,7 @@ public class RsOverseasRecordController extends BaseController { return R.ok("测试数据初始化成功,请刷新页面查看"); } catch (Exception e) { - return R.fail("测试数据初始化失败: " + e.getMessage()); + return R.fail("测试数据初始化失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } 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 9b494139..11b53e28 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 @@ -1,5 +1,6 @@ package org.dromara.rs.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import org.dromara.common.core.domain.R; @@ -222,7 +223,7 @@ public class RsTrainingApplicationController extends BaseController { } catch (Exception e) { e.printStackTrace(); log.error("添加培训申请失败: " + e.getMessage(), e); - return R.fail("添加培训申请失败: " + e.getMessage()); + return R.fail("添加培训申请失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -322,7 +323,7 @@ public class RsTrainingApplicationController extends BaseController { } catch (Exception e) { e.printStackTrace(); log.error("添加培训申请失败: " + e.getMessage(), e); - return R.fail("添加培训申请失败: " + e.getMessage()); + return R.fail("添加培训申请失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -410,7 +411,7 @@ public class RsTrainingApplicationController extends BaseController { return result ? R.ok() : R.fail("审批失败"); } catch (Exception e) { log.error("审批培训申请失败: {}", e.getMessage(), e); - return R.fail("审批失败: " + e.getMessage()); + return R.fail("审批失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -441,7 +442,7 @@ public class RsTrainingApplicationController extends BaseController { return R.ok(result); } catch (Exception e) { log.error("查询待审批申请失败: {}", e.getMessage(), e); - return R.fail("查询失败: " + e.getMessage()); + return R.fail("查询失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -487,7 +488,7 @@ public class RsTrainingApplicationController extends BaseController { } } catch (NumberFormatException e) { log.error("解析用户ID失败,原始字符串: {}, 错误: {}", userIds, e.getMessage()); - return R.fail("用户ID格式错误: " + e.getMessage()); + return R.fail("用户ID格式错误: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } if (CollUtil.isEmpty(userIdList)) { diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java index 0c6c56b6..97b51b0b 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java @@ -52,33 +52,6 @@ public class RsTrainingRecordController extends BaseController { return trainingRecordService.selectPageRsTrainingRecordList(record, pageQuery); } - /** - * 测试培训地点搜索功能 - */ - @SaCheckPermission("rs:trainingrecord:list") - @GetMapping("/test-location") - public R> testLocationSearch(@RequestParam("location") String location) { - log.info("测试培训地点搜索 - location参数: {}", location); - - // 创建查询对象 - RsTrainingRecord record = new RsTrainingRecord(); - record.setLocation(location); - - log.info("创建的查询对象 - location: {}", record.getLocation()); - - // 直接调用Mapper方法 - List list = trainingRecordService.selectRsTrainingRecordList(record); - - log.info("查询结果数量: {}", list.size()); - if (!list.isEmpty()) { - log.info("前3条记录的location值:"); - list.stream().limit(3) - .forEach(r -> log.info(" - recordId: {}, location: {}", r.getRecordId(), r.getLocation())); - } - - return R.ok(list); - } - /** * 导出培训记录列表 */ diff --git a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java index b5813b53..9bad5e87 100644 --- a/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.file.MimeTypeUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.oss.core.OssClient; import org.dromara.common.oss.entity.UploadResult; @@ -86,7 +87,10 @@ public class RsOverseasApplicationAttachmentServiceImpl implements IRsOverseasAp // 获取文件信息 String originalFilename = file.getOriginalFilename(); String fileExtension = getFileExtension(originalFilename); - String contentType = file.getContentType(); + if (!MimeTypeUtils.isAllowedExtension(fileExtension)) { + throw new ServiceException("不允许的文件类型: " + fileExtension); + } + String contentType = MimeTypeUtils.getSafeContentType(fileExtension); // 生成文件名 String fileName = "overseas_attachment_" + applicationId + "_" + System.currentTimeMillis() diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityAttendanceController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityAttendanceController.java index 34fab2fc..43e61ef5 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityAttendanceController.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityAttendanceController.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.dromara.common.idempotent.annotation.RepeatSubmit; @@ -132,6 +133,11 @@ public class StglActivityAttendanceController extends BaseController { @RequestParam(value = "studentNo", required = false) String studentNo) { List list; + if (!StpUtil.hasPermission("stgl:activityRegister:approve") + && !StpUtil.hasPermission("stgl:activity:edit")) { + userId = LoginHelper.getUserId(); + studentNo = null; + } if (userId != null) { list = stglActivityAttendanceService.queryAllByActivityAndUser(activityId, userId); } else if (StringUtils.isNotBlank(studentNo)) { diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityRegistrationController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityRegistrationController.java index 83d2575e..7dfd7294 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityRegistrationController.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityRegistrationController.java @@ -1,6 +1,7 @@ package org.dromara.stgl.controller; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -123,7 +124,7 @@ public class StglActivityRegistrationController extends BaseController { */ @GetMapping("/checkRegistered") public R checkRegistered(@RequestParam("activityId") Long activityId, @RequestParam(value = "userId", required = false) Long userId) { - Long currentUserId = userId != null ? userId : LoginHelper.getUserId(); + Long currentUserId = userId != null && (StpUtil.hasPermission("stgl:activityRegister:approve") || StpUtil.hasPermission("stgl:activity:edit")) ? userId : LoginHelper.getUserId(); return R.ok(stglActivityRegistrationService.checkUserRegistered(activityId, currentUserId)); } @@ -132,7 +133,7 @@ public class StglActivityRegistrationController extends BaseController { */ @GetMapping("/getUserRegistration") public R getUserRegistration(@RequestParam("activityId") Long activityId, @RequestParam(value = "userId", required = false) Long userId) { - Long currentUserId = userId != null ? userId : LoginHelper.getUserId(); + Long currentUserId = userId != null && (StpUtil.hasPermission("stgl:activityRegister:approve") || StpUtil.hasPermission("stgl:activity:edit")) ? userId : LoginHelper.getUserId(); return R.ok(stglActivityRegistrationService.getUserRegistration(activityId, currentUserId)); } diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubController.java index ec16eb81..db0f6588 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubController.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubController.java @@ -192,27 +192,6 @@ public class StglClubController extends BaseController { return toAjax(stglClubService.transferClub(id, newPresidentUserId)); } - /** - * 临时测试接口:验证副社长数据映射 - */ - @Operation(summary = "测试副社长数据映射") - @SaCheckPermission("stgl:club:query") - @GetMapping("/testVicePresidentData/{id}") - public R testVicePresidentData(@PathVariable("id") Long id) { - StglClubVo club = stglClubService.queryById(id); - if (club != null) { - // 输出调试信息到日志 - System.out.println("=== 副社长数据测试 ==="); - System.out.println("副社长姓名: " + club.getVicePresidentName()); - System.out.println("副社长院系: " + club.getVicePresidentDept()); - System.out.println("副社长专业: " + club.getVicePresidentMajor()); - System.out.println("副社长班级: " + club.getVicePresidentClass()); - System.out.println("副社长电话: " + club.getVicePresidentPhone()); - System.out.println("====================="); - } - return R.ok(club); - } - /** * 启用/禁用社团 */ diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java index 654e0875..a95967ab 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java @@ -1,5 +1,6 @@ package org.dromara.stgl.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -53,7 +54,7 @@ public class StglClubPermissionController extends BaseController { return R.ok(result); } catch (Exception e) { log.error("检查社团活动权限失败,clubId: {}", clubId, e); - return R.fail("检查权限失败: " + e.getMessage()); + return R.fail("检查权限失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -85,7 +86,7 @@ public class StglClubPermissionController extends BaseController { return R.ok(results); } catch (Exception e) { log.error("批量检查社团权限失败,clubIds: {}", clubIds, e); - return R.fail("批量检查权限失败: " + e.getMessage()); + return R.fail("批量检查权限失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -109,7 +110,7 @@ public class StglClubPermissionController extends BaseController { return R.ok(inWindow); } catch (Exception e) { log.error("检查年审窗口期失败,clubId: {}", clubId, e); - return R.fail("检查失败: " + e.getMessage()); + return R.fail("检查失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -133,7 +134,7 @@ public class StglClubPermissionController extends BaseController { return R.ok(days); } catch (Exception e) { log.error("获取距离年审天数失败,clubId: {}", clubId, e); - return R.fail("获取失败: " + e.getMessage()); + return R.fail("获取失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -157,7 +158,7 @@ public class StglClubPermissionController extends BaseController { return R.ok(hasSubmitted); } catch (Exception e) { log.error("检查年审提交状态失败,clubId: {}", clubId, e); - return R.fail("检查失败: " + e.getMessage()); + return R.fail("检查失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -194,7 +195,7 @@ public class StglClubPermissionController extends BaseController { return R.ok(details); } catch (Exception e) { log.error("获取社团权限详情失败,clubId: {}", clubId, e); - return R.fail("获取详情失败: " + e.getMessage()); + return R.fail("获取详情失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglVolunteerTeamController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglVolunteerTeamController.java index a99201df..8e206e4a 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglVolunteerTeamController.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglVolunteerTeamController.java @@ -1,5 +1,6 @@ package org.dromara.stgl.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; @@ -257,7 +258,7 @@ public class StglVolunteerTeamController extends BaseController { Integer fixedCount = stglVolunteerTeamService.fixExistingTeamLeaderRecords(); return R.ok("修复完成,共补充了 " + fixedCount + " 条队长成员记录"); } catch (Exception e) { - return R.fail("修复失败:" + e.getMessage()); + return R.fail("修复失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java index 4984c37f..95e15c06 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java @@ -683,54 +683,32 @@ public class StglActivityServiceImpl extends ServiceImpl roleKeys = LoginHelper.getLoginUser().getRolePermission(); - - // 如果是stgl_president角色且不是管理员,只能查看自己创建的活动 - if (roleKeys.contains("stgl_president") && - !roleKeys.contains("admin") && - !roleKeys.contains("common")) { - - // 设置数据权限过滤条件 - 使用params.dataScope方式,与XML中的${activity.params.dataScope}对应 - if (bo.getParams() == null) { - bo.setParams(new HashMap<>()); - } - bo.getParams().put("dataScope", "AND sa.create_by = " + currentUserId); - log.info("应用数据权限:用户 {} 为社长角色,只能查看自己创建的活动", currentUserId); - } - - // 🔥 修正:如果是stgl_instructor角色且不是管理员,只能查看自己作为指导老师的活动 - if (roleKeys.contains("stgl_instructor") && - !roleKeys.contains("admin") && - !roleKeys.contains("common")) { - - // 获取用户作为指导老师的社团ID列表 - List userAdvisorClubIds = getUserAdvisorClubIds(currentUserId); - - // 获取用户作为指导老师的志愿者队伍ID列表 - List userAdvisorTeamIds = getUserAdvisorTeamIds(currentUserId); - - // 获取用户作为指导老师的活动ID列表 - List userAdvisorActivityIds = getUserAdvisorActivityIds(currentUserId); - - // 构建数据权限过滤条件 - String dataScope = buildInstructorDataScope(userAdvisorClubIds, userAdvisorTeamIds, userAdvisorActivityIds); - - if (bo.getParams() == null) { - bo.setParams(new HashMap<>()); - } - bo.getParams().put("dataScope", dataScope); - log.info("应用数据权限:用户 {} 为指导老师角色,数据权限条件: {}", currentUserId, dataScope); - } - - } catch (Exception e) { - log.error("应用数据权限控制失败:{}", e.getMessage(), e); + if (bo.getParams() == null) { + bo.setParams(new HashMap<>()); + } + // 数据权限参数只能由服务端写入,丢弃请求中同名参数 + bo.getParams().keySet().removeIf(key -> key.startsWith("ds") || "dataScope".equals(key)); + + Long currentUserId = LoginHelper.getUserId(); + if (currentUserId == null) { + return; + } + Set roleKeys = LoginHelper.getLoginUser().getRolePermission(); + boolean manager = roleKeys.contains("admin") || roleKeys.contains("common"); + + // 社长:只能查看自己创建的活动 + if (roleKeys.contains("stgl_president") && !manager) { + bo.getParams().put("dsCreateBy", currentUserId); + log.info("应用数据权限:用户 {} 为社长角色,只能查看自己创建的活动", currentUserId); + } + + // 指导老师:只能查看自己指导的社团/志愿者队伍/活动 + if (roleKeys.contains("stgl_instructor") && !manager) { + bo.getParams().put("dsInstructor", Boolean.TRUE); + bo.getParams().put("dsClubIds", getUserAdvisorClubIds(currentUserId)); + bo.getParams().put("dsTeamIds", getUserAdvisorTeamIds(currentUserId)); + bo.getParams().put("dsActivityIds", getUserAdvisorActivityIds(currentUserId)); + log.info("应用数据权限:用户 {} 为指导老师角色", currentUserId); } } @@ -921,50 +899,6 @@ public class StglActivityServiceImpl extends ServiceImpl userAdvisorClubIds, List userAdvisorTeamIds, List userAdvisorActivityIds) { - List conditions = new ArrayList<>(); - - // 1. 是活动关联社团的指导老师 - if (!userAdvisorClubIds.isEmpty()) { - String clubIdsStr = userAdvisorClubIds.stream() - .map(String::valueOf) - .collect(Collectors.joining(",")); - conditions.add("(sa.related_type = 1 AND sa.related_club_id IN (" + clubIdsStr + "))"); - } - - // 2. 是活动关联志愿者队伍的指导老师 - if (!userAdvisorTeamIds.isEmpty()) { - String teamIdsStr = userAdvisorTeamIds.stream() - .map(String::valueOf) - .collect(Collectors.joining(",")); - conditions.add("(sa.related_type = 2 AND sa.related_club_id IN (" + teamIdsStr + "))"); - } - - // 3. 是活动本身的指导老师 - if (!userAdvisorActivityIds.isEmpty()) { - String activityIdsStr = userAdvisorActivityIds.stream() - .map(String::valueOf) - .collect(Collectors.joining(",")); - conditions.add("sa.id IN (" + activityIdsStr + ")"); - } - - // 如果没有任何权限,返回一个永远为false的条件 - if (conditions.isEmpty()) { - return "AND 1 = 0"; - } - - // 组合所有条件,使用OR连接 - return "AND (" + String.join(" OR ", conditions) + ")"; - } - @Override public List getActivityRatings(Long activityId) { if (activityId == null) { diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml index 58b5eb1f..2415c785 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml @@ -207,7 +207,27 @@ AND sa.related_club_id = #{relatedClubId} - ${params.dataScope} + + + AND sa.create_by = #{params.dsCreateBy} + + + AND (1 = 0 + + OR (sa.related_type = 1 AND sa.related_club_id IN + #{dsId}) + + + OR (sa.related_type = 2 AND sa.related_club_id IN + #{dsId}) + + + OR sa.id IN + #{dsId} + + ) + + ORDER BY sa.create_time DESC @@ -366,7 +386,27 @@ ) - ${activity.params.dataScope} + + + AND sa.create_by = #{activity.params.dsCreateBy} + + + AND (1 = 0 + + OR (sa.related_type = 1 AND sa.related_club_id IN + #{dsId}) + + + OR (sa.related_type = 2 AND sa.related_club_id IN + #{dsId}) + + + OR sa.id IN + #{dsId} + + ) + + ORDER BY sa.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysBucketPasswordController.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysBucketPasswordController.java index 426665cb..5e6609fa 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysBucketPasswordController.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysBucketPasswordController.java @@ -1,5 +1,6 @@ package org.dromara.system.controller.system; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import org.dromara.common.core.domain.R; import org.dromara.common.web.core.BaseController; @@ -46,7 +47,7 @@ public class SysBucketPasswordController extends BaseController { return R.fail("密码重置失败"); } } catch (Exception e) { - return R.fail("密码重置失败:" + e.getMessage()); + return R.fail("密码重置失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -76,7 +77,7 @@ public class SysBucketPasswordController extends BaseController { return R.fail("bucket密码配置不存在"); } } catch (Exception e) { - return R.fail("获取bucket密码信息失败:" + e.getMessage()); + return R.fail("获取bucket密码信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -97,7 +98,7 @@ public class SysBucketPasswordController extends BaseController { var records = bucketPasswordService.getBucketAccessRecords(bucketName, limit); return R.ok(records); } catch (Exception e) { - return R.fail("获取bucket访问记录失败:" + e.getMessage()); + return R.fail("获取bucket访问记录失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -119,7 +120,7 @@ public class SysBucketPasswordController extends BaseController { bucketName, password, "127.0.0.1", "API-Test"); return R.ok(isValid); } catch (Exception e) { - return R.fail("密码验证失败:" + e.getMessage()); + return R.fail("密码验证失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysConfigController.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysConfigController.java index 1a0ab9ff..a01dce9a 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysConfigController.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysConfigController.java @@ -1,6 +1,8 @@ package org.dromara.system.controller.system; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.stp.StpUtil; import org.dromara.common.core.domain.R; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.common.log.annotation.Log; @@ -29,6 +31,15 @@ import java.util.List; @RequestMapping("/system/config") public class SysConfigController extends BaseController { + /** + * 普通登录用户可读取的参数键,其余参数(如初始密码)需具备参数查询权限 + */ + private static final java.util.Set PUBLIC_CONFIG_KEYS = java.util.Set.of( + "sys.oss.previewListResource", + "sys.account.registerUser", + "sys.account.captchaEnabled" + ); + private final ISysConfigService configService; /** @@ -69,6 +80,11 @@ public class SysConfigController extends BaseController { */ @GetMapping(value = "/configKey/{configKey}") public R getConfigKey(@PathVariable("configKey") String configKey) { + if (!PUBLIC_CONFIG_KEYS.contains(configKey) + && !StpUtil.hasPermission("system:config:query") + && !("sys.user.initPassword".equals(configKey) && (StpUtil.hasPermission("system:user:add") || StpUtil.hasPermission("system:user:resetPwd")))) { + throw new NotPermissionException("system:config:query"); + } return R.ok("操作成功", configService.selectConfigByKey(configKey)); } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysFileAccessController.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysFileAccessController.java index d005b50a..1438b646 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysFileAccessController.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysFileAccessController.java @@ -1,6 +1,7 @@ package org.dromara.system.controller.system; import cn.hutool.core.util.ObjectUtil; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.domain.R; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.oss.core.OssClient; @@ -24,10 +25,13 @@ import java.util.Map; * * @author Lion Li */ +@Slf4j @RestController @RequestMapping("/resource/file") public class SysFileAccessController extends BaseController { + private static final int MAX_EXPIRE_SECONDS = 3600; + private final ISysOssService ossService; private final ISysBucketAccessPasswordService bucketPasswordService; @@ -82,7 +86,7 @@ public class SysFileAccessController extends BaseController { // 生成预签名URL String presignedUrl = storage.getPrivateUrl( oss.getFileName(), - Duration.ofSeconds(expireSeconds) + Duration.ofSeconds(limitExpireSeconds(expireSeconds)) ); // 记录文件访问日志 @@ -91,7 +95,8 @@ public class SysFileAccessController extends BaseController { return R.ok(presignedUrl); } catch (Exception e) { - return R.fail("获取文件访问链接失败:" + e.getMessage()); + log.error("获取文件访问链接失败", e); + return R.fail("获取文件访问链接失败"); } } @@ -132,7 +137,7 @@ public class SysFileAccessController extends BaseController { OssClient storage = OssFactory.instance(oss.getService()); String presignedUrl = storage.getPrivateUrl( oss.getFileName(), - Duration.ofSeconds(expireSeconds) + Duration.ofSeconds(limitExpireSeconds(expireSeconds)) ); urlMap.put(ossId, presignedUrl); @@ -144,7 +149,8 @@ public class SysFileAccessController extends BaseController { return R.ok(urlMap); } catch (Exception e) { - return R.fail("批量获取文件访问链接失败:" + e.getMessage()); + log.error("批量获取文件访问链接失败", e); + return R.fail("批量获取文件访问链接失败"); } } @@ -189,7 +195,7 @@ public class SysFileAccessController extends BaseController { OssClient storage = OssFactory.instance(oss.getService()); String presignedUrl = storage.getPrivateUrl( oss.getFileName(), - Duration.ofSeconds(expireSeconds) + Duration.ofSeconds(limitExpireSeconds(expireSeconds)) ); // 记录下载日志 @@ -198,7 +204,8 @@ public class SysFileAccessController extends BaseController { return R.ok(presignedUrl); } catch (Exception e) { - return R.fail("获取文件下载链接失败:" + e.getMessage()); + log.error("获取文件下载链接失败", e); + return R.fail("获取文件下载链接失败"); } } @@ -243,7 +250,7 @@ public class SysFileAccessController extends BaseController { OssClient storage = OssFactory.instance(oss.getService()); String presignedUrl = storage.getPrivateUrl( oss.getFileName(), - Duration.ofSeconds(expireSeconds) + Duration.ofSeconds(limitExpireSeconds(expireSeconds)) ); // 记录预览日志 @@ -252,10 +259,18 @@ public class SysFileAccessController extends BaseController { return R.ok(presignedUrl); } catch (Exception e) { - return R.fail("获取文件预览链接失败:" + e.getMessage()); + log.error("获取文件预览链接失败", e); + return R.fail("获取文件预览链接失败"); } } + private static int limitExpireSeconds(Integer expireSeconds) { + if (expireSeconds == null || expireSeconds <= 0) { + return 300; + } + return Math.min(expireSeconds, MAX_EXPIRE_SECONDS); + } + /** * 验证bucket访问密码 * diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/wechat/SysWeChatController.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/wechat/SysWeChatController.java index 705fedca..73b3cc57 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/wechat/SysWeChatController.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/wechat/SysWeChatController.java @@ -1,5 +1,6 @@ package org.dromara.system.controller.wechat; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -46,7 +47,7 @@ public class SysWeChatController extends BaseController { return R.ok("获取配置成功", config); } catch (Exception e) { log.error("获取微信JS-SDK配置失败", e); - return R.fail("获取配置失败: " + e.getMessage()); + return R.fail("获取配置失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -67,7 +68,7 @@ public class SysWeChatController extends BaseController { return R.ok("上传成功", result); } catch (Exception e) { log.error("上传微信媒体文件失败", e); - return R.fail("上传失败: " + e.getMessage()); + return R.fail("上传失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -88,7 +89,7 @@ public class SysWeChatController extends BaseController { return R.ok("下载成功", result); } catch (Exception e) { log.error("下载微信媒体文件失败", e); - return R.fail("下载失败: " + e.getMessage()); + return R.fail("下载失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -109,7 +110,7 @@ public class SysWeChatController extends BaseController { return R.ok("获取用户信息成功", result); } catch (Exception e) { log.error("获取微信用户信息失败", e); - return R.fail("获取用户信息失败: " + e.getMessage()); + return R.fail("获取用户信息失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -130,7 +131,7 @@ public class SysWeChatController extends BaseController { return R.ok("创建支付订单成功", result); } catch (Exception e) { log.error("创建微信支付订单失败", e); - return R.fail("创建支付订单失败: " + e.getMessage()); + return R.fail("创建支付订单失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -151,7 +152,7 @@ public class SysWeChatController extends BaseController { return R.ok("查询支付状态成功", result); } catch (Exception e) { log.error("查询微信支付状态失败", e); - return R.fail("查询支付状态失败: " + e.getMessage()); + return R.fail("查询支付状态失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -172,7 +173,7 @@ public class SysWeChatController extends BaseController { return R.ok("下载文件到OSS成功", result); } catch (Exception e) { log.error("下载微信媒体文件到OSS失败", e); - return R.fail("下载文件到OSS失败: " + e.getMessage()); + return R.fail("下载文件到OSS失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java index 56eed402..cd0cf958 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java @@ -27,7 +27,10 @@ import org.dromara.common.oss.factory.OssFactory; import org.dromara.system.domain.SysOss; import org.dromara.system.domain.bo.SysOssBo; import org.dromara.system.domain.vo.SysOssVo; +import org.dromara.common.mybatis.helper.DataPermissionHelper; +import org.dromara.system.domain.SysUser; import org.dromara.system.mapper.SysOssMapper; +import org.dromara.system.mapper.SysUserMapper; import org.dromara.system.config.FileCacheConfig; import org.dromara.system.service.ISysOssService; import org.jetbrains.annotations.NotNull; @@ -66,7 +69,10 @@ import java.util.zip.ZipOutputStream; @Service public class SysOssServiceImpl implements ISysOssService, OssService { + private static final String STUDENT_ACCOUNT_TYPE = "2"; + private final SysOssMapper baseMapper; + private final SysUserMapper sysUserMapper; private final FileCacheConfig fileCacheConfig; // 🚀 LRU缓存实现 @@ -292,7 +298,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { OssClient storage = OssFactory.instance(); UploadResult uploadResult; try { - uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType()); + uploadResult = storage.uploadSuffix(file.getBytes(), suffix, MimeTypeUtils.getSafeContentType(ext)); } catch (IOException e) { throw new ServiceException(e.getMessage()); } @@ -907,6 +913,11 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return true; } + // 学生账号:不能访问其他学生上传的文件(身份证、贫困证明等),教职工上传的文件可访问 + if (isStudentAccount(userId)) { + return ObjectUtil.isNotNull(oss.getCreateBy()) && !isStudentAccount(oss.getCreateBy()); + } + // 5. 具备 OSS 管理/下载类权限的用户 if (StpUtil.hasPermission("system:oss:download") || StpUtil.hasPermission("system:oss:list") @@ -915,12 +926,18 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return true; } - // 6. 业务场景(请假/宿检等):审批人需查看申请人附件。 - // 全量列表已由 system:oss:list 约束;此处允许任意已登录用户按 ossId 访问单个文件。 - // 后续应按业务单据做归属校验以消除枚举 IDOR。 + // 6. 业务场景(请假/宿检等):教职工审批人需查看申请人附件 return true; } + private boolean isStudentAccount(Long userId) { + SysUser user = DataPermissionHelper.ignore(() -> sysUserMapper.selectOne( + Wrappers.lambdaQuery(SysUser.class) + .select(SysUser::getUserId, SysUser::getAccountType) + .eq(SysUser::getUserId, userId))); + return user != null && STUDENT_ACCOUNT_TYPE.equals(user.getAccountType()); + } + /** * 记录文件访问日志 * diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysWeChatServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysWeChatServiceImpl.java index eadbadce..81f7a382 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysWeChatServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysWeChatServiceImpl.java @@ -136,7 +136,7 @@ public class SysWeChatServiceImpl implements ISysWeChatService { "https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s", accessToken, mediaId); - log.info("微信多媒体下载URL: {}", downloadUrl); + log.info("微信多媒体下载,mediaId: {}", mediaId); // 4. 发起HTTP请求下载文件 HttpResponse response = HttpRequest.get(downloadUrl).execute(); @@ -314,7 +314,7 @@ public class SysWeChatServiceImpl implements ISysWeChatService { "https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s", accessToken, mediaId); - log.info("微信多媒体下载URL: {}", downloadUrl); + log.info("微信多媒体下载,mediaId: {}", mediaId); // 4. 发起HTTP请求下载文件 HttpResponse response = HttpRequest.get(downloadUrl).execute(); diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowPermissionConstant.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowPermissionConstant.java new file mode 100644 index 00000000..4875d3b6 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowPermissionConstant.java @@ -0,0 +1,28 @@ +package org.dromara.workflow.common.constant; + +/** + * 工作流管理类接口权限标识(菜单见 script/sql/update/update_20261026_workflow_permission.sql) + */ +public interface FlowPermissionConstant { + + /** + * 流程定义查看(含设计器读取) + */ + String DEFINITION_LIST = "workflow:definition:list"; + + /** + * 流程定义新增/修改/发布/导入/删除(含设计器保存) + */ + String DEFINITION_EDIT = "workflow:definition:edit"; + + /** + * 流程实例管理:全部实例查询、作废、激活挂起、变量查看、删除他人实例 + */ + String INSTANCE_MANAGE = "workflow:instance:manage"; + + /** + * 全部待办/已办查询与转派办理人 + */ + String TASK_MANAGE = "workflow:task:manage"; + +} diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/WarmFlowConfig.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/WarmFlowConfig.java index 08f1808d..3c245706 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/WarmFlowConfig.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/WarmFlowConfig.java @@ -1,16 +1,70 @@ package org.dromara.workflow.config; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.stp.StpUtil; +import lombok.extern.slf4j.Slf4j; +import org.dromara.warm.flow.core.condition.ConditionStrategy; +import org.dromara.warm.flow.core.listener.ListenerStrategy; +import org.dromara.warm.flow.core.strategy.ExpressionStrategy; +import org.dromara.warm.flow.core.utils.ExpressionUtil; +import org.dromara.warm.flow.core.variable.VariableStrategy; import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.common.constant.FlowPermissionConstant; +import org.dromara.workflow.config.expression.SafeExpressionStrategies; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; /** * warmFlow配置 * * @author may */ +@Slf4j @ConditionalOnEnable @Configuration -public class WarmFlowConfig { +public class WarmFlowConfig implements WebMvcConfigurer { + + private static final String WARM_FLOW_SPEL_PACKAGE = "org.dromara.warm.plugin.modes.sb.expression."; + + /** + * warm-flow 自带 SpEL 策略使用无限制的 StandardEvaluationContext,存在表达式注入执行系统命令的风险, + * 启动完成后移除并替换为受限实现 + */ + @EventListener(ApplicationReadyEvent.class) + public void replaceSpelStrategies() { + removeBuiltinSpel(ConditionStrategy.expressionStrategyList); + removeBuiltinSpel(ListenerStrategy.expressionStrategyList); + removeBuiltinSpel(VariableStrategy.expressionStrategyList); + ExpressionUtil.setExpression(new SafeExpressionStrategies.ConditionDefault()); + ExpressionUtil.setExpression(new SafeExpressionStrategies.ConditionSpel()); + ExpressionUtil.setExpression(new SafeExpressionStrategies.ListenerSpel()); + ExpressionUtil.setExpression(new SafeExpressionStrategies.VariableSpel()); + log.info("warm-flow SpEL 表达式策略已替换为受限实现"); + } + + private static void removeBuiltinSpel(List> strategies) { + strategies.removeIf(s -> s.getClass().getName().startsWith(WARM_FLOW_SPEL_PACKAGE)); + } + + /** + * warm-flow 设计器接口(/warm-flow/**)只做了登录校验,此处按权限收紧;流程图查看接口保持登录即可访问 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/warm-flow/save-json", "/warm-flow/form-content") + .check(() -> StpUtil.checkPermission(FlowPermissionConstant.DEFINITION_EDIT)); + SaRouter.match("/warm-flow/query-def/**", "/warm-flow/handler-*", "/warm-flow/published-form", + "/warm-flow/form-content/**") + .check(() -> StpUtil.checkPermission(FlowPermissionConstant.DEFINITION_LIST)); + }).isAnnotation(false)) + .addPathPatterns("/warm-flow/**"); + } } - diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeExpressionStrategies.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeExpressionStrategies.java new file mode 100644 index 00000000..54ff506f --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeExpressionStrategies.java @@ -0,0 +1,83 @@ +package org.dromara.workflow.config.expression; + +import org.dromara.warm.flow.core.condition.ConditionStrategy; +import org.dromara.warm.flow.core.listener.ListenerStrategy; +import org.dromara.warm.flow.core.variable.VariableStrategy; + +import java.util.Map; + +/** + * 与 warm-flow-plugin-modes-sb 中 SpEL 策略行为一致,仅将解析器替换为 {@link SafeSpelHelper} + */ +public final class SafeExpressionStrategies { + + private SafeExpressionStrategies() { + } + + /** + * 条件表达式:default@@${flag} == 1 + */ + public static class ConditionDefault implements ConditionStrategy { + @Override + public String getType() { + return "default"; + } + + @Override + public Boolean eval(String expression, Map variable) { + expression = expression.replace("$", "#"); + for (Map.Entry entry : variable.entrySet()) { + if (expression.contains(entry.getKey())) { + expression = expression.replace(entry.getKey(), "#" + entry.getKey()); + } + } + return Boolean.TRUE.equals(SafeSpelHelper.parseExpression(expression, variable)); + } + } + + /** + * 条件表达式:spel@@#{...} + */ + public static class ConditionSpel implements ConditionStrategy { + @Override + public String getType() { + return "spel"; + } + + @Override + public Boolean eval(String expression, Map variable) { + return Boolean.TRUE.equals(SafeSpelHelper.parseExpression(expression, variable)); + } + } + + /** + * 监听器表达式:#{...} + */ + public static class ListenerSpel implements ListenerStrategy { + @Override + public String getType() { + return "#"; + } + + @Override + public Boolean eval(String expression, Map variable) { + SafeSpelHelper.parseExpression(expression, variable); + return true; + } + } + + /** + * 办理人表达式:#{...} + */ + public static class VariableSpel implements VariableStrategy { + @Override + public String getType() { + return "#"; + } + + @Override + public Object preEval(String expression, Map variable) { + return SafeSpelHelper.parseExpression(expression, variable); + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeSpelHelper.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeSpelHelper.java new file mode 100644 index 00000000..85b38f21 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeSpelHelper.java @@ -0,0 +1,98 @@ +package org.dromara.workflow.config.expression; + +import org.dromara.common.core.utils.SpringUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.ReflectiveMethodResolver; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 受限的流程表达式解析器 + *

+ * 替代 warm-flow 自带 SpelHelper(StandardEvaluationContext 无任何限制,可通过 T(java.lang.Runtime) 执行系统命令)。 + * 仅允许:变量引用、运算符、org.dromara 业务 Bean(排除 org.dromara.common)以及基础值类型上的方法调用。 + */ +public final class SafeSpelHelper { + + private static final ExpressionParser PARSER = new SpelExpressionParser(); + private static final ParserContext PARSER_CONTEXT = new TemplateParserContext(); + + private static final String ALLOWED_PACKAGE = "org.dromara."; + private static final String DENIED_PACKAGE = "org.dromara.common."; + + private static final Set DENIED_METHODS = Set.of( + "getClass", "getClassLoader", "forName", "newInstance", "invoke", "exec", "start", + "getRuntime", "getMethod", "getMethods", "getDeclaredMethod", "getDeclaredMethods", + "getField", "getDeclaredField", "getConstructor", "getDeclaredConstructor", + "getBean", "getBeansOfType", "getApplicationContext", "getBeanFactory", + "wait", "notify", "notifyAll" + ); + + private SafeSpelHelper() { + } + + public static Object parseExpression(String expression, Map variables) { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setTypeLocator(typeName -> { + throw new EvaluationException("流程表达式禁止引用类型: " + typeName); + }); + context.setConstructorResolvers(new ArrayList<>()); + context.setMethodResolvers(List.of(new SafeMethodResolver())); + context.setBeanResolver(new SafeBeanResolver()); + context.setVariables(variables); + return PARSER.parseExpression(expression, PARSER_CONTEXT).getValue(context, Object.class); + } + + private static boolean isBusinessClass(Class clazz) { + String name = clazz.getName(); + return name.startsWith(ALLOWED_PACKAGE) && !name.startsWith(DENIED_PACKAGE); + } + + private static boolean isValueObject(Object target) { + return target instanceof CharSequence || target instanceof Number || target instanceof Boolean + || target instanceof Character || target instanceof Date || target instanceof Temporal + || target instanceof Collection || target instanceof Map; + } + + private static class SafeMethodResolver extends ReflectiveMethodResolver { + @Override + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException { + if (targetObject == null || targetObject instanceof Class || DENIED_METHODS.contains(name)) { + throw new AccessException("流程表达式禁止调用方法: " + name); + } + if (!isValueObject(targetObject) && !isBusinessClass(AopUtils.getTargetClass(targetObject))) { + throw new AccessException("流程表达式禁止调用该对象的方法: " + name); + } + return super.resolve(context, targetObject, name, argumentTypes); + } + } + + private static class SafeBeanResolver implements BeanResolver { + @Override + public Object resolve(EvaluationContext context, String beanName) throws AccessException { + Object bean = SpringUtils.getBean(beanName); + if (!isBusinessClass(AopUtils.getTargetClass(bean))) { + throw new AccessException("流程表达式禁止引用该 Bean: " + beanName); + } + return bean; + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwDefinitionController.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwDefinitionController.java index 315be297..dcefa4fe 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwDefinitionController.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwDefinitionController.java @@ -1,5 +1,6 @@ package org.dromara.workflow.controller; +import cn.dev33.satoken.annotation.SaCheckPermission; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; @@ -13,6 +14,7 @@ import org.dromara.warm.flow.core.entity.Definition; import org.dromara.warm.flow.core.service.DefService; import org.dromara.warm.flow.orm.entity.FlowDefinition; import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.common.constant.FlowPermissionConstant; import org.dromara.workflow.domain.vo.FlowDefinitionVo; import org.dromara.workflow.service.IFlwDefinitionService; import org.springframework.transaction.annotation.Transactional; @@ -44,6 +46,7 @@ public class FlwDefinitionController extends BaseController { * @param flowDefinition 参数 * @param pageQuery 分页 */ + @SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST) @GetMapping("/list") public TableDataInfo list(FlowDefinition flowDefinition, PageQuery pageQuery) { return flwDefinitionService.queryList(flowDefinition, pageQuery); @@ -55,6 +58,7 @@ public class FlwDefinitionController extends BaseController { * @param flowDefinition 参数 * @param pageQuery 分页 */ + @SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST) @GetMapping("/unPublishList") public TableDataInfo unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery) { return flwDefinitionService.unPublishList(flowDefinition, pageQuery); @@ -65,6 +69,7 @@ public class FlwDefinitionController extends BaseController { * * @param id 流程定义id */ + @SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST) @GetMapping(value = "/{id}") public R getInfo(@PathVariable("id") Long id) { return R.ok(defService.getById(id)); @@ -76,6 +81,7 @@ public class FlwDefinitionController extends BaseController { * @param flowDefinition 参数 */ @Log(title = "流程定义", businessType = BusinessType.INSERT) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PostMapping @RepeatSubmit() @Transactional(rollbackFor = Exception.class) @@ -89,6 +95,7 @@ public class FlwDefinitionController extends BaseController { * @param flowDefinition 参数 */ @Log(title = "流程定义", businessType = BusinessType.UPDATE) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PutMapping @RepeatSubmit() @Transactional(rollbackFor = Exception.class) @@ -102,6 +109,7 @@ public class FlwDefinitionController extends BaseController { * @param id 流程定义id */ @Log(title = "流程定义", businessType = BusinessType.INSERT) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PutMapping("/publish/{id}") @RepeatSubmit() public R publish(@PathVariable("id") Long id) { @@ -114,6 +122,7 @@ public class FlwDefinitionController extends BaseController { * @param id 流程定义id */ @Log(title = "流程定义", businessType = BusinessType.INSERT) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PutMapping("/unPublish/{id}") @RepeatSubmit() @Transactional(rollbackFor = Exception.class) @@ -125,6 +134,7 @@ public class FlwDefinitionController extends BaseController { * 删除流程定义 */ @Log(title = "流程定义", businessType = BusinessType.DELETE) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @DeleteMapping("/{ids}") public R remove(@PathVariable("ids") List ids) { return toAjax(flwDefinitionService.removeDef(ids)); @@ -136,6 +146,7 @@ public class FlwDefinitionController extends BaseController { * @param id 流程定义id */ @Log(title = "流程定义", businessType = BusinessType.INSERT) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PostMapping("/copy/{id}") @RepeatSubmit() @Transactional(rollbackFor = Exception.class) @@ -150,6 +161,7 @@ public class FlwDefinitionController extends BaseController { * @param category 分类 */ @Log(title = "流程定义", businessType = BusinessType.IMPORT) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PostMapping("/importDef") public R importDef(MultipartFile file, String category) { return R.ok(flwDefinitionService.importJson(file, category)); @@ -163,6 +175,7 @@ public class FlwDefinitionController extends BaseController { * @throws IOException 异常 */ @Log(title = "流程定义", businessType = BusinessType.EXPORT) + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PostMapping("/exportDef/{id}") public void exportDef(@PathVariable("id") Long id, HttpServletResponse response) throws IOException { flwDefinitionService.exportDef(id, response); @@ -173,6 +186,7 @@ public class FlwDefinitionController extends BaseController { * * @param id 流程定义id */ + @SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST) @GetMapping("/xmlString/{id}") public R xmlString(@PathVariable("id") Long id) { return R.ok("操作成功", defService.exportJson(id)); @@ -185,6 +199,7 @@ public class FlwDefinitionController extends BaseController { * @param active 激活/挂起 */ @RepeatSubmit() + @SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT) @PutMapping("/active/{id}") @Transactional(rollbackFor = Exception.class) public R active(@PathVariable("id") Long id, @RequestParam("active") boolean active) { diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java index e64ebdbb..32d7cb5b 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java @@ -1,5 +1,6 @@ package org.dromara.workflow.controller; +import cn.dev33.satoken.annotation.SaCheckPermission; import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; import org.dromara.common.idempotent.annotation.RepeatSubmit; @@ -10,6 +11,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.web.core.BaseController; import org.dromara.warm.flow.core.service.InsService; import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.common.constant.FlowPermissionConstant; import org.dromara.workflow.domain.bo.FlowCancelBo; import org.dromara.workflow.domain.bo.FlowInstanceBo; import org.dromara.workflow.domain.bo.FlowInvalidBo; @@ -43,6 +45,7 @@ public class FlwInstanceController extends BaseController { * @param flowInstanceBo 流程实例 * @param pageQuery 分页 */ + @SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE) @GetMapping("/pageByRunning") public TableDataInfo selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) { return flwInstanceService.selectRunningInstanceList(flowInstanceBo, pageQuery); @@ -54,6 +57,7 @@ public class FlwInstanceController extends BaseController { * @param flowInstanceBo 流程实例 * @param pageQuery 分页 */ + @SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE) @GetMapping("/pageByFinish") public TableDataInfo selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) { return flwInstanceService.selectFinishInstanceList(flowInstanceBo, pageQuery); @@ -74,6 +78,7 @@ public class FlwInstanceController extends BaseController { * * @param businessIds 业务id */ + @SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE) @DeleteMapping("/deleteByBusinessIds/{businessIds}") public R deleteByBusinessIds(@PathVariable("businessIds") List businessIds) { return toAjax(flwInstanceService.deleteByBusinessIds(businessIds)); @@ -107,6 +112,7 @@ public class FlwInstanceController extends BaseController { * @param active 激活/挂起 */ @RepeatSubmit() + @SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE) @PutMapping("/active/{id}") public R active(@PathVariable("id") Long id, @RequestParam("active") boolean active) { return R.ok(active ? insService.active(id) : insService.unActive(id)); @@ -146,6 +152,7 @@ public class FlwInstanceController extends BaseController { * * @param instanceId 流程实例id */ + @SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE) @GetMapping("/instanceVariable/{instanceId}") public R> instanceVariable(@PathVariable("instanceId") Long instanceId) { return R.ok(flwInstanceService.instanceVariable(instanceId)); @@ -158,6 +165,7 @@ public class FlwInstanceController extends BaseController { */ @Log(title = "流程实例管理", businessType = BusinessType.INSERT) @RepeatSubmit() + @SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE) @PostMapping("/invalid") public R invalid(@Validated @RequestBody FlowInvalidBo bo) { return R.ok(flwInstanceService.processInvalid(bo)); diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwTaskController.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwTaskController.java index 108f031e..801db39f 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwTaskController.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwTaskController.java @@ -1,5 +1,6 @@ package org.dromara.workflow.controller; +import cn.dev33.satoken.annotation.SaCheckPermission; import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.dto.StartProcessReturnDTO; @@ -14,6 +15,7 @@ import org.dromara.common.web.core.BaseController; import org.dromara.warm.flow.core.entity.Node; import org.dromara.warm.flow.orm.entity.FlowNode; import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.common.constant.FlowPermissionConstant; import org.dromara.workflow.domain.bo.*; import org.dromara.workflow.domain.vo.FlowHisTaskVo; import org.dromara.workflow.domain.vo.FlowTaskVo; @@ -91,6 +93,7 @@ public class FlwTaskController extends BaseController { * @param flowTaskBo 参数 * @param pageQuery 分页 */ + @SaCheckPermission(FlowPermissionConstant.TASK_MANAGE) @GetMapping("/pageByAllTaskWait") public TableDataInfo pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) { return flwTaskService.pageByAllTaskWait(flowTaskBo, pageQuery); @@ -102,6 +105,7 @@ public class FlwTaskController extends BaseController { * @param flowTaskBo 参数 * @param pageQuery 分页 */ + @SaCheckPermission(FlowPermissionConstant.TASK_MANAGE) @GetMapping("/pageByAllTaskFinish") public TableDataInfo pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) { return flwTaskService.pageByAllTaskFinish(flowTaskBo, pageQuery); @@ -171,6 +175,7 @@ public class FlwTaskController extends BaseController { */ @Log(title = "任务管理", businessType = BusinessType.UPDATE) @RepeatSubmit() + @SaCheckPermission(FlowPermissionConstant.TASK_MANAGE) @PutMapping("/updateAssignee/{userId}") public R updateAssignee(@RequestBody List taskIdList, @PathVariable("userId") String userId) { return toAjax(flwTaskService.updateAssignee(taskIdList, userId)); diff --git a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java index 191d9b8b..b61308b6 100644 --- a/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java @@ -1,5 +1,6 @@ package org.dromara.workflow.service.impl; +import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; @@ -34,6 +35,7 @@ import org.dromara.warm.flow.orm.entity.FlowTask; import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper; import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.common.constant.FlowPermissionConstant; import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.bo.FlowCancelBo; import org.dromara.workflow.domain.bo.FlowInstanceBo; @@ -214,6 +216,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { log.warn("未找到对应的流程实例信息,无法执行删除操作。"); return false; } + instances.forEach(this::checkInstanceOwner); // 获取定义信息 Map definitionMap = defService.getByIds( StreamUtils.toList(instances, Instance::getDefinitionId)).stream() @@ -246,6 +249,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { if (instance == null) { throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE); } + checkInstanceOwner(instance); Definition definition = defService.getById(instance.getDefinitionId()); if (definition == null) { throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF); @@ -267,6 +271,18 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { return true; } + /** + * 仅流程发起人或具备流程实例管理权限的用户可操作该实例 + */ + private void checkInstanceOwner(Instance instance) { + if (LoginHelper.isSuperAdmin() || StpUtil.hasPermission(FlowPermissionConstant.INSTANCE_MANAGE)) { + return; + } + if (!StringUtils.equals(instance.getCreateBy(), LoginHelper.getUserIdStr())) { + throw new ServiceException("无权操作他人发起的流程"); + } + } + /** * 获取当前登陆人发起的流程实例 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStuColBatchClassController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStuColBatchClassController.java index b59e3e91..27a48d34 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStuColBatchClassController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStuColBatchClassController.java @@ -1,5 +1,6 @@ package org.dromara.xg.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import java.util.ArrayList; import java.util.Date; @@ -126,7 +127,7 @@ public class XgStuColBatchClassController extends BaseController { List classIds = xgStuColBatchClassMapper.selectClassIdsByBatchId(batchId); return R.ok(classIds); } catch (Exception e) { - return R.fail("获取收集范围失败:" + e.getMessage()); + return R.fail("获取收集范围失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -142,7 +143,7 @@ public class XgStuColBatchClassController extends BaseController { List scopeDetail = xgStuColBatchClassMapper.selectScopeDetailByBatchId(batchId); return R.ok(scopeDetail); } catch (Exception e) { - return R.fail("获取权限范围详情失败:" + e.getMessage()); + return R.fail("获取权限范围详情失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -201,7 +202,7 @@ public class XgStuColBatchClassController extends BaseController { } } catch (Exception e) { - return R.fail("权限设置异常:" + e.getMessage()); + return R.fail("权限设置异常:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -225,7 +226,7 @@ public class XgStuColBatchClassController extends BaseController { batchName); return R.ok(batchList); } catch (Exception e) { - return R.fail("查询学生可参与批次信息失败:" + e.getMessage()); + return R.fail("查询学生可参与批次信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -256,7 +257,7 @@ public class XgStuColBatchClassController extends BaseController { return R.ok(batchList); } catch (Exception e) { - return R.fail("查询老师关联批次信息失败:" + e.getMessage()); + return R.fail("查询老师关联批次信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -287,7 +288,7 @@ public class XgStuColBatchClassController extends BaseController { return R.ok(filteredTree); } catch (Exception e) { - return R.fail("获取批次组织权限范围失败:" + e.getMessage()); + return R.fail("获取批次组织权限范围失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java index d2a50c29..aea18aa6 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java @@ -1,5 +1,6 @@ package org.dromara.xg.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -126,7 +127,7 @@ public class XgStudentLeaveController extends BaseController { checkDateTime); return R.ok(result); } catch (Exception e) { - return R.fail("参数格式错误:" + e.getMessage()); + return R.fail("参数格式错误:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java index 13c05ca4..0529cd15 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java @@ -673,15 +673,15 @@ public class XgStudentMsgController extends BaseController { } /** - * 按学号重置学生登录密码为固定默认值 + * 按学号重置学生登录密码为随机临时密码,返回临时密码 */ @SaCheckPermission("studentMsg:studentPwdReset:reset") - @Log(title = "重置学生密码", businessType = BusinessType.UPDATE) + @Log(title = "重置学生密码", businessType = BusinessType.UPDATE, isSaveResponseData = false) @RepeatSubmit() @PutMapping("/resetStudentPassword/{studentNo}") - public R resetStudentPassword( + public R resetStudentPassword( @NotBlank(message = "学号不能为空") @PathVariable("studentNo") String studentNo) { - return toAjax(xgStudentMsgService.resetStudentPasswordByStudentNo(studentNo)); + return R.ok("操作成功", xgStudentMsgService.resetStudentPasswordByStudentNo(studentNo)); } /** diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java index 3cd5e827..68014956 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java @@ -187,10 +187,10 @@ public interface IXgStudentMsgService { StudentPwdResetVo queryForPwdResetByStudentNo(String studentNo); /** - * 按学号重置学生登录密码为固定默认值 + * 按学号重置学生登录密码为随机临时密码,并踢出该学生已登录会话 * * @param studentNo 学号 - * @return 是否成功 + * @return 临时密码 */ - Boolean resetStudentPasswordByStudentNo(String studentNo); + String resetStudentPasswordByStudentNo(String studentNo); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgBatchServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgBatchServiceImpl.java index 0fb2e16a..f448939e 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgBatchServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgBatchServiceImpl.java @@ -834,16 +834,16 @@ public class XgStudentMsgBatchServiceImpl implements IXgStudentMsgBatchService { // 使用前台传递的id if (bo.getId() != null) { add.setId(bo.getId()); - add.setDeleted(0L); - add.setCreateBy(currentUserId); - add.setCreateTime(new Date()); - add.setUpdateBy(currentUserId); - add.setUpdateTime(new Date()); - add.setCreateDept(LoginHelper.getDeptId()); - add.setBatchId(bo.getBatchId()); - add.setInfoStatus("3"); - add.setDataSource("manual"); } + add.setDeleted(0L); + add.setCreateBy(currentUserId); + add.setCreateTime(new Date()); + add.setUpdateBy(currentUserId); + add.setUpdateTime(new Date()); + add.setCreateDept(LoginHelper.getDeptId()); + add.setBatchId(bo.getBatchId()); + add.setInfoStatus("3"); + add.setDataSource("manual"); boolean insertResult = baseMapper.insert(add) > 0; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java index f2cb44c8..6504ea4e 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java @@ -1,10 +1,12 @@ package org.dromara.xg.service.impl; +import cn.dev33.satoken.stp.StpUtil; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.helper.DataPermissionHelper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -1560,8 +1562,41 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { return currentUserCollegeHelper.resolveCurrentUserCollegeId(); } - /** 学生账号重置密码固定值(仅用于管理端重置学生密码) */ - private static final String STUDENT_RESET_PASSWORD = "Hzdyjsxs123456!@#"; + /** + * 学院级账号只能操作本学院学生(一年级代理学院可操作全部学生);未归属学院的校级账号不受限 + */ + private void checkStudentInCurrentUserCollege(XgStudentMsg student) { + if (LoginHelper.isSuperAdmin()) { + return; + } + Long collegeId = getCurrentUserCollegeId(); + if (collegeId == null) { + return; + } + Long proxyCollegeId = freshmanCollegeProxyHelper.resolveProxyCollegeIdOrNull(); + if (collegeId.equals(proxyCollegeId) || String.valueOf(collegeId).equals(student.getCollegeId())) { + return; + } + throw new ServiceException("只能操作本学院的学生"); + } + + private static String generateTempPassword() { + String upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + String lower = "abcdefghijkmnpqrstuvwxyz"; + String digit = "23456789"; + String special = "@#$%"; + java.security.SecureRandom random = new java.security.SecureRandom(); + StringBuilder sb = new StringBuilder(); + sb.append(upper.charAt(random.nextInt(upper.length()))); + for (int i = 0; i < 5; i++) { + sb.append(lower.charAt(random.nextInt(lower.length()))); + } + sb.append(special.charAt(random.nextInt(special.length()))); + for (int i = 0; i < 3; i++) { + sb.append(digit.charAt(random.nextInt(digit.length()))); + } + return sb.toString(); + } @Override public StudentPwdResetVo queryForPwdResetByStudentNo(String studentNo) { @@ -1576,6 +1611,7 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { if (student == null) { throw new ServiceException("未找到学号对应的学生信息"); } + checkStudentInCurrentUserCollege(student); StudentPwdResetVo vo = new StudentPwdResetVo(); vo.setId(student.getId()); vo.setStudentNo(student.getStudentNo()); @@ -1600,7 +1636,7 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { } @Override - public Boolean resetStudentPasswordByStudentNo(String studentNo) { + public String resetStudentPasswordByStudentNo(String studentNo) { if (StringUtils.isBlank(studentNo)) { throw new ServiceException("学号不能为空"); } @@ -1616,7 +1652,8 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { if (student.getId() == null) { throw new ServiceException("学生数据异常,缺少主键"); } - SysUserVo user = sysUserService.selectUserById(student.getId()); + checkStudentInCurrentUserCollege(student); + SysUserVo user = DataPermissionHelper.ignore(() -> sysUserService.selectUserById(student.getId())); if (user == null) { throw new ServiceException("该学生没有登录账号,无法重置密码"); } @@ -1626,12 +1663,20 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService { if (StringUtils.isNotBlank(user.getUserName()) && !no.equals(user.getUserName())) { throw new ServiceException("学号与登录账号不一致,请联系管理员"); } - int rows = sysUserService.resetUserPwd(student.getId(), BCrypt.hashpw(STUDENT_RESET_PASSWORD)); + String tempPassword = generateTempPassword(); + // 学院范围已校验;sys_user.update 自带部门数据权限,学生挂在班级部门,班主任在学院部门,不忽略会更新 0 行 + int rows = DataPermissionHelper.ignore( + () -> sysUserService.resetUserPwd(student.getId(), BCrypt.hashpw(tempPassword))); if (rows <= 0) { throw new ServiceException("重置密码失败,请稍后重试"); } + try { + StpUtil.logout(user.getUserType() + ":" + user.getUserId()); + } catch (Exception e) { + log.warn("学生密码已重置但踢下线失败, studentNo={}, userId={}", no, user.getUserId(), e); + } log.info("学生密码已重置, studentNo={}, studentId={}, operator={}", no, student.getId(), LoginHelper.getUserId()); - return true; + return tempPassword; } } diff --git a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationAppointmentController.java b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationAppointmentController.java index 39afed8b..9d6d6db4 100644 --- a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationAppointmentController.java +++ b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationAppointmentController.java @@ -138,6 +138,10 @@ public class XljkConsultationAppointmentController extends BaseController { */ @PostMapping("/submit") public R submitAppointment(@RequestBody XljkConsultationAppointmentBo bo) { + if (!StpUtil.hasPermission("xljk:consultationappointment:add") + && !StpUtil.hasPermission("xljk:consultationappointment:edit")) { + bo.setStudentId(LoginHelper.getUserId()); + } boolean result = service.submitAppointment(bo); if (result) { return R.ok(true); diff --git a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationNoShowController.java b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationNoShowController.java index 040a9ad7..845df2f5 100644 --- a/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationNoShowController.java +++ b/backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationNoShowController.java @@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; import java.util.List; import java.util.Map; @@ -64,6 +65,10 @@ public class XljkConsultationNoShowController extends BaseController { */ @GetMapping("/check-limitation") public R> checkLimitation(@RequestParam("studentId") Long studentId) { + if (!StpUtil.hasPermission("xljk:consultationnoshow:list") + && !StpUtil.hasPermission("xljk:consultationappointment:list")) { + studentId = LoginHelper.getUserId(); + } return R.ok(service.checkLimitation(studentId)); } } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhAssessmentApplicationApprovalMapper.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhAssessmentApplicationApprovalMapper.java index 99e75ae0..6d37d4a5 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhAssessmentApplicationApprovalMapper.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhAssessmentApplicationApprovalMapper.java @@ -1,5 +1,7 @@ package org.dromara.xskh.mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.xskh.domain.XskhAssessmentApplicationApproval; import org.dromara.xskh.domain.vo.XskhAssessmentApplicationApprovalVo; @@ -26,4 +28,16 @@ public interface XskhAssessmentApplicationApprovalMapper List selectSelfScoresWithIndicatorInfo( Long applicationId); + /** + * 根据学生信息ID查询学号 + */ + @Select("SELECT student_no FROM xg_student_msg WHERE id = #{studentId} AND deleted = 0 LIMIT 1") + String selectStudentNoByStudentId(@Param("studentId") Long studentId); + + /** + * 按学号统计学生信息条数(用于判断当前登录账号是否为学生) + */ + @Select("SELECT COUNT(1) FROM xg_student_msg WHERE student_no = #{studentNo} AND deleted = 0") + long countStudentByStudentNo(@Param("studentNo") String studentNo); + } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentApplicationApprovalServiceImpl.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentApplicationApprovalServiceImpl.java index 102685ca..2dc33420 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentApplicationApprovalServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentApplicationApprovalServiceImpl.java @@ -1,7 +1,9 @@ package org.dromara.xskh.service.impl; import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -55,6 +57,9 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess @Override public XskhAssessmentApplicationApprovalVo queryById(Long id) { XskhAssessmentApplicationApprovalVo vo = baseMapper.selectVoById(id); + if (vo != null && isCurrentUserStudent() && !StringUtils.equals(vo.getStudentNo(), LoginHelper.getUsername())) { + throw new ServiceException("无权查看他人的考核申请"); + } if (vo != null) { // 查询完整的自评得分列表(包含指标表关联数据) List selfScoreVos = baseMapper.selectSelfScoresWithIndicatorInfo(id); @@ -110,6 +115,14 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess @Transactional(rollbackFor = Exception.class) public Long insertByBo(XskhAssessmentApplicationApprovalBo bo) { XskhAssessmentApplicationApproval add = MapstructUtils.convert(bo, XskhAssessmentApplicationApproval.class); + if (isCurrentUserStudent()) { + String username = LoginHelper.getUsername(); + if (!StringUtils.equals(username, baseMapper.selectStudentNoByStudentId(add.getStudentId()))) { + throw new ServiceException("只能提交本人的考核申请"); + } + add.setStudentNo(username); + } + clearApprovalFields(add); validEntityBeforeSave(add); // 新增时默认状态为草稿 add.setStatus("1"); @@ -130,7 +143,23 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess @Override @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(XskhAssessmentApplicationApprovalBo bo) { + XskhAssessmentApplicationApproval existing = baseMapper.selectById(bo.getId()); + if (existing == null) { + throw new ServiceException("考核申请不存在"); + } + checkOwner(existing); + if (!"1".equals(existing.getStatus()) && !"2".equals(existing.getStatus()) + && !"3".equals(existing.getStatus())) { + throw new ServiceException("只有草稿、已提交、班主任审核状态的考核申请可以修改"); + } XskhAssessmentApplicationApproval update = MapstructUtils.convert(bo, XskhAssessmentApplicationApproval.class); + // 学生本人与学号归属不允许通过修改接口变更 + update.setStudentId(null); + update.setStudentNo(null); + if (!"1".equals(update.getStatus())) { + update.setStatus(null); + } + clearApprovalFields(update); validEntityBeforeSave(update); boolean flag = baseMapper.updateById(update) > 0; if (flag) { @@ -149,6 +178,32 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess // TODO 做一些数据校验,如唯一约束 } + /** + * 班主任评分与审核字段只能由审核接口写入 + */ + private void clearApprovalFields(XskhAssessmentApplicationApproval entity) { + entity.setTeacherScore(null); + entity.setTotalScore(null); + entity.setTeacherApprovalBy(null); + entity.setTeacherApprovalTime(null); + entity.setTeacherApprovalRemark(null); + entity.setSubmitTime(null); + } + + private boolean isCurrentUserStudent() { + String username = LoginHelper.getUsername(); + return StringUtils.isNotBlank(username) && baseMapper.countStudentByStudentNo(username) > 0; + } + + /** + * 学生账号只能操作本人的考核申请 + */ + private void checkOwner(XskhAssessmentApplicationApproval application) { + if (isCurrentUserStudent() && !StringUtils.equals(application.getStudentNo(), LoginHelper.getUsername())) { + throw new ServiceException("无权操作他人的考核申请"); + } + } + /** * 批量删除学生考核申请、班主任审核 */ @@ -158,6 +213,9 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess // 校验是否可以删除:只有草稿状态的申请可以删除 for (Long id : ids) { XskhAssessmentApplicationApproval application = baseMapper.selectById(id); + if (application != null) { + checkOwner(application); + } if (application != null && !"1".equals(application.getStatus())) { throw new RuntimeException("只有草稿状态的考核申请可以删除"); } @@ -181,6 +239,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess if (existing == null) { throw new RuntimeException("考核申请不存在"); } + checkOwner(existing); if (!"1".equals(existing.getStatus()) && !"2".equals(existing.getStatus()) && !"3".equals(existing.getStatus())) { throw new RuntimeException("只有草稿、已提交、班主任审核状态的考核申请可以保存"); @@ -201,6 +260,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess if (application == null) { throw new RuntimeException("考核申请不存在"); } + checkOwner(application); if (!"1".equals(application.getStatus()) && !"2".equals(application.getStatus()) && !"3".equals(application.getStatus())) { throw new RuntimeException("只有草稿、已提交、班主任审核状态的考核申请可以提交"); @@ -221,6 +281,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess if (application == null) { throw new RuntimeException("考核申请不存在"); } + checkOwner(application); if (!"1".equals(application.getStatus())) { throw new RuntimeException("只有草稿状态的考核申请可以删除"); } @@ -283,6 +344,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess wrapper.eq(XskhAssessmentApplicationApproval::getScheduleId, scheduleId); XskhAssessmentApplicationApproval application = baseMapper.selectOne(wrapper); if (application != null) { + checkOwner(application); return queryById(application.getId()); } return null; diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java index e6a6cad4..415528fe 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java @@ -1,5 +1,6 @@ package org.dromara.zsjy.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.dev33.satoken.annotation.SaCheckPermission; import org.dromara.common.core.domain.R; import org.dromara.common.mybatis.core.page.PageQuery; @@ -130,7 +131,7 @@ public class InternshipInfoController extends BaseController { } } catch (Exception e) { log.error("获取学生实习信息异常,用户ID: {},错误信息: {}", LoginHelper.getUserId(), e.getMessage(), e); - return R.fail("获取实习信息失败:" + e.getMessage()); + return R.fail("获取实习信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -153,7 +154,7 @@ public class InternshipInfoController extends BaseController { } } catch (Exception e) { log.error("新增实习信息异常,学生ID:{},错误信息:{}", addBO.getStudentId(), e.getMessage(), e); - return R.fail("新增实习信息失败:" + e.getMessage()); + return R.fail("新增实习信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -176,7 +177,7 @@ public class InternshipInfoController extends BaseController { } } catch (Exception e) { log.error("修改实习信息异常,实习信息ID:{},错误信息:{}", editBO.getInternshipId(), e.getMessage(), e); - return R.fail("修改实习信息失败:" + e.getMessage()); + return R.fail("修改实习信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -200,7 +201,7 @@ public class InternshipInfoController extends BaseController { } } catch (Exception e) { log.error("删除实习信息异常,IDs:{},错误信息:{}", internshipIds, e.getMessage(), e); - return R.fail("删除实习信息失败:" + e.getMessage()); + return R.fail("删除实习信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -223,7 +224,7 @@ public class InternshipInfoController extends BaseController { } } catch (Exception e) { log.error("获取当前学生实习信息失败:{}", e.getMessage(), e); - return R.fail("获取实习信息失败:" + e.getMessage()); + return R.fail("获取实习信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -248,7 +249,7 @@ public class InternshipInfoController extends BaseController { } catch (Exception e) { log.error("实习信息变更异常,原实习ID:{},错误信息:{}", changeBO.getInternshipId(), e.getMessage(), e); - return R.fail("实习信息变更失败:" + e.getMessage()); + return R.fail("实习信息变更失败:" + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentInfoController.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentInfoController.java index 80356b21..68b8718a 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentInfoController.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentInfoController.java @@ -1,5 +1,6 @@ package org.dromara.zsjy.controller; +import org.dromara.common.web.handler.GlobalExceptionHandler; import java.util.List; import lombok.Data; @@ -179,7 +180,7 @@ public class ZsjyEnrollmentInfoController extends BaseController { List colleges = enrollmentMajorService.selectOpenColleges(); return R.ok(colleges); } catch (Exception e) { - return R.fail("获取学院列表异常: " + e.getMessage()); + return R.fail("获取学院列表异常: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -192,7 +193,7 @@ public class ZsjyEnrollmentInfoController extends BaseController { List majors = enrollmentMajorService.selectOpenMajorsByCollegeId(collegeId); return R.ok(majors); } catch (Exception e) { - return R.fail("获取专业列表异常: " + e.getMessage()); + return R.fail("获取专业列表异常: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } } diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentProcessController.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentProcessController.java index ba6c0c9e..99b8fce6 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentProcessController.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentProcessController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.dromara.common.idempotent.annotation.RepeatSubmit; @@ -63,6 +64,7 @@ public class ZsjyEnrollmentProcessController extends BaseController { /** * 导出新生录取名册(与纸质录取名册字段一致) */ + @SaCheckPermission(value = {"zsjy:enrollment:process:export", "zsjy:enrollment:process:query", "zsjy:enrollmentProcess:export", "zsjy:enrollmentStatistics:view"}, mode = SaMode.OR) @Log(title = "新生录取名册", businessType = BusinessType.EXPORT) @PostMapping("/exportRegister") public void exportRegister(ZsjyEnrollmentProcessBo bo, HttpServletResponse response) { diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipDailyServiceImpl.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipDailyServiceImpl.java index 817b237c..338970eb 100644 --- a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipDailyServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipDailyServiceImpl.java @@ -1,5 +1,6 @@ package org.dromara.zsjy.service.impl; +import org.dromara.common.web.handler.GlobalExceptionHandler; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; @@ -156,7 +157,7 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi } catch (Exception e) { log.error("学生签到失败: {}", e.getMessage(), e); - return R.fail("签到失败: " + e.getMessage()); + return R.fail("签到失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -186,7 +187,7 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi } catch (Exception e) { log.error("获取考勤日历数据失败: {}", e.getMessage(), e); - return R.fail("获取考勤日历数据失败: " + e.getMessage()); + return R.fail("获取考勤日历数据失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -212,7 +213,7 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi } catch (Exception e) { log.error("获取考勤统计数据失败: {}", e.getMessage(), e); - return R.fail("获取考勤统计数据失败: " + e.getMessage()); + return R.fail("获取考勤统计数据失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } @@ -260,7 +261,7 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi } catch (Exception e) { log.error("获取学生月度统计数据失败: studentId={}, year={}, month={}, error={}", studentId, year, month, e.getMessage(), e); - return R.fail("获取学生月度统计数据失败: " + e.getMessage()); + return R.fail("获取学生月度统计数据失败: " + GlobalExceptionHandler.safeMessage(e.getMessage())); } } diff --git a/backend/script/sql/update/update_20261026_student_pwd_reset_menu.sql b/backend/script/sql/update/update_20261026_student_pwd_reset_menu.sql index 84efbea8..8440fa7a 100644 --- a/backend/script/sql/update/update_20261026_student_pwd_reset_menu.sql +++ b/backend/script/sql/update/update_20261026_student_pwd_reset_menu.sql @@ -4,7 +4,7 @@ -- 父菜单:学生管理 (1959499382894141441) -- Web 页面:xg/xsjcxx/studentMsg/studentPwdReset/index -- 移动端首页入口权限:student:pwdreset:listapp --- 重置密码固定值(服务端写死):Hzdyjsxs123456!@# +-- 重置后返回随机临时密码(仅显示一次),并踢出该学生已登录会话 -- 执行:开发/生产库均可幂等执行;执行后给角色勾选菜单或跑下方角色授权段 -- ============================================================================= diff --git a/backend/script/sql/update/update_20261026_workflow_permission.sql b/backend/script/sql/update/update_20261026_workflow_permission.sql new file mode 100644 index 00000000..a4b3c8fc --- /dev/null +++ b/backend/script/sql/update/update_20261026_workflow_permission.sql @@ -0,0 +1,51 @@ +-- ============================================================================= +-- 工作流管理类接口权限按钮(安全整改:流程定义/实例/全部待办接口原先仅校验登录) +-- 日期:2026-10-26 +-- 挂载菜单:流程定义(11620)、流程实例(11621)、待办任务(11631) +-- 授权方式:给已拥有对应菜单的角色补授权,不改变现有可见范围 +-- 执行后需要相关用户重新登录(或清理 Redis 中的登录缓存)权限才会生效 +-- 执行:开发/生产库均可幂等执行 +-- ============================================================================= + +INSERT INTO `sys_menu` ( + `menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, + `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, + `create_dept`, `create_by`, `create_time`, `remark` +) +SELECT t.menu_id, t.menu_name, t.parent_id, t.order_num, '#', '', NULL, + 1, 0, 'F', '0', '0', t.perms, '#', 103, 1, NOW(), t.remark +FROM ( + SELECT 2026102620000000101 AS menu_id, '流程定义查询' AS menu_name, 11620 AS parent_id, 1 AS order_num, + 'workflow:definition:list' AS perms, '流程定义列表/详情/设计器读取' AS remark + UNION ALL + SELECT 2026102620000000102, '流程定义维护', 11620, 2, + 'workflow:definition:edit', '流程定义新增/修改/发布/导入/删除/设计器保存' + UNION ALL + SELECT 2026102620000000103, '流程实例管理', 11621, 1, + 'workflow:instance:manage', '全部实例查询/作废/挂起/变量/删除他人实例/撤销他人流程' + UNION ALL + SELECT 2026102620000000104, '全部待办管理', 11631, 1, + 'workflow:task:manage', '全部待办/已办查询与修改办理人' +) t +WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` m WHERE m.menu_id = t.menu_id); + +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT DISTINCT rm.role_id, m.menu_id +FROM `sys_role_menu` rm +JOIN ( + SELECT 11620 AS parent_id, 2026102620000000101 AS menu_id UNION ALL + SELECT 11620, 2026102620000000102 UNION ALL + SELECT 11621, 2026102620000000103 UNION ALL + SELECT 11631, 2026102620000000104 +) m ON rm.menu_id = m.parent_id +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_role_menu` x WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id +); + +-- 核对:哪些角色拿到了工作流管理权限(应只有管理员类角色) +SELECT r.role_id, r.role_key, r.role_name, m.perms +FROM sys_role_menu rm +JOIN sys_role r ON r.role_id = rm.role_id +JOIN sys_menu m ON m.menu_id = rm.menu_id +WHERE m.menu_id BETWEEN 2026102620000000101 AND 2026102620000000104 +ORDER BY r.role_id, m.perms; diff --git a/docs/开发文档/API/学生密码重置API.md b/docs/开发文档/API/学生密码重置API.md index c51e82b6..74e5600e 100644 --- a/docs/开发文档/API/学生密码重置API.md +++ b/docs/开发文档/API/学生密码重置API.md @@ -10,16 +10,18 @@ ### 模块功能 -学工老师按学号查询学生基本信息后,一键将对应学生登录账号密码重置为系统固定默认值。 +学工老师按学号查询学生基本信息后,一键将对应学生登录账号密码重置为服务端随机生成的临时密码(2026-10-26 安全整改:取消固定默认密码)。 **不复用** `system:user:resetPwd`,避免把系统用户管理权下放给学工老师。 **核心约束**: - 仅允许重置 `sys_user.account_type = '2'`(学生账号) - 要求 `user_id = xg_student_msg.id`,且 `user_name` 与学号一致(若有用户名) -- 密码由服务端写死常量,前端不可传入自定义密码 -- 有菜单/按钮权限即可操作任意学生,不做班主任数据范围限制 +- 临时密码由服务端 SecureRandom 生成(10 位,含大小写、数字、特殊字符),前端不可传入自定义密码 +- 数据范围:超管、校级账号(无学院)可操作全部学生;学院账号只能查询/重置本学院(或一年级代理学院)的学生 +- 重置后立即踢下线该学生的所有在线会话 +- 操作日志不记录响应体,临时密码不落库日志 -**固定默认密码**(服务端常量,勿在客户端硬编码用于改密请求):`Hzdyjsxs123456!@#` +**临时密码**:仅在本次接口响应 `data` 中返回一次,由老师当面告知学生,学生登录后应自行修改。 ### 权限说明 @@ -39,7 +41,7 @@ | 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 | | -------- | --------- | ---- | -------- | -------- | | 按学号查询学生 | GET | `/studentMsg/studentMsg/queryByStudentNo/{studentNo}` | `studentMsg:studentPwdReset:query` | 返回姓名、学号、班级、班主任 | -| 重置学生密码 | PUT | `/studentMsg/studentMsg/resetStudentPassword/{studentNo}` | `studentMsg:studentPwdReset:reset` | 重置为固定默认密码 | +| 重置学生密码 | PUT | `/studentMsg/studentMsg/resetStudentPassword/{studentNo}` | `studentMsg:studentPwdReset:reset` | 重置为随机临时密码 | --- @@ -87,7 +89,7 @@ - **接口路径**:`PUT /studentMsg/studentMsg/resetStudentPassword/{studentNo}` - **权限标识**:`studentMsg:studentPwdReset:reset` -- **功能描述**:将学号对应学生登录密码重置为固定默认值;写操作日志「重置学生密码」 +- **功能描述**:将学号对应学生登录密码重置为随机临时密码并踢下线;写操作日志「重置学生密码」(不记录响应) - **防重复提交**:`@RepeatSubmit` #### 路径参数 @@ -98,14 +100,15 @@ #### 请求体 -无。密码由服务端常量生成 BCrypt 后写入,不允许客户端传密。 +无。临时密码由服务端随机生成并 BCrypt 后写入,不允许客户端传密。 #### 成功响应 ```json { "code": 200, - "msg": "操作成功" + "msg": "操作成功", + "data": "Abcdef@123" } ``` @@ -117,6 +120,7 @@ | 无登录账号 | 该学生没有登录账号,无法重置密码 | | 非学生账号 | 该账号不是学生账号,禁止重置 | | 学号与登录名不一致 | 学号与登录账号不一致,请联系管理员 | +| 非本学院学生 | 只能操作本学院的学生 | | 无权限 | 403 | --- diff --git a/mobile/src/api/studentPwdReset.js b/mobile/src/api/studentPwdReset.js index 26207ca0..c46646aa 100644 --- a/mobile/src/api/studentPwdReset.js +++ b/mobile/src/api/studentPwdReset.js @@ -15,7 +15,7 @@ export const queryStudentForPwdReset = (studentNo) => { }; /** - * 按学号重置学生登录密码为固定默认值 + * 按学号重置学生登录密码为随机临时密码,data 为临时密码 * @param {string} studentNo 学号 */ export const resetStudentPassword = (studentNo) => { diff --git a/mobile/src/pages/xg/xsjcxx/studentPwdReset/index.vue b/mobile/src/pages/xg/xsjcxx/studentPwdReset/index.vue index 120d1797..0277f57d 100644 --- a/mobile/src/pages/xg/xsjcxx/studentPwdReset/index.vue +++ b/mobile/src/pages/xg/xsjcxx/studentPwdReset/index.vue @@ -103,6 +103,36 @@ export default { this.queryLoading = false; } }, + copyPassword(pwd) { + if (!pwd) { + uni.showToast({ title: '没有可复制的密码', icon: 'none' }); + return; + } + uni.setClipboardData({ + data: pwd, + success: () => { + uni.showToast({ title: '密码已复制', icon: 'success' }); + }, + fail: () => { + uni.showToast({ title: '复制失败,请手动复制', icon: 'none' }); + } + }); + }, + showResetResult(tempPwd) { + uni.showModal({ + title: '密码已重置', + content: `临时密码:${tempPwd}\n请告知学生登录后立即修改密码,此密码仅显示一次。`, + showCancel: true, + cancelText: '复制密码', + confirmText: '我已记录', + success: (modalRes) => { + if (modalRes.cancel) { + this.copyPassword(tempPwd); + this.showResetResult(tempPwd); + } + } + }); + }, handleReset() { if (!this.student || !this.student.studentNo) { uni.showToast({ title: '请先查询学生', icon: 'none' }); @@ -112,7 +142,7 @@ export default { const no = this.student.studentNo; uni.showModal({ title: '重置密码确认', - content: `确认将学号「${no}」(${name})的登录密码重置为系统默认密码?`, + content: `确认将学号「${no}」(${name})的登录密码重置为随机临时密码?重置后该学生需重新登录。`, confirmText: '确定', cancelText: '取消', success: async (modalRes) => { @@ -121,8 +151,8 @@ export default { } this.resetLoading = true; try { - await resetStudentPassword(no); - uni.showToast({ title: '密码已重置为默认密码', icon: 'success' }); + const res = await resetStudentPassword(no); + this.showResetResult(String((res && res.data) || '')); } catch (e) { // 错误提示由 request 拦截器处理 } finally { diff --git a/web/src/api/xg/xsjcxx/studentMsg/studentMsg/index.ts b/web/src/api/xg/xsjcxx/studentMsg/studentMsg/index.ts index ce5f011c..f1798939 100644 --- a/web/src/api/xg/xsjcxx/studentMsg/studentMsg/index.ts +++ b/web/src/api/xg/xsjcxx/studentMsg/studentMsg/index.ts @@ -137,7 +137,7 @@ export const queryStudentForPwdReset = (studentNo: string): AxiosPromise { return request({ diff --git a/web/src/layout/components/SocialCallback/index.vue b/web/src/layout/components/SocialCallback/index.vue index a5cd7683..fcc1a4ec 100644 --- a/web/src/layout/components/SocialCallback/index.vue +++ b/web/src/layout/components/SocialCallback/index.vue @@ -65,6 +65,13 @@ const init = async () => { // 如果域名不相等 则重定向处理 const host = window.location.host; if (domain !== host) { + // 只允许跳转到本校域名,防止授权码被带到第三方站点 + const domainHost = String(domain || '').split(':')[0].toLowerCase(); + const trusted = domainHost === 'hzdyjsxy.cn' || domainHost.endsWith('.hzdyjsxy.cn') || domainHost === 'localhost' || domainHost === '127.0.0.1'; + if (!trusted) { + handleError(new Error('非法的回调域名')); + return; + } const urlFull = new URL(window.location.href); urlFull.host = domain; window.location.href = urlFull.toString(); diff --git a/web/src/utils/htmlSafe.ts b/web/src/utils/htmlSafe.ts new file mode 100644 index 00000000..89a08237 --- /dev/null +++ b/web/src/utils/htmlSafe.ts @@ -0,0 +1,62 @@ +/** + * HTML 安全处理工具:拼接 innerHTML / v-html 前必须经过转义或清洗,防止存储型 XSS + */ + +/** 转义为纯文本,用于把用户输入拼接进 HTML 字符串 */ +export function escapeHtml(value: unknown): string { + if (value === null || value === undefined) { + return ''; + } + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +const ALLOWED_TAGS = new Set([ + 'p', 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'span', 'div', 'ul', 'ol', 'li', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'table', 'thead', + 'tbody', 'tr', 'th', 'td', 'img', 'a', 'hr', 'sub', 'sup', 'font' +]); + +const ALLOWED_ATTRS = new Set(['style', 'class', 'href', 'src', 'alt', 'title', 'width', 'height', 'colspan', 'rowspan', 'align', 'color', 'target']); + +const SAFE_URL = /^(https?:|mailto:|tel:|\/|#|data:image\/(png|jpe?g|gif|bmp|webp);)/i; + +/** 清洗富文本:只保留白名单标签与属性,移除脚本、事件属性及 javascript: 链接 */ +export function sanitizeHtml(html: unknown): string { + if (html === null || html === undefined || html === '') { + return ''; + } + const doc = new DOMParser().parseFromString(`

${String(html)}
`, 'text/html'); + const root = doc.body.firstElementChild as HTMLElement; + + const walk = (node: Element) => { + Array.from(node.children).forEach((child) => { + const tag = child.tagName.toLowerCase(); + if (!ALLOWED_TAGS.has(tag)) { + child.replaceWith(doc.createTextNode(child.textContent || '')); + return; + } + Array.from(child.attributes).forEach((attr) => { + const name = attr.name.toLowerCase(); + const value = attr.value.trim(); + if (!ALLOWED_ATTRS.has(name)) { + child.removeAttribute(attr.name); + } else if ((name === 'href' || name === 'src') && !SAFE_URL.test(value)) { + child.removeAttribute(attr.name); + } else if (name === 'style' && /expression|javascript:|url\s*\(/i.test(value)) { + child.removeAttribute(attr.name); + } + }); + if (tag === 'a') { + child.setAttribute('rel', 'noopener noreferrer'); + } + walk(child); + }); + }; + walk(root); + return root.innerHTML; +} diff --git a/web/src/views/hq/pm/repairOrder/index.vue b/web/src/views/hq/pm/repairOrder/index.vue index 463dabde..c9ea46b9 100644 --- a/web/src/views/hq/pm/repairOrder/index.vue +++ b/web/src/views/hq/pm/repairOrder/index.vue @@ -2179,7 +2179,10 @@ const viewImageGallery = (attachment: any) => { // 使用Element Plus的图片预览功能 const previewList = imageList.map((img) => img.filePath); const imageViewer = document.createElement('div'); - imageViewer.innerHTML = ``; + const previewImg = document.createElement('img'); + previewImg.src = attachment.filePath; + previewImg.style.display = 'none'; + imageViewer.appendChild(previewImg); document.body.appendChild(imageViewer); // 触发预览 diff --git a/web/src/views/rs/index.vue b/web/src/views/rs/index.vue index 1b8b76e1..2de8e7ab 100644 --- a/web/src/views/rs/index.vue +++ b/web/src/views/rs/index.vue @@ -713,6 +713,7 @@ import { exportResumePdf } from '@/api/rs/job'; import { parseTime } from '@/utils/ruoyi'; +import { escapeHtml } from '@/utils/htmlSafe'; import type { JobVO, JobForm, ApplicationVO } from '@/api/rs/job'; import { listDept } from '@/api/system/dept'; import type { DeptVO } from '@/api/system/dept/types'; @@ -1770,25 +1771,25 @@ function handleExportResume() {

基本信息

-

姓名:${selectedRecord.applicantName || '未提供'}

-

应聘职位:${selectedRecord.jobTitle || '未提供'}

-

联系电话:${selectedRecord.contactPhone || '未提供'}

-

联系邮箱:${selectedRecord.contactEmail || '未提供'}

+

姓名:${escapeHtml(selectedRecord.applicantName || '未提供')}

+

应聘职位:${escapeHtml(selectedRecord.jobTitle || '未提供')}

+

联系电话:${escapeHtml(selectedRecord.contactPhone || '未提供')}

+

联系邮箱:${escapeHtml(selectedRecord.contactEmail || '未提供')}

教育背景

-

最高学历:${selectedRecord.education || '未提供'}

+

最高学历:${escapeHtml(selectedRecord.education || '未提供')}

工作经验

-

${selectedRecord.experience || '未提供工作经验'}

+

${escapeHtml(selectedRecord.experience || '未提供工作经验')}

自我介绍

-

${selectedRecord.selfDescription || '未提供自我介绍'}

+

${escapeHtml(selectedRecord.selfDescription || '未提供自我介绍')}

diff --git a/web/src/views/rs/recruitment-portal.vue b/web/src/views/rs/recruitment-portal.vue index 985f2f12..405ff7fd 100644 --- a/web/src/views/rs/recruitment-portal.vue +++ b/web/src/views/rs/recruitment-portal.vue @@ -164,12 +164,12 @@

岗位职责

-
+

任职要求

-
+
@@ -205,6 +205,7 @@ import { Search, Refresh, Briefcase, Document, User, List, Grid, OfficeBuilding, import { listPublicJob, getPublicJob, type JobVO, type JobQuery } from '@/api/rs/job'; import { incrementJobViewCount } from '@/api/rs/application'; import { parseTime } from '@/utils/ruoyi'; +import { sanitizeHtml } from '@/utils/htmlSafe'; // 响应式数据 const loading = ref(false); diff --git a/web/src/views/xg/xsjcxx/studentMsg/studentPwdReset/index.vue b/web/src/views/xg/xsjcxx/studentMsg/studentPwdReset/index.vue index ae899a68..1ad16262 100644 --- a/web/src/views/xg/xsjcxx/studentMsg/studentPwdReset/index.vue +++ b/web/src/views/xg/xsjcxx/studentMsg/studentPwdReset/index.vue @@ -96,6 +96,31 @@ const handleQuery = async () => { } }; +const copyPassword = async (pwd: string) => { + if (!pwd) { + ElMessage.warning('没有可复制的密码'); + return; + } + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(pwd); + } else { + const textarea = document.createElement('textarea'); + textarea.value = pwd; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'absolute'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + textarea.remove(); + } + ElMessage.success('密码已复制'); + } catch { + ElMessage.error('复制失败,请手动复制'); + } +}; + const handleReset = async () => { if (!student.value?.studentNo) { ElMessage.warning('请先查询学生'); @@ -103,7 +128,7 @@ const handleReset = async () => { } try { await ElMessageBox.confirm( - `确认将学号「${student.value.studentNo}」(${student.value.name || ''})的登录密码重置为系统默认密码?`, + `确认将学号「${student.value.studentNo}」(${student.value.name || ''})的登录密码重置为随机临时密码?重置后该学生需重新登录。`, '重置密码确认', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' } ); @@ -112,8 +137,27 @@ const handleReset = async () => { } resetLoading.value = true; try { - await resetStudentPassword(student.value.studentNo); - ElMessage.success('密码已重置为默认密码'); + const res = await resetStudentPassword(student.value.studentNo); + const tempPwd = String(res.data ?? ''); + await ElMessageBox.alert( + `临时密码:${tempPwd}
请告知学生登录后立即修改密码,此密码仅显示一次。`, + '密码已重置', + { + type: 'success', + dangerouslyUseHTMLString: true, + showCancelButton: true, + distinguishCancelAndClose: true, + cancelButtonText: '复制密码', + confirmButtonText: '我已记录', + beforeClose: (action, _instance, done) => { + if (action === 'cancel') { + copyPassword(tempPwd); + return; + } + done(); + } + } + ); } finally { resetLoading.value = false; } diff --git a/问题整改/20260922-安全扫描修改.md b/问题整改/20260922-安全扫描修改.md index def803ef..24a920c4 100644 --- a/问题整改/20260922-安全扫描修改.md +++ b/问题整改/20260922-安全扫描修改.md @@ -8,6 +8,25 @@ | 优先级 | 项 | 处理结果 | |--------|----|----------| | P0 | `POST /rs/init/update-menu` 未授权改菜单 | **已整改** | +| P0 | `/rs/init/menu`、`/clear` 登录即可写删菜单 | **已整改**(类级 `system:menu:edit`,见 `P0-rs-init全接口权限锁死整改说明.md`) | +| P0 | `SysUserController` 权限注解被注释 | **已整改**(见 `P0-SysUser权限注解恢复整改说明.md`) | +| P0 | `SysRole` / `SysTenant` 权限注解被注释 | **已整改**(见 `P0-SysRole-SysTenant权限注解恢复整改说明.md`) | +| P0 | `SysOss` 管理越权 + 上传无白名单 | **已整改并复测通过**(见 `P0-SysOss权限与上传白名单整改说明.md`;学生角色仍误授 `system:oss:*` 属配权问题) | +| P1 | 招生 query/myStatus IDOR + 完整 PII | **已整改**(见 `P1-招生报名查询双因子与脱敏整改说明.md`) | +| P1 | 毕业生按学号/身份证 IDOR | **已整改**(见 `P1-毕业生信息权限恢复整改说明.md`) | +| P1 | 学生信息 list/详情/导出垂直越权 | **已整改并复测通过**(见 `P1-学生信息权限恢复整改说明.md`;学生角色仍误授 `studentMsg:*` 属配权问题) | +| P0 | Bucket 密码重置 + OSS 配置密钥面 | **已整改并复测通过**(见 `P0-Bucket密码与OSS配置权限恢复整改说明.md`) | +| P0 | `SysClient` 客户端密钥面权限注释 | **已整改并复测通过**(见 `P0-SysClient权限恢复整改说明.md`) | +| P1 | 贫困认定申请/审批垂直越权 | **已整改并复测通过**(见 `P1-贫困认定权限恢复整改说明.md`) | +| P1 | 心理健康测评/预约垂直越权 | **已整改并复测通过**(见 `P1-心理健康权限恢复整改说明.md`) | +| P1 | 奖助学金申请/配置垂直越权 | **已整改并复测通过**(见 `P1-奖助学金权限恢复整改说明.md`) | +| P1 | 勤工俭学岗位/申请垂直越权 | **已整改并复测通过**(见 `P1-勤工俭学权限恢复整改说明.md`;学生角色仍误授部分 `workStudy*` 属配权问题) | +| P1 | 出国研修/培训垂直越权 | **已整改并复测通过**(见 `P1-出国研修培训权限恢复整改说明.md`) | +| P1 | 宿舍/假期留宿/操行分等垂直越权 | **已整改并复测通过**(见 `P1-宿舍假期留宿权限恢复整改说明.md`;学生角色仍误授大量宿舍相关权限,属配权问题) | +| P1 | 招生就业模块(录取流程/实习等)垂直越权 | **已整改并一次复测通过**(见 `批量权限恢复与Redis清理说明.md`) | +| P1 | 业务模块剩余注释权限(财务/物业/社团等) | **源码已清零并一次复测通过**(见 `批量权限恢复与Redis清理说明.md`) | +| 运维 | Redis 登录 Session 坏缓存 | **已清理**(DB8 Authorization/online_tokens) | +| 配权 | 学生角色误授管理权限 | **开发库已执行并复测通过**(删约 99 条;见 `收紧学生角色过宽权限.sql`) | | P0 | Swagger `/api-docs`、`/swagger-ui/**` | **已下线** | | P0 | Demo `/demo/mail/**`、`/demo/queue/delayed/**` | **已下线(运行包不再引入)** | | P0 | 招生 5 个免登接口(含扫描点名 `query`) | **已整改**(须登录) | diff --git a/问题整改/20261026-安全扫描整改说明.md b/问题整改/20261026-安全扫描整改说明.md new file mode 100644 index 00000000..98a4fdab --- /dev/null +++ b/问题整改/20261026-安全扫描整改说明.md @@ -0,0 +1,60 @@ +# 2026-10-26 安全扫描整改说明 + +> 背景:杭州市网安对生产环境进行安全扫描。本次对严重、高危、中危问题做代码级整改;Snail-Job 生产已下线,不在本次范围。 +> 后端 `mvn -o compile` 全量通过;前端改动文件无新增类型错误。 + +## 一、上线前必须执行 + +| 项 | 说明 | +| --- | --- | +| SQL | `backend/script/sql/update/update_20261026_workflow_permission.sql`:新增工作流管理按钮权限(`workflow:definition:list/edit`、`workflow:instance:manage`、`workflow:task:manage`),并自动授给已拥有「流程定义 / 流程实例 / 待办任务」父菜单的角色。执行后相关用户需**重新登录**。 | +| 配置 | `application.yml` 新增 `security.cors-allowed-origins`(默认放行 `*.hzdyjsxy.cn` 与 localhost),若有其它域名访问后端须补充;`user.password.ipMaxRetryCount: 30`;`sa-token.jwt-secret-key` 已换成随机值。 | +| nginx | 必须向后端传递真实客户端 IP(`X-Forwarded-For` / `X-Real-IP`),否则按 IP 的登录失败锁定失效。 | + +## 二、整改清单 + +### 严重 + +| 问题 | 整改 | +| --- | --- | +| 工作流 SpEL 表达式注入可执行系统命令(`T(java.lang.Runtime)`) | 新增 `SafeSpelHelper` / `SafeExpressionStrategies`,启动时替换 warm-flow 内置 SpEL 策略:禁止类型引用 `T()`、`new`、反射与 `getClass/forName/exec/getBean` 等方法,仅允许对值类型与 `org.dromara` 业务 Bean 调用 | +| 工作流管理接口无权限控制 | 流程定义全部接口、实例运行/已完成/删除/激活/挂起/变量/作废、全部待办/已办/修改办理人加权限码;`/warm-flow/**` 设计器接口按读写加权限;删除实例、撤销流程要求必须是发起人或具备管理权限 | +| 社团活动列表 SQL 注入 `${params.dataScope}` | Mapper 改为 `#{}` + `foreach` 的类型化参数;Service 先清除请求中 `ds*`/`dataScope` 参数再写入 | + +### 高危 + +| 问题 | 整改 | +| --- | --- | +| 文件 ossId 越权访问 | 学生账号只能访问本人或教职工上传的文件;预签名链接最长 1 小时;异常不回显 | +| 登录喷洒 / 账号枚举 | 账号不存在与密码错误提示一致;同一公网 IP 失败 30 次锁定(内网 IP 不计数) | +| 调试接口暴露 | 删除 `testCoefficient`、`testVicePresidentData`、两个 `debug/permission`、`test/scholarshipTypesStructure`、`debug/sqlLogs`、`test-location`、勤工助学 `debug/{id}` | +| 新生录取名册导出无权限 | 加 `zsjy:enrollment:process:export` 或 `query` | +| 学生考核申请越权、篡改分数 | 学生只能增删改查本人申请;审核字段(教师评分、总分、审核人等)服务端清空;只允许状态 1/2/3 时修改 | +| 应聘越权 | `/apply`、`/my` 强制使用当前登录人;`/submit` 强制状态为 pending | +| 按学号重置学生密码 | 改为随机临时密码并踢下线;学院账号只能操作本学院学生;操作日志不记录响应 | +| 异常信息回显 SQL/堆栈 | `GlobalExceptionHandler.safeMessage` 过滤含 SQL、类名等技术信息的消息;64 处 `R.fail(e.getMessage())` 统一过滤 | +| 出国附件任意类型上传 | 扩展名白名单;Content-Type 按扩展名决定(系统 OSS 上传同步改造),不信任客户端 | + +### 中危 + +| 问题 | 整改 | +| --- | --- | +| SSE 任意用户群发 | 删除 `/resource/sse/send`、`/sendAll`(前端未使用,服务端推送不受影响) | +| CORS 回显任意 Origin 且允许携带凭证 | 仅放行配置的本校域名 | +| 心理咨询预约/爽约限制越权 | 无管理权限时 studentId 强制为本人 | +| 社团报名/签到越权查询 | 无管理权限时 userId/学号强制为本人 | +| 奖学金 `/my` 越权 | 无列表权限时 applicantId 强制为本人 | +| 参数配置读取初始密码 | `configKey` 仅公开白名单键;`sys.user.initPassword` 需用户新增/重置密码权限 | +| 学生信息完善批量赋值 | 状态、来源、创建人始终由服务端设置 | +| 前端存储型 XSS | 新增 `web/src/utils/htmlSafe.ts`;招聘岗位富文本清洗、简历 PDF 字段转义、报修图片预览去掉 innerHTML | +| 开放重定向 | 社交登录回调只允许跳转本校域名;报修附件下载仅重定向 http(s) 地址并安全编码文件名 | +| 日志泄露 | 生产关闭 MyBatis StdOut SQL 日志;登录监听不再打印 token;微信下载不再打印带 access_token 的 URL | +| JWT 默认密钥 | 更换为随机值 | + +## 三、遗留与运维事项 + +1. 默认密码 `Hzdyjsxs123456!@#`、`Hzdyjs123456!@#` 已出现在攻击日志中,需排查并强制修改仍使用默认密码的账号。 +2. 轮换 git 历史中出现过的数据库、Redis、监控、CAS 等密钥;确认数据库、Redis、监控端口不对公网开放。 +3. 实习日志 `calendar` / `monthly-statistics` 的 studentId 未做归属校验(需权限码,移动端未调用),后续补充。 +4. 社团成员 `approvalFlow/{memberId}`、用户导入结果弹窗 `dangerouslyUseHTMLString`(仅管理员)风险较低,后续处理。 +5. web 端 `xljk/consultation/calendar/debug.vue` 页面与勤工助学 `debug/statistics` 调用对应的后端接口已不存在,可删除前端代码。