Procházet zdrojové kódy

fix(security): 修复网安扫描相关严重/高危/中危漏洞并加固学生密码重置

关闭工作流 SpEL 命令执行与社团活动 SQL 注入,补齐越权、调试接口、登录喷洒和异常回显等问题;学生密码改为随机临时密码,Web/移动端支持复制。

Constraint: 流程发起/办理接口保持仅登录校验,避免影响请假审批
Rejected: 开启验证码 | Web/移动端登录页均未实现,开启会导致无法登录
Confidence: high
Scope-risk: broad
Not-tested: 生产环境与社团社长/指导老师视角
Co-authored-by: Cursor <cursoragent@cursor.com>
“wuzhen” před 4 dny
rodič
revize
da8d332fac
79 změnil soubory, kde provedl 1156 přidání a 547 odebrání
  1. 3 2
      backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CasController.java
  2. 4 4
      backend/ruoyi-admin/src/main/java/org/dromara/web/listener/UserActionListener.java
  3. 36 0
      backend/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java
  4. 4 2
      backend/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java
  5. 2 1
      backend/ruoyi-admin/src/main/resources/application-prod.yml
  6. 7 2
      backend/ruoyi-admin/src/main/resources/application.yml
  7. 1 0
      backend/ruoyi-admin/src/main/resources/i18n/messages.properties
  8. 1 0
      backend/ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  9. 1 0
      backend/ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  10. 42 0
      backend/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/MimeTypeUtils.java
  11. 1 1
      backend/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/handler/MybatisExceptionHandler.java
  12. 0 28
      backend/ruoyi-common/ruoyi-common-sse/src/main/java/org/dromara/common/sse/controller/SseController.java
  13. 5 3
      backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java
  14. 33 8
      backend/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java
  15. 3 12
      backend/ruoyi-modules/ruoyi-bzrkh/src/main/java/org/dromara/bzrkh/controller/TeacherAssessmentController.java
  16. 3 2
      backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwApprovalRecordController.java
  17. 11 5
      backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqPmRepairAttachmentController.java
  18. 4 3
      backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqSatSurveyController.java
  19. 5 79
      backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtScholarshipApplicationController.java
  20. 0 48
      backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationCollegeReviewController.java
  21. 2 46
      backend/ruoyi-modules/ruoyi-jcbt/src/main/java/org/dromara/jcbt/controller/JcbtWorkStudyApplicationController.java
  22. 6 6
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsApplicationController.java
  23. 4 3
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsInitController.java
  24. 2 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationAttachmentController.java
  25. 4 3
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java
  26. 2 35
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java
  27. 2 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java
  28. 6 5
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingApplicationController.java
  29. 0 27
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsTrainingRecordController.java
  30. 5 1
      backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasApplicationAttachmentServiceImpl.java
  31. 6 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityAttendanceController.java
  32. 3 2
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityRegistrationController.java
  33. 0 21
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubController.java
  34. 7 6
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java
  35. 2 1
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglVolunteerTeamController.java
  36. 26 92
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java
  37. 42 2
      backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml
  38. 5 4
      backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysBucketPasswordController.java
  39. 16 0
      backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysConfigController.java
  40. 23 8
      backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysFileAccessController.java
  41. 8 7
      backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/wechat/SysWeChatController.java
  42. 21 4
      backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java
  43. 2 2
      backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysWeChatServiceImpl.java
  44. 28 0
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowPermissionConstant.java
  45. 56 2
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/WarmFlowConfig.java
  46. 83 0
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeExpressionStrategies.java
  47. 98 0
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/config/expression/SafeSpelHelper.java
  48. 15 0
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwDefinitionController.java
  49. 8 0
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java
  50. 5 0
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwTaskController.java
  51. 16 0
      backend/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java
  52. 7 6
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStuColBatchClassController.java
  53. 2 1
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java
  54. 4 4
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentMsgController.java
  55. 3 3
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentMsgService.java
  56. 9 9
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgBatchServiceImpl.java
  57. 51 6
      backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentMsgServiceImpl.java
  58. 4 0
      backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationAppointmentController.java
  59. 5 0
      backend/ruoyi-modules/ruoyi-xljk/src/main/java/org/dromara/xljk/controller/XljkConsultationNoShowController.java
  60. 14 0
      backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhAssessmentApplicationApprovalMapper.java
  61. 62 0
      backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentApplicationApprovalServiceImpl.java
  62. 7 6
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/InternshipInfoController.java
  63. 3 2
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentInfoController.java
  64. 2 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyEnrollmentProcessController.java
  65. 5 4
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipDailyServiceImpl.java
  66. 1 1
      backend/script/sql/update/update_20261026_student_pwd_reset_menu.sql
  67. 51 0
      backend/script/sql/update/update_20261026_workflow_permission.sql
  68. 12 8
      docs/开发文档/API/学生密码重置API.md
  69. 1 1
      mobile/src/api/studentPwdReset.js
  70. 33 3
      mobile/src/pages/xg/xsjcxx/studentPwdReset/index.vue
  71. 1 1
      web/src/api/xg/xsjcxx/studentMsg/studentMsg/index.ts
  72. 7 0
      web/src/layout/components/SocialCallback/index.vue
  73. 62 0
      web/src/utils/htmlSafe.ts
  74. 4 1
      web/src/views/hq/pm/repairOrder/index.vue
  75. 8 7
      web/src/views/rs/index.vue
  76. 3 2
      web/src/views/rs/recruitment-portal.vue
  77. 47 3
      web/src/views/xg/xsjcxx/studentMsg/studentPwdReset/index.vue
  78. 19 0
      问题整改/20260922-安全扫描修改.md
  79. 60 0
      问题整改/20261026-安全扫描整改说明.md

+ 3 - 2
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()));
         }
     }
 }

+ 4 - 4
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);
     }
 
     /**

+ 36 - 0
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;
+    }
+
     /**
      * 校验租户
      *

+ 4 - 2
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<SysUser>().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);

+ 2 - 1
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
     # 开启缓存

+ 7 - 2
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:
       # 主键类型

+ 1 - 0
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=角色已封禁,请联系管理员

+ 1 - 0
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

+ 1 - 0
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=角色已封禁,请联系管理员

+ 42 - 0
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<String, String> 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");
+    }
+
 }

+ 1 - 1
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, "数据处理异常,请稍后重试或联系管理员");
     }
 
 }

+ 0 - 28
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<Void> 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<Void> send(String msg) {
-        sseEmitterManager.publishAll(msg);
-        return R.ok();
-    }
-
     /**
      * 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误
      */

+ 5 - 3
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<String> allowedOrigins) {
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
-        // 设置访问源地址
-        config.addAllowedOriginPattern("*");
+        // 设置访问源地址(仅允许配置的本校域名,禁止回显任意 Origin)
+        allowedOrigins.forEach(config::addAllowedOriginPattern);
         // 设置访问源请求头
         config.addAllowedHeader("*");
         // 设置访问源请求方法

+ 33 - 8
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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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, "请求参数格式错误");
     }
 
 }

+ 3 - 12
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<Object> testStudentCountCoefficient(@RequestParam("studentCount") int studentCount) {
-        BigDecimal coefficient = teacherAssessmentService.testStudentCountCoefficient(studentCount);
-        return R.ok("学生人数: " + studentCount + ", 计算系数: " + coefficient);
-    }
-
     /**
      * 查询班级学生违纪记录和奖励记录
      */

+ 3 - 2
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()));
         }
     }
 

+ 11 - 5
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);
         }

+ 4 - 3
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()));
         }
     }
 

+ 5 - 79
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<List<JcbtScholarshipApplicationVo>> 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<Map<String, Object>> testScholarshipTypesStructure(@Parameter(description = "批次ID") @RequestParam("batchId") Long batchId,
-                                                               @Parameter(description = "班级ID") @RequestParam(value = "classId", required = false) Long classId) {
-        Map<String, Object> 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<Map<String, Object>> 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<String, Object> 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);
-    }
 }

+ 0 - 48
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<Map<String, Object>> debugPermission() {
-        Map<String, Object> 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
      * 通过查询学生信息或用户部门信息获取

+ 2 - 46
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<Map<String, Object>> debugQuery(@PathVariable("id") Long id) {
-        log.info("调试查询接口,ID: {}", id);
-
-        Map<String, Object> 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<JcbtWorkStudyApplication> 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()));
         }
     }
 

+ 6 - 6
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<Void> 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<RsApplication> myApplications(@PathVariable("applicantId") Long applicantId, PageQuery pageQuery) {
-        List<RsApplication> list = applicationService.selectApplicationsByApplicant(applicantId);
+        // 路径参数仅为兼容旧前端保留,只返回当前登录人的申请
+        List<RsApplication> list = applicationService.selectApplicationsByApplicant(LoginHelper.getUserId());
         return TableDataInfo.build(list);
     }
 
@@ -298,10 +301,7 @@ public class RsApplicationController extends BaseController {
     @PostMapping("/submit")
     public R<Map<String, Object>> 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()));
         }
     }
 

+ 4 - 3
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()));
         }
     }
 } 

+ 2 - 1
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()));
         }
     }
 

+ 4 - 3
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()));
         }
     }
 

+ 2 - 35
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<Map<String, Object>> debugPermission(RsOverseasApprovalQuery query) {
-        Map<String, Object> 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<RsOverseasApprovalVo> 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()));
         }
     }
 }

+ 2 - 1
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()));
         }
     }
 

+ 6 - 5
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)) {

+ 0 - 27
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<List<RsTrainingRecord>> testLocationSearch(@RequestParam("location") String location) {
-        log.info("测试培训地点搜索 - location参数: {}", location);
-
-        // 创建查询对象
-        RsTrainingRecord record = new RsTrainingRecord();
-        record.setLocation(location);
-
-        log.info("创建的查询对象 - location: {}", record.getLocation());
-
-        // 直接调用Mapper方法
-        List<RsTrainingRecord> 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);
-    }
-
     /**
      * 导出培训记录列表
      */

+ 5 - 1
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() 

+ 6 - 0
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<StglActivityAttendanceVo> 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)) {

+ 3 - 2
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<Boolean> 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<StglActivityRegistrationVo> 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));
     }
 

+ 0 - 21
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<StglClubVo> 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);
-    }
-
     /**
      * 启用/禁用社团
      */

+ 7 - 6
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()));
         }
     }
 } 

+ 2 - 1
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()));
         }
     }
 

+ 26 - 92
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java

@@ -683,54 +683,32 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
      * @param bo 查询条件
      */
     private void applyDataScope(StglActivityBo bo) {
-        try {
-            // 获取当前登录用户信息
-            Long currentUserId = LoginHelper.getUserId();
-            if (currentUserId == null) {
-                return;
-            }
-            
-            Set<String> 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<Long> userAdvisorClubIds = getUserAdvisorClubIds(currentUserId);
-                
-                // 获取用户作为指导老师的志愿者队伍ID列表
-                List<Long> userAdvisorTeamIds = getUserAdvisorTeamIds(currentUserId);
-                
-                // 获取用户作为指导老师的活动ID列表
-                List<Long> 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<String> 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<StglActivityMapper, Stg
         return activityIds;
     }
 
-    /**
-     * 构建指导老师数据权限过滤条件
-     *
-     * @param userAdvisorClubIds 用户作为指导老师的社团ID列表
-     * @param userAdvisorTeamIds 用户作为指导老师的志愿者队伍ID列表
-     * @param userAdvisorActivityIds 用户作为指导老师的活动ID列表
-     * @return 数据权限过滤条件
-     */
-    private String buildInstructorDataScope(List<Long> userAdvisorClubIds, List<Long> userAdvisorTeamIds, List<Long> userAdvisorActivityIds) {
-        List<String> 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<ActivityRatingVo> getActivityRatings(Long activityId) {
         if (activityId == null) {

+ 42 - 2
backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml

@@ -207,7 +207,27 @@
                 AND sa.related_club_id = #{relatedClubId}
             </if>
             <!-- 数据权限:非管理员只能查看自己创建的活动 -->
-            ${params.dataScope}
+            <if test="params != null">
+            <if test="params.dsCreateBy != null">
+                AND sa.create_by = #{params.dsCreateBy}
+            </if>
+            <if test="params.dsInstructor != null">
+                AND (1 = 0
+                <if test="params.dsClubIds != null and params.dsClubIds.size() > 0">
+                    OR (sa.related_type = 1 AND sa.related_club_id IN
+                    <foreach collection="params.dsClubIds" item="dsId" open="(" separator="," close=")">#{dsId}</foreach>)
+                </if>
+                <if test="params.dsTeamIds != null and params.dsTeamIds.size() > 0">
+                    OR (sa.related_type = 2 AND sa.related_club_id IN
+                    <foreach collection="params.dsTeamIds" item="dsId" open="(" separator="," close=")">#{dsId}</foreach>)
+                </if>
+                <if test="params.dsActivityIds != null and params.dsActivityIds.size() > 0">
+                    OR sa.id IN
+                    <foreach collection="params.dsActivityIds" item="dsId" open="(" separator="," close=")">#{dsId}</foreach>
+                </if>
+                )
+            </if>
+            </if>
         </where>
         ORDER BY sa.create_time DESC
     </select>
@@ -366,7 +386,27 @@
                 )
             </if>
             <!-- 数据权限:非管理员只能查看自己创建的活动 -->
-            ${activity.params.dataScope}
+            <if test="activity.params != null">
+            <if test="activity.params.dsCreateBy != null">
+                AND sa.create_by = #{activity.params.dsCreateBy}
+            </if>
+            <if test="activity.params.dsInstructor != null">
+                AND (1 = 0
+                <if test="activity.params.dsClubIds != null and activity.params.dsClubIds.size() > 0">
+                    OR (sa.related_type = 1 AND sa.related_club_id IN
+                    <foreach collection="activity.params.dsClubIds" item="dsId" open="(" separator="," close=")">#{dsId}</foreach>)
+                </if>
+                <if test="activity.params.dsTeamIds != null and activity.params.dsTeamIds.size() > 0">
+                    OR (sa.related_type = 2 AND sa.related_club_id IN
+                    <foreach collection="activity.params.dsTeamIds" item="dsId" open="(" separator="," close=")">#{dsId}</foreach>)
+                </if>
+                <if test="activity.params.dsActivityIds != null and activity.params.dsActivityIds.size() > 0">
+                    OR sa.id IN
+                    <foreach collection="activity.params.dsActivityIds" item="dsId" open="(" separator="," close=")">#{dsId}</foreach>
+                </if>
+                )
+            </if>
+            </if>
         </where>
         ORDER BY sa.create_time DESC
     </select>

+ 5 - 4
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()));
         }
     }
 }

+ 16 - 0
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<String> 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<String> 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));
     }
 

+ 23 - 8
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,8 +259,16 @@ 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);
     }
 
     /**

+ 8 - 7
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()));
         }
     }
 }

+ 21 - 4
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());
+    }
+
     /**
      * 记录文件访问日志
      *

+ 2 - 2
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();

+ 28 - 0
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";
+
+}

+ 56 - 2
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 <T> void removeBuiltinSpel(List<ExpressionStrategy<T>> 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/**");
+    }
+
+}

+ 83 - 0
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<String, Object> variable) {
+            expression = expression.replace("$", "#");
+            for (Map.Entry<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> variable) {
+            return SafeSpelHelper.parseExpression(expression, variable);
+        }
+    }
+}

+ 98 - 0
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;
+
+/**
+ * 受限的流程表达式解析器
+ * <p>
+ * 替代 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<String> 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<String, Object> 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<TypeDescriptor> 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;
+        }
+    }
+}

+ 15 - 0
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<FlowDefinitionVo> 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<FlowDefinitionVo> 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<Definition> 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<Boolean> 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<Void> remove(@PathVariable("ids") List<Long> 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<Boolean> 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<String> 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<Boolean> active(@PathVariable("id") Long id, @RequestParam("active") boolean active) {

+ 8 - 0
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<FlowInstanceVo> 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<FlowInstanceVo> 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<Void> deleteByBusinessIds(@PathVariable("businessIds") List<Long> 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<Boolean> 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<Map<String, Object>> 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<Boolean> invalid(@Validated @RequestBody FlowInvalidBo bo) {
         return R.ok(flwInstanceService.processInvalid(bo));

+ 5 - 0
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<FlowTaskVo> 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<FlowHisTaskVo> 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<Void> updateAssignee(@RequestBody List<Long> taskIdList, @PathVariable("userId") String userId) {
         return toAjax(flwTaskService.updateAssignee(taskIdList, userId));

+ 16 - 0
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<Long, Definition> 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("无权操作他人发起的流程");
+        }
+    }
+
     /**
      * 获取当前登陆人发起的流程实例
      *

+ 7 - 6
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<Long> 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<XgStuColBatchClassVo> 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()));
         }
     }
 

+ 2 - 1
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()));
         }
     }
 }

+ 4 - 4
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<Void> resetStudentPassword(
+    public R<String> resetStudentPassword(
         @NotBlank(message = "学号不能为空") @PathVariable("studentNo") String studentNo) {
-        return toAjax(xgStudentMsgService.resetStudentPasswordByStudentNo(studentNo));
+        return R.ok("操作成功", xgStudentMsgService.resetStudentPasswordByStudentNo(studentNo));
     }
 
     /**

+ 3 - 3
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);
 }

+ 9 - 9
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;
 

+ 51 - 6
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;
     }
 }

+ 4 - 0
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<Boolean> 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);

+ 5 - 0
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<Map<String, Object>> 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));
     }
 } 

+ 14 - 0
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<org.dromara.xskh.domain.vo.XskhAssessmentSelfScoreVo> 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);
+
 }

+ 62 - 0
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<XskhAssessmentSelfScoreVo> 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;

+ 7 - 6
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()));
         }
     }
 }

+ 3 - 2
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<ZsjyEnrollmentDeptVo> 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<ZsjyEnrollmentDeptVo> majors = enrollmentMajorService.selectOpenMajorsByCollegeId(collegeId);
             return R.ok(majors);
         } catch (Exception e) {
-            return R.fail("获取专业列表异常: " + e.getMessage());
+            return R.fail("获取专业列表异常: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
         }
     }
 }

+ 2 - 0
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) {

+ 5 - 4
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()));
         }
     }
 

+ 1 - 1
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!@#
+-- 重置后返回随机临时密码(仅显示一次),并踢出该学生已登录会话
 -- 执行:开发/生产库均可幂等执行;执行后给角色勾选菜单或跑下方角色授权段
 -- =============================================================================
 

+ 51 - 0
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;

+ 12 - 8
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 |
 
 ---

+ 1 - 1
mobile/src/api/studentPwdReset.js

@@ -15,7 +15,7 @@ export const queryStudentForPwdReset = (studentNo) => {
 };
 
 /**
- * 按学号重置学生登录密码为固定默认值
+ * 按学号重置学生登录密码为随机临时密码,data 为临时密码
  * @param {string} studentNo 学号
  */
 export const resetStudentPassword = (studentNo) => {

+ 33 - 3
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 {

+ 1 - 1
web/src/api/xg/xsjcxx/studentMsg/studentMsg/index.ts

@@ -137,7 +137,7 @@ export const queryStudentForPwdReset = (studentNo: string): AxiosPromise<Student
 };
 
 /**
- * 按学号重置学生密码为固定默认值
+ * 按学号重置学生密码为随机临时密码,data 为临时密码
  */
 export const resetStudentPassword = (studentNo: string) => {
   return request({

+ 7 - 0
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();

+ 62 - 0
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, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
+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(`<div>${String(html)}</div>`, '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;
+}

+ 4 - 1
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 = `<el-image :src="'${attachment.filePath}'" :preview-src-list="previewList" style="display: none;"></el-image>`;
+  const previewImg = document.createElement('img');
+  previewImg.src = attachment.filePath;
+  previewImg.style.display = 'none';
+  imageViewer.appendChild(previewImg);
   document.body.appendChild(imageViewer);
 
   // 触发预览

+ 8 - 7
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() {
       
       <div style="margin-bottom: 30px;">
         <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">基本信息</h2>
-        <p><strong>姓名:</strong>${selectedRecord.applicantName || '未提供'}</p>
-        <p><strong>应聘职位:</strong>${selectedRecord.jobTitle || '未提供'}</p>
-        <p><strong>联系电话:</strong>${selectedRecord.contactPhone || '未提供'}</p>
-        <p><strong>联系邮箱:</strong>${selectedRecord.contactEmail || '未提供'}</p>
+        <p><strong>姓名:</strong>${escapeHtml(selectedRecord.applicantName || '未提供')}</p>
+        <p><strong>应聘职位:</strong>${escapeHtml(selectedRecord.jobTitle || '未提供')}</p>
+        <p><strong>联系电话:</strong>${escapeHtml(selectedRecord.contactPhone || '未提供')}</p>
+        <p><strong>联系邮箱:</strong>${escapeHtml(selectedRecord.contactEmail || '未提供')}</p>
       </div>
       
       <div style="margin-bottom: 30px;">
         <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">教育背景</h2>
-        <p><strong>最高学历:</strong>${selectedRecord.education || '未提供'}</p>
+        <p><strong>最高学历:</strong>${escapeHtml(selectedRecord.education || '未提供')}</p>
       </div>
       
       <div style="margin-bottom: 30px;">
         <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">工作经验</h2>
-        <p style="white-space: pre-wrap;">${selectedRecord.experience || '未提供工作经验'}</p>
+        <p style="white-space: pre-wrap;">${escapeHtml(selectedRecord.experience || '未提供工作经验')}</p>
       </div>
       
       <div style="margin-bottom: 30px;">
         <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">自我介绍</h2>
-        <p style="white-space: pre-wrap;">${selectedRecord.selfDescription || '未提供自我介绍'}</p>
+        <p style="white-space: pre-wrap;">${escapeHtml(selectedRecord.selfDescription || '未提供自我介绍')}</p>
       </div>
       
       <div style="margin-top: 50px; text-align: right; font-size: 12px; color: #999;">

+ 3 - 2
web/src/views/rs/recruitment-portal.vue

@@ -164,12 +164,12 @@
         <div class="detail-content">
           <div class="content-section">
             <h4>岗位职责</h4>
-            <div class="content-text" v-html="currentJob.description"></div>
+            <div class="content-text" v-html="sanitizeHtml(currentJob.description)"></div>
           </div>
 
           <div class="content-section">
             <h4>任职要求</h4>
-            <div class="content-text" v-html="currentJob.requirements"></div>
+            <div class="content-text" v-html="sanitizeHtml(currentJob.requirements)"></div>
           </div>
 
           <div class="content-section">
@@ -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);

+ 47 - 3
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}<br/>请告知学生登录后立即修改密码,此密码仅显示一次。`,
+      '密码已重置',
+      {
+        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;
   }

+ 19 - 0
问题整改/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`) | **已整改**(须登录) |

+ 60 - 0
问题整改/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` 调用对应的后端接口已不存在,可删除前端代码。