Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09593b221b | ||
|
|
eeae0af978 | ||
|
|
937cbb6bbd | ||
|
|
a170cc6a48 | ||
|
|
24a1d7f745 | ||
|
|
fa6bf7aea2 | ||
|
|
9b1ce3c4b1 | ||
|
|
5b4be04645 | ||
|
|
da8d332fac | ||
|
|
845c7fd942 | ||
|
|
df4c21bd0d | ||
|
|
0765f00458 | ||
|
|
bea76c9e11 | ||
|
|
2550e9713d | ||
|
|
4e1ca614d4 | ||
|
|
291de33d54 | ||
|
|
902b04a21a | ||
|
|
ba6a6af131 | ||
|
|
5ab54912a5 | ||
|
|
d6af2f1892 | ||
|
|
dcd29040de |
No files matched your search
@@ -933,15 +933,16 @@ FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memor
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:prod": "node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode production",
|
"build:prod": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode production",
|
||||||
"build:dev": "node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode development"
|
"build:dev": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode development"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 参数说明
|
#### 参数说明
|
||||||
- `--max-old-space-size=8192`:设置堆内存限制为8GB
|
- `--max-old-space-size=6144`:设置堆内存限制为 6GB
|
||||||
- 可根据项目规模和机器内存调整数值(4096=4GB, 8192=8GB)
|
- 可根据项目规模和机器内存调整数值(4096=4GB, 6144=6GB, 8192=8GB)
|
||||||
|
- 2026-10-21:4096MB 会在生产构建 transform 约 2400 个模块时 OOM。当前开发机物理内存为 8GB,脚本使用 6144MB,避免堆上限等于整机内存
|
||||||
|
|
||||||
#### 适用场景
|
#### 适用场景
|
||||||
- 大型前端项目构建
|
- 大型前端项目构建
|
||||||
|
|||||||
+25
-22
@@ -1,5 +1,5 @@
|
|||||||
# 仅 tag 触发:顺序打包 backend / web / mobile-h5
|
# 仅 tag 触发:在内网 Runner 顺序打包 backend / mobile-h5 / web
|
||||||
# 产物:/root/hzdyjs-releases/<tag>/{backend,web,mobile-h5}-<tag>.*
|
# 产物:/root/hzdyjs-releases/<tag>/{backend,mobile-h5,web}-<tag>.*
|
||||||
# Maven 缓存:宿主机 /root/maven-repo → 容器 /maven-repo(由 runner DRONE_RUNNER_VOLUMES 注入)
|
# Maven 缓存:宿主机 /root/maven-repo → 容器 /maven-repo(由 runner DRONE_RUNNER_VOLUMES 注入)
|
||||||
# 发版目录:/root/hzdyjs-releases → /hzdyjs-releases(同上,勿在本文件重复挂载)
|
# 发版目录:/root/hzdyjs-releases → /hzdyjs-releases(同上,勿在本文件重复挂载)
|
||||||
|
|
||||||
@@ -35,24 +35,6 @@ steps:
|
|||||||
- cp -v ruoyi-admin/target/ruoyi-admin.jar "/hzdyjs-releases/${DRONE_TAG}/backend-${DRONE_TAG}.jar"
|
- cp -v ruoyi-admin/target/ruoyi-admin.jar "/hzdyjs-releases/${DRONE_TAG}/backend-${DRONE_TAG}.jar"
|
||||||
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/backend-${DRONE_TAG}.jar"
|
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/backend-${DRONE_TAG}.jar"
|
||||||
|
|
||||||
- name: package-web
|
|
||||||
# 无 package-lock(被 gitignore);Node20 需 --legacy-peer-deps 规避 npm edgesOut
|
|
||||||
# 低内存模式:串行 transform、关 gzip 插件、3G 堆(见 web build:ci)
|
|
||||||
image: node:20-bookworm
|
|
||||||
environment:
|
|
||||||
NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
|
|
||||||
commands:
|
|
||||||
- set -eu
|
|
||||||
- echo "==> web tag=${DRONE_TAG} npm=$(npm -v) node=$(node -v) (build:ci low-mem)"
|
|
||||||
- mkdir -p "/hzdyjs-releases/${DRONE_TAG}"
|
|
||||||
- cd web
|
|
||||||
- npm install --legacy-peer-deps --no-audit --no-fund
|
|
||||||
# build:ci 内置 CI/VITE_LOW_MEM/关 gzip + 5G 堆(勿在 Drone environment 再声明自定义变量,会被预处理掏空)
|
|
||||||
- npm run build:ci
|
|
||||||
- test -f dist/index.html
|
|
||||||
- tar -C dist -czf "/hzdyjs-releases/${DRONE_TAG}/web-${DRONE_TAG}.tar.gz" .
|
|
||||||
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/web-${DRONE_TAG}.tar.gz"
|
|
||||||
|
|
||||||
- name: package-mobile-h5
|
- name: package-mobile-h5
|
||||||
image: node:20-bookworm
|
image: node:20-bookworm
|
||||||
environment:
|
environment:
|
||||||
@@ -64,10 +46,31 @@ steps:
|
|||||||
- corepack enable
|
- corepack enable
|
||||||
- corepack prepare pnpm@9 --activate
|
- corepack prepare pnpm@9 --activate
|
||||||
- cd mobile
|
- cd mobile
|
||||||
- pnpm config set registry https://registry.npmmirror.com
|
# H5 构建不需要 Chromium
|
||||||
- pnpm install --frozen-lockfile
|
- export PUPPETEER_SKIP_DOWNLOAD=1 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
|
- pnpm install --frozen-lockfile --registry https://registry.npmmirror.com
|
||||||
- pnpm run build
|
- pnpm run build
|
||||||
- test -d dist/build/h5
|
- test -d dist/build/h5
|
||||||
- tar -C dist/build/h5 -czf "/hzdyjs-releases/${DRONE_TAG}/mobile-h5-${DRONE_TAG}.tar.gz" .
|
- tar -C dist/build/h5 -czf "/hzdyjs-releases/${DRONE_TAG}/mobile-h5-${DRONE_TAG}.tar.gz" .
|
||||||
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/"
|
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/"
|
||||||
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/mobile-h5-${DRONE_TAG}.tar.gz"
|
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/mobile-h5-${DRONE_TAG}.tar.gz"
|
||||||
|
|
||||||
|
- name: package-web
|
||||||
|
image: node:20-bookworm
|
||||||
|
environment:
|
||||||
|
NPM_CONFIG_REGISTRY: https://registry.npmmirror.com
|
||||||
|
commands:
|
||||||
|
- set -eu
|
||||||
|
- echo "==> web tag=${DRONE_TAG} commit=${DRONE_COMMIT}"
|
||||||
|
- test -n "${DRONE_TAG}"
|
||||||
|
- mkdir -p "/hzdyjs-releases/${DRONE_TAG}"
|
||||||
|
- cd web
|
||||||
|
# 固定兼容 Node 20 的 npm 11,避开镜像内 npm 10 的 edgesOut 解析异常。
|
||||||
|
- npm install --global npm@11.21.1 --no-audit --no-fund --registry https://registry.npmmirror.com
|
||||||
|
- npm install --no-audit --no-fund --registry https://registry.npmmirror.com
|
||||||
|
# build:ci 使用 6 GB Node 堆并限制 Vite 并行,避免与其他打包步骤叠加内存。
|
||||||
|
- npm run build:ci
|
||||||
|
- test -s dist/index.html
|
||||||
|
- tar -C dist -czf "/hzdyjs-releases/${DRONE_TAG}/web-${DRONE_TAG}.tar.gz" .
|
||||||
|
- ls -lh "/hzdyjs-releases/${DRONE_TAG}/web-${DRONE_TAG}.tar.gz"
|
||||||
|
- sha256sum "/hzdyjs-releases/${DRONE_TAG}/web-${DRONE_TAG}.tar.gz"
|
||||||
+1
-1
@@ -80,7 +80,7 @@ mobile/yarn-debug.log*
|
|||||||
mobile/yarn-error.log*
|
mobile/yarn-error.log*
|
||||||
mobile/package-lock.json
|
mobile/package-lock.json
|
||||||
mobile/yarn.lock
|
mobile/yarn.lock
|
||||||
mobile/pnpm-lock.yaml
|
# mobile/pnpm-lock.yaml 纳入仓库,保证 CI 与本地依赖树一致
|
||||||
|
|
||||||
# Vue 自动生成的文件
|
# Vue 自动生成的文件
|
||||||
web/auto-imports.d.ts
|
web/auto-imports.d.ts
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.web.controller;
|
package org.dromara.web.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
@@ -178,14 +179,14 @@ public class CasController {
|
|||||||
return R.fail(ue.getMessage());
|
return R.fail(ue.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("处理 CAS profile 失败", e);
|
log.error("处理 CAS profile 失败", e);
|
||||||
return R.fail("处理 CAS profile 失败: " + e.getMessage());
|
return R.fail("处理 CAS profile 失败: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return R.fail("无法解析 CAS access_token");
|
return R.fail("无法解析 CAS access_token");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("请求 CAS accessToken 失败", e);
|
log.error("请求 CAS accessToken 失败", e);
|
||||||
return R.fail("请求 CAS accessToken 失败: " + e.getMessage());
|
return R.fail("请求 CAS accessToken 失败: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-16
@@ -73,7 +73,27 @@ public class UserActionListener implements SaTokenListener {
|
|||||||
SpringUtils.context().publishEvent(logininforEvent);
|
SpringUtils.context().publishEvent(logininforEvent);
|
||||||
// 更新登录信息
|
// 更新登录信息
|
||||||
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
|
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
|
||||||
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
|
log.info("user doLogin, userId:{}", loginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理在线用户缓存。JWT 密钥变更后旧 token 无法 getExtra,绝不能抛异常打断登录流程
|
||||||
|
* (登录时 logoutByMaxLoginCount 会回调 doLogout/doReplaced)。
|
||||||
|
*/
|
||||||
|
private void clearOnlineCache(Object loginId, String tokenValue) {
|
||||||
|
try {
|
||||||
|
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
|
||||||
|
TenantHelper.dynamic(tenantId, () ->
|
||||||
|
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("清理在线缓存时 token 无法解析(多为 JWT 密钥变更后的旧会话), userId={}, err={}",
|
||||||
|
loginId, e.getMessage());
|
||||||
|
try {
|
||||||
|
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,11 +101,8 @@ public class UserActionListener implements SaTokenListener {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void doLogout(String loginType, Object loginId, String tokenValue) {
|
public void doLogout(String loginType, Object loginId, String tokenValue) {
|
||||||
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
|
clearOnlineCache(loginId, tokenValue);
|
||||||
TenantHelper.dynamic(tenantId, () -> {
|
log.info("user doLogout, userId:{}", loginId);
|
||||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
|
||||||
});
|
|
||||||
log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,11 +110,8 @@ public class UserActionListener implements SaTokenListener {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void doKickout(String loginType, Object loginId, String tokenValue) {
|
public void doKickout(String loginType, Object loginId, String tokenValue) {
|
||||||
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
|
clearOnlineCache(loginId, tokenValue);
|
||||||
TenantHelper.dynamic(tenantId, () -> {
|
log.info("user doKickout, userId:{}", loginId);
|
||||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
|
||||||
});
|
|
||||||
log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,11 +119,8 @@ public class UserActionListener implements SaTokenListener {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void doReplaced(String loginType, Object loginId, String tokenValue) {
|
public void doReplaced(String loginType, Object loginId, String tokenValue) {
|
||||||
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
|
clearOnlineCache(loginId, tokenValue);
|
||||||
TenantHelper.dynamic(tenantId, () -> {
|
log.info("user doReplaced, userId:{}", loginId);
|
||||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
|
||||||
});
|
|
||||||
log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ public class SysLoginService {
|
|||||||
@Value("${user.password.lockTime}")
|
@Value("${user.password.lockTime}")
|
||||||
private Integer lockTime;
|
private Integer lockTime;
|
||||||
|
|
||||||
|
@Value("${user.password.ipMaxRetryCount:30}")
|
||||||
|
private Integer ipMaxRetryCount;
|
||||||
|
|
||||||
private final ISysTenantService tenantService;
|
private final ISysTenantService tenantService;
|
||||||
private final ISysPermissionService permissionService;
|
private final ISysPermissionService permissionService;
|
||||||
private final ISysSocialService sysSocialService;
|
private final ISysSocialService sysSocialService;
|
||||||
@@ -200,7 +203,23 @@ public class SysLoginService {
|
|||||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
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 (supplier.get()) {
|
||||||
|
if (ipErrorKey != null) {
|
||||||
|
int ipErrorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(ipErrorKey), 0);
|
||||||
|
RedisUtils.setCacheObject(ipErrorKey, ipErrorNumber + 1, Duration.ofMinutes(lockTime));
|
||||||
|
}
|
||||||
// 错误次数递增
|
// 错误次数递增
|
||||||
errorNumber++;
|
errorNumber++;
|
||||||
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
||||||
@@ -219,6 +238,23 @@ public class SysLoginService {
|
|||||||
RedisUtils.deleteObject(errorKey);
|
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
@@ -64,7 +64,9 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
|||||||
}
|
}
|
||||||
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
||||||
SysUserVo user = loadUserByUsername(username);
|
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
|
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||||
return loginService.buildLoginUser(user);
|
return loginService.buildLoginUser(user);
|
||||||
});
|
});
|
||||||
@@ -112,7 +114,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
|||||||
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
|
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
|
||||||
if (ObjectUtil.isNull(user)) {
|
if (ObjectUtil.isNull(user)) {
|
||||||
log.info("登录用户:{} 不存在.", username);
|
log.info("登录用户:{} 不存在.", username);
|
||||||
throw new UserException("user.not.exists", username);
|
return null;
|
||||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||||
log.info("登录用户:{} 已被停用.", username);
|
log.info("登录用户:{} 已被停用.", username);
|
||||||
throw new UserException("user.blocked", username);
|
throw new UserException("user.blocked", username);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
--- # Sa-Token(生产显式固定 JWT 密钥,禁止使用随机密钥,否则已登录会话会整体失效)
|
||||||
|
sa-token:
|
||||||
|
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
|
||||||
|
|
||||||
--- # 临时文件存储位置 避免临时文件被系统清理报错
|
--- # 临时文件存储位置 避免临时文件被系统清理报错
|
||||||
spring.servlet.multipart.location: /ruoyi/server/temp
|
spring.servlet.multipart.location: /ruoyi/server/temp
|
||||||
|
|
||||||
@@ -348,7 +352,8 @@ logging:
|
|||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
configuration:
|
configuration:
|
||||||
# 生产环境关闭 SQL 日志打印,避免性能影响和敏感信息泄露
|
# 生产环境关闭 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
|
map-underscore-to-camel-case: true
|
||||||
# 开启缓存
|
# 开启缓存
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ user:
|
|||||||
maxRetryCount: 5
|
maxRetryCount: 5
|
||||||
# 密码锁定时间(默认10分钟)
|
# 密码锁定时间(默认10分钟)
|
||||||
lockTime: 10
|
lockTime: 10
|
||||||
|
# 同一公网IP在锁定时间内允许的最大登录失败次数(内网IP不计,0为关闭)
|
||||||
|
ipMaxRetryCount: 30
|
||||||
|
|
||||||
# Spring配置
|
# Spring配置
|
||||||
spring:
|
spring:
|
||||||
@@ -174,7 +176,8 @@ mybatis-plus:
|
|||||||
typeAliasesPackage: org.dromara.**.domain
|
typeAliasesPackage: org.dromara.**.domain
|
||||||
# 启用SQL日志输出,用于调试数据库插入问题
|
# 启用SQL日志输出,用于调试数据库插入问题
|
||||||
configuration:
|
configuration:
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
# 生产禁止将完整 SQL 及参数(身份证号、手机号等)打印到控制台,需排查时在 dev 环境开启
|
||||||
|
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
global-config:
|
global-config:
|
||||||
dbConfig:
|
dbConfig:
|
||||||
# 主键类型
|
# 主键类型
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ user.not.exists=对不起, 您的账号:{0} 不存在.
|
|||||||
user.password.not.match=用户不存在/密码错误
|
user.password.not.match=用户不存在/密码错误
|
||||||
user.password.retry.limit.count=密码输入错误{0}次
|
user.password.retry.limit.count=密码输入错误{0}次
|
||||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||||
|
user.ip.retry.limit.exceed=登录失败次数过多,请{0}分钟后再试
|
||||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||||
role.blocked=角色已封禁,请联系管理员
|
role.blocked=角色已封禁,请联系管理员
|
||||||
|
|||||||
@@ -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.not.match=User does not exist/Password error
|
||||||
user.password.retry.limit.count=Password input error {0} times
|
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.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.password.delete=Sorry, your account:{0} has been deleted
|
||||||
user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator
|
user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator
|
||||||
role.blocked=Role disabled,please contact administrators
|
role.blocked=Role disabled,please contact administrators
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ user.not.exists=对不起, 您的账号:{0} 不存在.
|
|||||||
user.password.not.match=用户不存在/密码错误
|
user.password.not.match=用户不存在/密码错误
|
||||||
user.password.retry.limit.count=密码输入错误{0}次
|
user.password.retry.limit.count=密码输入错误{0}次
|
||||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||||
|
user.ip.retry.limit.exceed=登录失败次数过多,请{0}分钟后再试
|
||||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||||
role.blocked=角色已封禁,请联系管理员
|
role.blocked=角色已封禁,请联系管理员
|
||||||
|
|||||||
+42
@@ -37,4 +37,46 @@ public class MimeTypeUtils {
|
|||||||
// pdf
|
// pdf
|
||||||
"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
@@ -41,7 +41,7 @@ public class MybatisExceptionHandler {
|
|||||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
|
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
|
||||||
}
|
}
|
||||||
log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
|
log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
|
||||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message);
|
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "数据处理异常,请稍后重试或联系管理员");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+11
@@ -3,6 +3,7 @@ package org.dromara.common.satoken.handler;
|
|||||||
import cn.dev33.satoken.exception.NotLoginException;
|
import cn.dev33.satoken.exception.NotLoginException;
|
||||||
import cn.dev33.satoken.exception.NotPermissionException;
|
import cn.dev33.satoken.exception.NotPermissionException;
|
||||||
import cn.dev33.satoken.exception.NotRoleException;
|
import cn.dev33.satoken.exception.NotRoleException;
|
||||||
|
import cn.dev33.satoken.jwt.exception.SaJwtException;
|
||||||
import cn.hutool.http.HttpStatus;
|
import cn.hutool.http.HttpStatus;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -49,4 +50,14 @@ public class SaTokenExceptionHandler {
|
|||||||
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
|
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 签名无效/解析失败(常见于密钥变更后残留旧 token)
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(SaJwtException.class)
|
||||||
|
public R<Void> handleSaJwtException(SaJwtException e, HttpServletRequest request) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.error("请求地址'{}',JWT校验失败'{}',无法访问系统资源", requestURI, e.getMessage());
|
||||||
|
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "登录状态已失效,请重新登录");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+11
-1
@@ -59,9 +59,19 @@ public class SecurityConfig implements WebMvcConfigurer {
|
|||||||
StpUtil.checkLogin();
|
StpUtil.checkLogin();
|
||||||
|
|
||||||
// 检查 header 与 param 里的 clientid 与 token 里的是否一致
|
// 检查 header 与 param 里的 clientid 与 token 里的是否一致
|
||||||
|
// jwt 密钥切换后旧 token 会在 getExtra 时抛 SaJwtException,转为未登录避免页面报“未知异常”
|
||||||
String headerCid = request.getHeader(LoginHelper.CLIENT_KEY);
|
String headerCid = request.getHeader(LoginHelper.CLIENT_KEY);
|
||||||
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
|
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
|
||||||
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
|
Object clientIdExtra;
|
||||||
|
try {
|
||||||
|
clientIdExtra = StpUtil.getExtra(LoginHelper.CLIENT_KEY);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw NotLoginException.newInstance(StpUtil.getLoginType(),
|
||||||
|
NotLoginException.INVALID_TOKEN,
|
||||||
|
"token无效或已过期,请重新登录",
|
||||||
|
StpUtil.getTokenValue());
|
||||||
|
}
|
||||||
|
String clientId = clientIdExtra == null ? null : clientIdExtra.toString();
|
||||||
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
|
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
|
||||||
// token 无效
|
// token 无效
|
||||||
throw NotLoginException.newInstance(StpUtil.getLoginType(),
|
throw NotLoginException.newInstance(StpUtil.getLoginType(),
|
||||||
|
|||||||
-28
@@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.satoken.utils.LoginHelper;
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.dromara.common.sse.core.SseEmitterManager;
|
import org.dromara.common.sse.core.SseEmitterManager;
|
||||||
import org.dromara.common.sse.dto.SseMessageDto;
|
|
||||||
import org.springframework.beans.factory.DisposableBean;
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.http.MediaType;
|
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.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSE 控制器
|
* SSE 控制器
|
||||||
@@ -51,32 +49,6 @@ public class SseController implements DisposableBean {
|
|||||||
return R.ok();
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误
|
* 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ public class ResourcesConfig implements WebMvcConfigurer {
|
|||||||
public CorsFilter corsFilter() {
|
public CorsFilter corsFilter() {
|
||||||
CorsConfiguration config = new CorsConfiguration();
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
config.setAllowCredentials(true);
|
config.setAllowCredentials(true);
|
||||||
// 设置访问源地址
|
// 设置访问源地址(恢复为原通配,避免生产域名/IP 不匹配导致整站接口被浏览器拦截)
|
||||||
config.addAllowedOriginPattern("*");
|
config.addAllowedOriginPattern("*");
|
||||||
// 设置访问源请求头
|
// 设置访问源请求头
|
||||||
config.addAllowedHeader("*");
|
config.addAllowedHeader("*");
|
||||||
|
|||||||
+33
-8
@@ -37,6 +37,30 @@ import java.io.IOException;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
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) {
|
public R<Void> handleServiceException(ServiceException e, HttpServletRequest request) {
|
||||||
log.error(e.getMessage());
|
log.error(e.getMessage());
|
||||||
Integer code = e.getCode();
|
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) {
|
public R<Void> handleServletException(ServletException e, HttpServletRequest request) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
log.error("请求地址'{}',发生未知异常.", requestURI, e);
|
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)
|
@ExceptionHandler(BaseException.class)
|
||||||
public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
|
public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
|
||||||
log.error(e.getMessage());
|
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) {
|
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI);
|
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) {
|
public R<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
log.error("请求地址'{}',发生未知异常.", requestURI, e);
|
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) {
|
public R<Void> handleException(Exception e, HttpServletRequest request) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
log.error("请求地址'{}',发生系统异常.", requestURI, e);
|
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) {
|
public R<Void> handleJsonParseException(JsonParseException e, HttpServletRequest request) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
log.error("请求地址'{}' 发生 JSON 解析异常: {}", requestURI, e.getMessage());
|
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)
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
public R<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
|
public R<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
|
||||||
log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage());
|
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
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.bzrkh.controller;
|
package org.dromara.bzrkh.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ public class TeacherAssessmentController extends BaseController {
|
|||||||
return toAjax(result);
|
return toAjax(result);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
// 返回具体的错误信息
|
// 返回具体的错误信息
|
||||||
return R.fail(e.getMessage());
|
return R.fail(GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 记录详细的异常信息
|
// 记录详细的异常信息
|
||||||
log.error("提交考核异常", e);
|
log.error("提交考核异常", e);
|
||||||
@@ -135,7 +136,7 @@ public class TeacherAssessmentController extends BaseController {
|
|||||||
return toAjax(result);
|
return toAjax(result);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
// 返回具体的错误信息
|
// 返回具体的错误信息
|
||||||
return R.fail(e.getMessage());
|
return R.fail(GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 记录详细的异常信息
|
// 记录详细的异常信息
|
||||||
log.error("提交考核评分数据异常", 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
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.cw.controller;
|
package org.dromara.cw.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
@@ -298,7 +299,7 @@ public class CwApprovalRecordController extends BaseController {
|
|||||||
return R.ok();
|
return R.ok();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("❌ 分管领导审批失败:applicationId={}, error={}", applicationId, e.getMessage(), 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();
|
return R.ok();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("❌ 财务审批失败:applicationId={}, error={}", applicationId, e.getMessage(), e);
|
log.error("❌ 财务审批失败:applicationId={}, error={}", applicationId, e.getMessage(), e);
|
||||||
return R.fail("审批失败:" + e.getMessage());
|
return R.fail("审批失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-5
@@ -1,5 +1,7 @@
|
|||||||
package org.dromara.hq.controller;
|
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 cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
@@ -120,7 +122,7 @@ public class HqPmRepairAttachmentController extends BaseController {
|
|||||||
HqPmRepairAttachmentVo result = attachmentService.uploadFile(orderId, uploadStage, file, description);
|
HqPmRepairAttachmentVo result = attachmentService.uploadFile(orderId, uploadStage, file, description);
|
||||||
return R.ok(result);
|
return R.ok(result);
|
||||||
} catch (Exception e) {
|
} 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);
|
descriptions);
|
||||||
return R.ok(results);
|
return R.ok(results);
|
||||||
} catch (Exception e) {
|
} 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 {
|
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.setContentType("application/octet-stream");
|
||||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + attachment.getFileName() + "\"");
|
FileUtils.setAttachmentResponseHeader(response, attachment.getFileName());
|
||||||
|
|
||||||
// 这里需要根据具体的文件存储方式来实现下载逻辑
|
|
||||||
// 如果使用OSS,可以直接重定向到OSS的URL
|
// 如果使用OSS,可以直接重定向到OSS的URL
|
||||||
response.sendRedirect(attachment.getFilePath());
|
response.sendRedirect(filePath);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("下载文件失败: {}", e.getMessage(), e);
|
log.error("下载文件失败: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.hq.controller;
|
package org.dromara.hq.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -204,7 +205,7 @@ public class HqSatSurveyController extends BaseController {
|
|||||||
return R.ok(result);
|
return R.ok(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("提交问卷答案失败", 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);
|
return R.ok(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("保存问卷答案草稿失败", 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("未找到有效的答题记录")) {
|
if (e.getMessage() != null && e.getMessage().contains("未找到有效的答题记录")) {
|
||||||
return R.fail(404, "未找到有效的答题记录");
|
return R.fail(404, "未找到有效的答题记录");
|
||||||
}
|
}
|
||||||
return R.fail("获取答题记录失败:" + e.getMessage());
|
return R.fail("获取答题记录失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-79
@@ -1,6 +1,8 @@
|
|||||||
package org.dromara.jcbt.controller;
|
package org.dromara.jcbt.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
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.domain.R;
|
||||||
import org.dromara.common.core.validate.AddGroup;
|
import org.dromara.common.core.validate.AddGroup;
|
||||||
import org.dromara.common.core.validate.EditGroup;
|
import org.dromara.common.core.validate.EditGroup;
|
||||||
@@ -145,6 +147,9 @@ public class JcbtScholarshipApplicationController extends BaseController {
|
|||||||
@Operation(summary = "查询我的申请记录")
|
@Operation(summary = "查询我的申请记录")
|
||||||
public R<List<JcbtScholarshipApplicationVo>> getMyApplications(@Parameter(description = "申请人ID") @RequestParam("applicantId") Long applicantId,
|
public R<List<JcbtScholarshipApplicationVo>> getMyApplications(@Parameter(description = "申请人ID") @RequestParam("applicantId") Long applicantId,
|
||||||
@Parameter(description = "批次ID") @RequestParam(value = "batchId", required = false) Long batchId) {
|
@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));
|
return R.ok(applicationService.queryApplicationsByApplicant(applicantId, batchId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,83 +340,4 @@ public class JcbtScholarshipApplicationController extends BaseController {
|
|||||||
return R.ok(applicationService.confirmClassSelection(teacherId, classId));
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
-48
@@ -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
|
* 获取当前用户的学院ID
|
||||||
* 通过查询学生信息或用户部门信息获取
|
* 通过查询学生信息或用户部门信息获取
|
||||||
|
|||||||
+2
-46
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.jcbt.controller;
|
package org.dromara.jcbt.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@@ -120,51 +121,6 @@ public class JcbtWorkStudyApplicationController extends BaseController {
|
|||||||
ExcelUtil.exportExcel(list, "勤工俭学申请信息", JcbtWorkStudyApplicationVo.class, response);
|
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个) ==========
|
// ========== 业务功能接口(4个) ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -342,7 +298,7 @@ public class JcbtWorkStudyApplicationController extends BaseController {
|
|||||||
return R.ok(studentInfo);
|
return R.ok(studentInfo);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取当前用户学生信息失败", e);
|
log.error("获取当前用户学生信息失败", e);
|
||||||
return R.fail("获取学生信息失败:" + e.getMessage());
|
return R.fail("获取学生信息失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.rs.controller;
|
package org.dromara.rs.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -224,6 +225,7 @@ public class RsApplicationController extends BaseController {
|
|||||||
@Log(title = "提交职位申请", businessType = BusinessType.INSERT)
|
@Log(title = "提交职位申请", businessType = BusinessType.INSERT)
|
||||||
@PostMapping("/apply")
|
@PostMapping("/apply")
|
||||||
public R<Void> apply(@RequestBody RsApplication application) {
|
public R<Void> apply(@RequestBody RsApplication application) {
|
||||||
|
application.setApplicantId(LoginHelper.getUserId());
|
||||||
// 检查是否已经申请过
|
// 检查是否已经申请过
|
||||||
RsApplication existingApp = applicationService.checkApplied(application.getJobId(), application.getApplicantId());
|
RsApplication existingApp = applicationService.checkApplied(application.getJobId(), application.getApplicantId());
|
||||||
if (existingApp != null) {
|
if (existingApp != null) {
|
||||||
@@ -241,7 +243,8 @@ public class RsApplicationController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/my/{applicantId}")
|
@GetMapping("/my/{applicantId}")
|
||||||
public TableDataInfo<RsApplication> myApplications(@PathVariable("applicantId") Long applicantId, PageQuery pageQuery) {
|
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);
|
return TableDataInfo.build(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,10 +301,7 @@ public class RsApplicationController extends BaseController {
|
|||||||
@PostMapping("/submit")
|
@PostMapping("/submit")
|
||||||
public R<Map<String, Object>> submitApplication(@RequestBody RsApplication application) {
|
public R<Map<String, Object>> submitApplication(@RequestBody RsApplication application) {
|
||||||
try {
|
try {
|
||||||
// 设置默认状态
|
|
||||||
if (StringUtils.isEmpty(application.getStatus())) {
|
|
||||||
application.setStatus("pending");
|
application.setStatus("pending");
|
||||||
}
|
|
||||||
|
|
||||||
// 设置申请时间
|
// 设置申请时间
|
||||||
application.setApplyTime(new java.util.Date());
|
application.setApplyTime(new java.util.Date());
|
||||||
@@ -331,7 +331,7 @@ public class RsApplicationController extends BaseController {
|
|||||||
return R.fail("申请提交失败");
|
return R.fail("申请提交失败");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("申请提交失败:" + e.getMessage());
|
return R.fail("申请提交失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.rs.controller;
|
package org.dromara.rs.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.log.annotation.Log;
|
import org.dromara.common.log.annotation.Log;
|
||||||
import org.dromara.common.log.enums.BusinessType;
|
import org.dromara.common.log.enums.BusinessType;
|
||||||
@@ -85,7 +86,7 @@ public class RsInitController extends BaseController {
|
|||||||
return R.ok("菜单初始化成功");
|
return R.ok("菜单初始化成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
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("菜单清除成功");
|
return R.ok("菜单清除成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
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("菜单更新成功");
|
return R.ok("菜单更新成功");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return R.fail("菜单更新失败: " + e.getMessage());
|
return R.fail("菜单更新失败: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.rs.controller;
|
package org.dromara.rs.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -76,7 +77,7 @@ public class RsOverseasApplicationAttachmentController extends BaseController {
|
|||||||
return R.ok("上传成功", vo);
|
return R.ok("上传成功", vo);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("上传附件失败", e);
|
log.error("上传附件失败", e);
|
||||||
return R.fail("上传失败:" + e.getMessage());
|
return R.fail("上传失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.rs.controller;
|
package org.dromara.rs.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
@@ -392,7 +393,7 @@ public class RsOverseasApplicationController extends BaseController {
|
|||||||
int fixedCount = applicationService.fixApprovedApplicationsWithoutRecords();
|
int fixedCount = applicationService.fixApprovedApplicationsWithoutRecords();
|
||||||
return R.ok("修复完成,共修复 " + fixedCount + " 条记录");
|
return R.ok("修复完成,共修复 " + fixedCount + " 条记录");
|
||||||
} catch (Exception e) {
|
} 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) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
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) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return R.fail("系统异常:" + e.getMessage());
|
return R.fail("系统异常:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-35
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.rs.controller;
|
package org.dromara.rs.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
@@ -78,40 +79,6 @@ public class RsOverseasApprovalController extends BaseController {
|
|||||||
return approvalService.queryCombinedPageList(query);
|
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查询审批记录列表
|
* 根据申请ID查询审批记录列表
|
||||||
*/
|
*/
|
||||||
@@ -187,7 +154,7 @@ public class RsOverseasApprovalController extends BaseController {
|
|||||||
return R.ok("审批记录数据修复成功!");
|
return R.ok("审批记录数据修复成功!");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("修复审批记录数据失败", e);
|
log.error("修复审批记录数据失败", e);
|
||||||
return R.fail("修复失败:" + e.getMessage());
|
return R.fail("修复失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.rs.controller;
|
package org.dromara.rs.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
@@ -254,7 +255,7 @@ public class RsOverseasRecordController extends BaseController {
|
|||||||
|
|
||||||
return R.ok("测试数据初始化成功,请刷新页面查看");
|
return R.ok("测试数据初始化成功,请刷新页面查看");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("测试数据初始化失败: " + e.getMessage());
|
return R.fail("测试数据初始化失败: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.rs.controller;
|
package org.dromara.rs.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
@@ -222,7 +223,7 @@ public class RsTrainingApplicationController extends BaseController {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
log.error("添加培训申请失败: " + e.getMessage(), e);
|
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) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
log.error("添加培训申请失败: " + e.getMessage(), e);
|
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("审批失败");
|
return result ? R.ok() : R.fail("审批失败");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("审批培训申请失败: {}", e.getMessage(), 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);
|
return R.ok(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("查询待审批申请失败: {}", e.getMessage(), 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) {
|
} catch (NumberFormatException e) {
|
||||||
log.error("解析用户ID失败,原始字符串: {}, 错误: {}", userIds, e.getMessage());
|
log.error("解析用户ID失败,原始字符串: {}, 错误: {}", userIds, e.getMessage());
|
||||||
return R.fail("用户ID格式错误: " + e.getMessage());
|
return R.fail("用户ID格式错误: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CollUtil.isEmpty(userIdList)) {
|
if (CollUtil.isEmpty(userIdList)) {
|
||||||
|
|||||||
-27
@@ -52,33 +52,6 @@ public class RsTrainingRecordController extends BaseController {
|
|||||||
return trainingRecordService.selectPageRsTrainingRecordList(record, pageQuery);
|
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
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
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.core.utils.StringUtils;
|
||||||
import org.dromara.common.oss.core.OssClient;
|
import org.dromara.common.oss.core.OssClient;
|
||||||
import org.dromara.common.oss.entity.UploadResult;
|
import org.dromara.common.oss.entity.UploadResult;
|
||||||
@@ -86,7 +87,10 @@ public class RsOverseasApplicationAttachmentServiceImpl implements IRsOverseasAp
|
|||||||
// 获取文件信息
|
// 获取文件信息
|
||||||
String originalFilename = file.getOriginalFilename();
|
String originalFilename = file.getOriginalFilename();
|
||||||
String fileExtension = getFileExtension(originalFilename);
|
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()
|
String fileName = "overseas_attachment_" + applicationId + "_" + System.currentTimeMillis()
|
||||||
|
|||||||
+6
@@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.constraints.*;
|
import jakarta.validation.constraints.*;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
||||||
@@ -132,6 +133,11 @@ public class StglActivityAttendanceController extends BaseController {
|
|||||||
@RequestParam(value = "studentNo", required = false) String studentNo) {
|
@RequestParam(value = "studentNo", required = false) String studentNo) {
|
||||||
|
|
||||||
List<StglActivityAttendanceVo> list;
|
List<StglActivityAttendanceVo> list;
|
||||||
|
if (!StpUtil.hasPermission("stgl:activityRegister:approve")
|
||||||
|
&& !StpUtil.hasPermission("stgl:activity:edit")) {
|
||||||
|
userId = LoginHelper.getUserId();
|
||||||
|
studentNo = null;
|
||||||
|
}
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
list = stglActivityAttendanceService.queryAllByActivityAndUser(activityId, userId);
|
list = stglActivityAttendanceService.queryAllByActivityAndUser(activityId, userId);
|
||||||
} else if (StringUtils.isNotBlank(studentNo)) {
|
} else if (StringUtils.isNotBlank(studentNo)) {
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
package org.dromara.stgl.controller;
|
package org.dromara.stgl.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
@@ -123,7 +124,7 @@ public class StglActivityRegistrationController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/checkRegistered")
|
@GetMapping("/checkRegistered")
|
||||||
public R<Boolean> checkRegistered(@RequestParam("activityId") Long activityId, @RequestParam(value = "userId", required = false) Long userId) {
|
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));
|
return R.ok(stglActivityRegistrationService.checkUserRegistered(activityId, currentUserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ public class StglActivityRegistrationController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/getUserRegistration")
|
@GetMapping("/getUserRegistration")
|
||||||
public R<StglActivityRegistrationVo> getUserRegistration(@RequestParam("activityId") Long activityId, @RequestParam(value = "userId", required = false) Long userId) {
|
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));
|
return R.ok(stglActivityRegistrationService.getUserRegistration(activityId, currentUserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-21
@@ -192,27 +192,6 @@ public class StglClubController extends BaseController {
|
|||||||
return toAjax(stglClubService.transferClub(id, newPresidentUserId));
|
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
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.stgl.controller;
|
package org.dromara.stgl.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -53,7 +54,7 @@ public class StglClubPermissionController extends BaseController {
|
|||||||
return R.ok(result);
|
return R.ok(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("检查社团活动权限失败,clubId: {}", clubId, 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);
|
return R.ok(results);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("批量检查社团权限失败,clubIds: {}", clubIds, 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);
|
return R.ok(inWindow);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("检查年审窗口期失败,clubId: {}", clubId, 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);
|
return R.ok(days);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取距离年审天数失败,clubId: {}", clubId, 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);
|
return R.ok(hasSubmitted);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("检查年审提交状态失败,clubId: {}", clubId, 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);
|
return R.ok(details);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取社团权限详情失败,clubId: {}", clubId, e);
|
log.error("获取社团权限详情失败,clubId: {}", clubId, e);
|
||||||
return R.fail("获取详情失败: " + e.getMessage());
|
return R.fail("获取详情失败: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.stgl.controller;
|
package org.dromara.stgl.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
@@ -257,7 +258,7 @@ public class StglVolunteerTeamController extends BaseController {
|
|||||||
Integer fixedCount = stglVolunteerTeamService.fixExistingTeamLeaderRecords();
|
Integer fixedCount = stglVolunteerTeamService.fixExistingTeamLeaderRecords();
|
||||||
return R.ok("修复完成,共补充了 " + fixedCount + " 条队长成员记录");
|
return R.ok("修复完成,共补充了 " + fixedCount + " 条队长成员记录");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("修复失败:" + e.getMessage());
|
return R.fail("修复失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-83
@@ -683,54 +683,32 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
|
|||||||
* @param bo 查询条件
|
* @param bo 查询条件
|
||||||
*/
|
*/
|
||||||
private void applyDataScope(StglActivityBo bo) {
|
private void applyDataScope(StglActivityBo bo) {
|
||||||
try {
|
if (bo.getParams() == null) {
|
||||||
// 获取当前登录用户信息
|
bo.setParams(new HashMap<>());
|
||||||
|
}
|
||||||
|
// 数据权限参数只能由服务端写入,丢弃请求中同名参数
|
||||||
|
bo.getParams().keySet().removeIf(key -> key.startsWith("ds") || "dataScope".equals(key));
|
||||||
|
|
||||||
Long currentUserId = LoginHelper.getUserId();
|
Long currentUserId = LoginHelper.getUserId();
|
||||||
if (currentUserId == null) {
|
if (currentUserId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> roleKeys = LoginHelper.getLoginUser().getRolePermission();
|
Set<String> roleKeys = LoginHelper.getLoginUser().getRolePermission();
|
||||||
|
boolean manager = roleKeys.contains("admin") || roleKeys.contains("common");
|
||||||
|
|
||||||
// 如果是stgl_president角色且不是管理员,只能查看自己创建的活动
|
// 社长:只能查看自己创建的活动
|
||||||
if (roleKeys.contains("stgl_president") &&
|
if (roleKeys.contains("stgl_president") && !manager) {
|
||||||
!roleKeys.contains("admin") &&
|
bo.getParams().put("dsCreateBy", currentUserId);
|
||||||
!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);
|
log.info("应用数据权限:用户 {} 为社长角色,只能查看自己创建的活动", currentUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 修正:如果是stgl_instructor角色且不是管理员,只能查看自己作为指导老师的活动
|
// 指导老师:只能查看自己指导的社团/志愿者队伍/活动
|
||||||
if (roleKeys.contains("stgl_instructor") &&
|
if (roleKeys.contains("stgl_instructor") && !manager) {
|
||||||
!roleKeys.contains("admin") &&
|
bo.getParams().put("dsInstructor", Boolean.TRUE);
|
||||||
!roleKeys.contains("common")) {
|
bo.getParams().put("dsClubIds", getUserAdvisorClubIds(currentUserId));
|
||||||
|
bo.getParams().put("dsTeamIds", getUserAdvisorTeamIds(currentUserId));
|
||||||
// 获取用户作为指导老师的社团ID列表
|
bo.getParams().put("dsActivityIds", getUserAdvisorActivityIds(currentUserId));
|
||||||
List<Long> userAdvisorClubIds = getUserAdvisorClubIds(currentUserId);
|
log.info("应用数据权限:用户 {} 为指导老师角色", 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,50 +899,6 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
|
|||||||
return activityIds;
|
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
|
@Override
|
||||||
public List<ActivityRatingVo> getActivityRatings(Long activityId) {
|
public List<ActivityRatingVo> getActivityRatings(Long activityId) {
|
||||||
if (activityId == null) {
|
if (activityId == null) {
|
||||||
|
|||||||
+42
-2
@@ -207,7 +207,27 @@
|
|||||||
AND sa.related_club_id = #{relatedClubId}
|
AND sa.related_club_id = #{relatedClubId}
|
||||||
</if>
|
</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>
|
</where>
|
||||||
ORDER BY sa.create_time DESC
|
ORDER BY sa.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
@@ -366,7 +386,27 @@
|
|||||||
)
|
)
|
||||||
</if>
|
</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>
|
</where>
|
||||||
ORDER BY sa.create_time DESC
|
ORDER BY sa.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
+5
-4
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.system.controller.system;
|
package org.dromara.system.controller.system;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.web.core.BaseController;
|
import org.dromara.common.web.core.BaseController;
|
||||||
@@ -46,7 +47,7 @@ public class SysBucketPasswordController extends BaseController {
|
|||||||
return R.fail("密码重置失败");
|
return R.fail("密码重置失败");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} 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密码配置不存在");
|
return R.fail("bucket密码配置不存在");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} 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);
|
var records = bucketPasswordService.getBucketAccessRecords(bucketName, limit);
|
||||||
return R.ok(records);
|
return R.ok(records);
|
||||||
} catch (Exception e) {
|
} 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");
|
bucketName, password, "127.0.0.1", "API-Test");
|
||||||
return R.ok(isValid);
|
return R.ok(isValid);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("密码验证失败:" + e.getMessage());
|
return R.fail("密码验证失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+16
@@ -1,6 +1,8 @@
|
|||||||
package org.dromara.system.controller.system;
|
package org.dromara.system.controller.system;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
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.core.domain.R;
|
||||||
import org.dromara.common.excel.utils.ExcelUtil;
|
import org.dromara.common.excel.utils.ExcelUtil;
|
||||||
import org.dromara.common.log.annotation.Log;
|
import org.dromara.common.log.annotation.Log;
|
||||||
@@ -29,6 +31,15 @@ import java.util.List;
|
|||||||
@RequestMapping("/system/config")
|
@RequestMapping("/system/config")
|
||||||
public class SysConfigController extends BaseController {
|
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;
|
private final ISysConfigService configService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +80,11 @@ public class SysConfigController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping(value = "/configKey/{configKey}")
|
@GetMapping(value = "/configKey/{configKey}")
|
||||||
public R<String> getConfigKey(@PathVariable("configKey") String 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));
|
return R.ok("操作成功", configService.selectConfigByKey(configKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-8
@@ -1,6 +1,7 @@
|
|||||||
package org.dromara.system.controller.system;
|
package org.dromara.system.controller.system;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import org.dromara.common.oss.core.OssClient;
|
import org.dromara.common.oss.core.OssClient;
|
||||||
@@ -24,10 +25,13 @@ import java.util.Map;
|
|||||||
*
|
*
|
||||||
* @author Lion Li
|
* @author Lion Li
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/resource/file")
|
@RequestMapping("/resource/file")
|
||||||
public class SysFileAccessController extends BaseController {
|
public class SysFileAccessController extends BaseController {
|
||||||
|
|
||||||
|
private static final int MAX_EXPIRE_SECONDS = 3600;
|
||||||
|
|
||||||
private final ISysOssService ossService;
|
private final ISysOssService ossService;
|
||||||
private final ISysBucketAccessPasswordService bucketPasswordService;
|
private final ISysBucketAccessPasswordService bucketPasswordService;
|
||||||
|
|
||||||
@@ -82,7 +86,7 @@ public class SysFileAccessController extends BaseController {
|
|||||||
// 生成预签名URL
|
// 生成预签名URL
|
||||||
String presignedUrl = storage.getPrivateUrl(
|
String presignedUrl = storage.getPrivateUrl(
|
||||||
oss.getFileName(),
|
oss.getFileName(),
|
||||||
Duration.ofSeconds(expireSeconds)
|
Duration.ofSeconds(limitExpireSeconds(expireSeconds))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 记录文件访问日志
|
// 记录文件访问日志
|
||||||
@@ -91,7 +95,8 @@ public class SysFileAccessController extends BaseController {
|
|||||||
|
|
||||||
return R.ok(presignedUrl);
|
return R.ok(presignedUrl);
|
||||||
} catch (Exception e) {
|
} 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());
|
OssClient storage = OssFactory.instance(oss.getService());
|
||||||
String presignedUrl = storage.getPrivateUrl(
|
String presignedUrl = storage.getPrivateUrl(
|
||||||
oss.getFileName(),
|
oss.getFileName(),
|
||||||
Duration.ofSeconds(expireSeconds)
|
Duration.ofSeconds(limitExpireSeconds(expireSeconds))
|
||||||
);
|
);
|
||||||
urlMap.put(ossId, presignedUrl);
|
urlMap.put(ossId, presignedUrl);
|
||||||
|
|
||||||
@@ -144,7 +149,8 @@ public class SysFileAccessController extends BaseController {
|
|||||||
|
|
||||||
return R.ok(urlMap);
|
return R.ok(urlMap);
|
||||||
} catch (Exception e) {
|
} 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());
|
OssClient storage = OssFactory.instance(oss.getService());
|
||||||
String presignedUrl = storage.getPrivateUrl(
|
String presignedUrl = storage.getPrivateUrl(
|
||||||
oss.getFileName(),
|
oss.getFileName(),
|
||||||
Duration.ofSeconds(expireSeconds)
|
Duration.ofSeconds(limitExpireSeconds(expireSeconds))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 记录下载日志
|
// 记录下载日志
|
||||||
@@ -198,7 +204,8 @@ public class SysFileAccessController extends BaseController {
|
|||||||
|
|
||||||
return R.ok(presignedUrl);
|
return R.ok(presignedUrl);
|
||||||
} catch (Exception e) {
|
} 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());
|
OssClient storage = OssFactory.instance(oss.getService());
|
||||||
String presignedUrl = storage.getPrivateUrl(
|
String presignedUrl = storage.getPrivateUrl(
|
||||||
oss.getFileName(),
|
oss.getFileName(),
|
||||||
Duration.ofSeconds(expireSeconds)
|
Duration.ofSeconds(limitExpireSeconds(expireSeconds))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 记录预览日志
|
// 记录预览日志
|
||||||
@@ -252,10 +259,18 @@ public class SysFileAccessController extends BaseController {
|
|||||||
|
|
||||||
return R.ok(presignedUrl);
|
return R.ok(presignedUrl);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("获取文件预览链接失败:" + e.getMessage());
|
log.error("获取文件预览链接失败", e);
|
||||||
|
return R.fail("获取文件预览链接失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int limitExpireSeconds(Integer expireSeconds) {
|
||||||
|
if (expireSeconds == null || expireSeconds <= 0) {
|
||||||
|
return 300;
|
||||||
|
}
|
||||||
|
return Math.min(expireSeconds, MAX_EXPIRE_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证bucket访问密码
|
* 验证bucket访问密码
|
||||||
*
|
*
|
||||||
|
|||||||
+8
-7
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.system.controller.wechat;
|
package org.dromara.system.controller.wechat;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -46,7 +47,7 @@ public class SysWeChatController extends BaseController {
|
|||||||
return R.ok("获取配置成功", config);
|
return R.ok("获取配置成功", config);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取微信JS-SDK配置失败", 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);
|
return R.ok("上传成功", result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("上传微信媒体文件失败", 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);
|
return R.ok("下载成功", result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("下载微信媒体文件失败", 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);
|
return R.ok("获取用户信息成功", result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取微信用户信息失败", 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);
|
return R.ok("创建支付订单成功", result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建微信支付订单失败", 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);
|
return R.ok("查询支付状态成功", result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("查询微信支付状态失败", 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);
|
return R.ok("下载文件到OSS成功", result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("下载微信媒体文件到OSS失败", e);
|
log.error("下载微信媒体文件到OSS失败", e);
|
||||||
return R.fail("下载文件到OSS失败: " + e.getMessage());
|
return R.fail("下载文件到OSS失败: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
-4
@@ -27,7 +27,10 @@ import org.dromara.common.oss.factory.OssFactory;
|
|||||||
import org.dromara.system.domain.SysOss;
|
import org.dromara.system.domain.SysOss;
|
||||||
import org.dromara.system.domain.bo.SysOssBo;
|
import org.dromara.system.domain.bo.SysOssBo;
|
||||||
import org.dromara.system.domain.vo.SysOssVo;
|
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.SysOssMapper;
|
||||||
|
import org.dromara.system.mapper.SysUserMapper;
|
||||||
import org.dromara.system.config.FileCacheConfig;
|
import org.dromara.system.config.FileCacheConfig;
|
||||||
import org.dromara.system.service.ISysOssService;
|
import org.dromara.system.service.ISysOssService;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -66,7 +69,10 @@ import java.util.zip.ZipOutputStream;
|
|||||||
@Service
|
@Service
|
||||||
public class SysOssServiceImpl implements ISysOssService, OssService {
|
public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||||
|
|
||||||
|
private static final String STUDENT_ACCOUNT_TYPE = "2";
|
||||||
|
|
||||||
private final SysOssMapper baseMapper;
|
private final SysOssMapper baseMapper;
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
private final FileCacheConfig fileCacheConfig;
|
private final FileCacheConfig fileCacheConfig;
|
||||||
|
|
||||||
// 🚀 LRU缓存实现
|
// 🚀 LRU缓存实现
|
||||||
@@ -292,7 +298,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
|||||||
OssClient storage = OssFactory.instance();
|
OssClient storage = OssFactory.instance();
|
||||||
UploadResult uploadResult;
|
UploadResult uploadResult;
|
||||||
try {
|
try {
|
||||||
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
|
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, MimeTypeUtils.getSafeContentType(ext));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ServiceException(e.getMessage());
|
throw new ServiceException(e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -907,6 +913,11 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 学生账号:不能访问其他学生上传的文件(身份证、贫困证明等),教职工上传的文件可访问
|
||||||
|
if (isStudentAccount(userId)) {
|
||||||
|
return ObjectUtil.isNotNull(oss.getCreateBy()) && !isStudentAccount(oss.getCreateBy());
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 具备 OSS 管理/下载类权限的用户
|
// 5. 具备 OSS 管理/下载类权限的用户
|
||||||
if (StpUtil.hasPermission("system:oss:download")
|
if (StpUtil.hasPermission("system:oss:download")
|
||||||
|| StpUtil.hasPermission("system:oss:list")
|
|| StpUtil.hasPermission("system:oss:list")
|
||||||
@@ -915,12 +926,18 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 业务场景(请假/宿检等):审批人需查看申请人附件。
|
// 6. 业务场景(请假/宿检等):教职工审批人需查看申请人附件
|
||||||
// 全量列表已由 system:oss:list 约束;此处允许任意已登录用户按 ossId 访问单个文件。
|
|
||||||
// 后续应按业务单据做归属校验以消除枚举 IDOR。
|
|
||||||
return true;
|
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
@@ -136,7 +136,7 @@ public class SysWeChatServiceImpl implements ISysWeChatService {
|
|||||||
"https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s",
|
"https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s",
|
||||||
accessToken, mediaId);
|
accessToken, mediaId);
|
||||||
|
|
||||||
log.info("微信多媒体下载URL: {}", downloadUrl);
|
log.info("微信多媒体下载,mediaId: {}", mediaId);
|
||||||
|
|
||||||
// 4. 发起HTTP请求下载文件
|
// 4. 发起HTTP请求下载文件
|
||||||
HttpResponse response = HttpRequest.get(downloadUrl).execute();
|
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",
|
"https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s",
|
||||||
accessToken, mediaId);
|
accessToken, mediaId);
|
||||||
|
|
||||||
log.info("微信多媒体下载URL: {}", downloadUrl);
|
log.info("微信多媒体下载,mediaId: {}", mediaId);
|
||||||
|
|
||||||
// 4. 发起HTTP请求下载文件
|
// 4. 发起HTTP请求下载文件
|
||||||
HttpResponse response = HttpRequest.get(downloadUrl).execute();
|
HttpResponse response = HttpRequest.get(downloadUrl).execute();
|
||||||
|
|||||||
+28
@@ -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";
|
||||||
|
|
||||||
|
}
|
||||||
+55
-1
@@ -1,16 +1,70 @@
|
|||||||
package org.dromara.workflow.config;
|
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.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.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配置
|
* warmFlow配置
|
||||||
*
|
*
|
||||||
* @author may
|
* @author may
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@ConditionalOnEnable
|
@ConditionalOnEnable
|
||||||
@Configuration
|
@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,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,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
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.workflow.controller;
|
package org.dromara.workflow.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.dromara.common.core.domain.R;
|
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.core.service.DefService;
|
||||||
import org.dromara.warm.flow.orm.entity.FlowDefinition;
|
import org.dromara.warm.flow.orm.entity.FlowDefinition;
|
||||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||||
|
import org.dromara.workflow.common.constant.FlowPermissionConstant;
|
||||||
import org.dromara.workflow.domain.vo.FlowDefinitionVo;
|
import org.dromara.workflow.domain.vo.FlowDefinitionVo;
|
||||||
import org.dromara.workflow.service.IFlwDefinitionService;
|
import org.dromara.workflow.service.IFlwDefinitionService;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -44,6 +46,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param flowDefinition 参数
|
* @param flowDefinition 参数
|
||||||
* @param pageQuery 分页
|
* @param pageQuery 分页
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST)
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo<FlowDefinitionVo> list(FlowDefinition flowDefinition, PageQuery pageQuery) {
|
public TableDataInfo<FlowDefinitionVo> list(FlowDefinition flowDefinition, PageQuery pageQuery) {
|
||||||
return flwDefinitionService.queryList(flowDefinition, pageQuery);
|
return flwDefinitionService.queryList(flowDefinition, pageQuery);
|
||||||
@@ -55,6 +58,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param flowDefinition 参数
|
* @param flowDefinition 参数
|
||||||
* @param pageQuery 分页
|
* @param pageQuery 分页
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST)
|
||||||
@GetMapping("/unPublishList")
|
@GetMapping("/unPublishList")
|
||||||
public TableDataInfo<FlowDefinitionVo> unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery) {
|
public TableDataInfo<FlowDefinitionVo> unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery) {
|
||||||
return flwDefinitionService.unPublishList(flowDefinition, pageQuery);
|
return flwDefinitionService.unPublishList(flowDefinition, pageQuery);
|
||||||
@@ -65,6 +69,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
*
|
*
|
||||||
* @param id 流程定义id
|
* @param id 流程定义id
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST)
|
||||||
@GetMapping(value = "/{id}")
|
@GetMapping(value = "/{id}")
|
||||||
public R<Definition> getInfo(@PathVariable("id") Long id) {
|
public R<Definition> getInfo(@PathVariable("id") Long id) {
|
||||||
return R.ok(defService.getById(id));
|
return R.ok(defService.getById(id));
|
||||||
@@ -76,6 +81,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param flowDefinition 参数
|
* @param flowDefinition 参数
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -89,6 +95,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param flowDefinition 参数
|
* @param flowDefinition 参数
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
|
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PutMapping
|
@PutMapping
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -102,6 +109,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param id 流程定义id
|
* @param id 流程定义id
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PutMapping("/publish/{id}")
|
@PutMapping("/publish/{id}")
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
public R<Boolean> publish(@PathVariable("id") Long id) {
|
public R<Boolean> publish(@PathVariable("id") Long id) {
|
||||||
@@ -114,6 +122,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param id 流程定义id
|
* @param id 流程定义id
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PutMapping("/unPublish/{id}")
|
@PutMapping("/unPublish/{id}")
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -125,6 +134,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* 删除流程定义
|
* 删除流程定义
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.DELETE)
|
@Log(title = "流程定义", businessType = BusinessType.DELETE)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@DeleteMapping("/{ids}")
|
@DeleteMapping("/{ids}")
|
||||||
public R<Void> remove(@PathVariable("ids") List<Long> ids) {
|
public R<Void> remove(@PathVariable("ids") List<Long> ids) {
|
||||||
return toAjax(flwDefinitionService.removeDef(ids));
|
return toAjax(flwDefinitionService.removeDef(ids));
|
||||||
@@ -136,6 +146,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param id 流程定义id
|
* @param id 流程定义id
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
@Log(title = "流程定义", businessType = BusinessType.INSERT)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PostMapping("/copy/{id}")
|
@PostMapping("/copy/{id}")
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -150,6 +161,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param category 分类
|
* @param category 分类
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.IMPORT)
|
@Log(title = "流程定义", businessType = BusinessType.IMPORT)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PostMapping("/importDef")
|
@PostMapping("/importDef")
|
||||||
public R<Boolean> importDef(MultipartFile file, String category) {
|
public R<Boolean> importDef(MultipartFile file, String category) {
|
||||||
return R.ok(flwDefinitionService.importJson(file, category));
|
return R.ok(flwDefinitionService.importJson(file, category));
|
||||||
@@ -163,6 +175,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @throws IOException 异常
|
* @throws IOException 异常
|
||||||
*/
|
*/
|
||||||
@Log(title = "流程定义", businessType = BusinessType.EXPORT)
|
@Log(title = "流程定义", businessType = BusinessType.EXPORT)
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PostMapping("/exportDef/{id}")
|
@PostMapping("/exportDef/{id}")
|
||||||
public void exportDef(@PathVariable("id") Long id, HttpServletResponse response) throws IOException {
|
public void exportDef(@PathVariable("id") Long id, HttpServletResponse response) throws IOException {
|
||||||
flwDefinitionService.exportDef(id, response);
|
flwDefinitionService.exportDef(id, response);
|
||||||
@@ -173,6 +186,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
*
|
*
|
||||||
* @param id 流程定义id
|
* @param id 流程定义id
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_LIST)
|
||||||
@GetMapping("/xmlString/{id}")
|
@GetMapping("/xmlString/{id}")
|
||||||
public R<String> xmlString(@PathVariable("id") Long id) {
|
public R<String> xmlString(@PathVariable("id") Long id) {
|
||||||
return R.ok("操作成功", defService.exportJson(id));
|
return R.ok("操作成功", defService.exportJson(id));
|
||||||
@@ -185,6 +199,7 @@ public class FlwDefinitionController extends BaseController {
|
|||||||
* @param active 激活/挂起
|
* @param active 激活/挂起
|
||||||
*/
|
*/
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.DEFINITION_EDIT)
|
||||||
@PutMapping("/active/{id}")
|
@PutMapping("/active/{id}")
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<Boolean> active(@PathVariable("id") Long id, @RequestParam("active") boolean active) {
|
public R<Boolean> active(@PathVariable("id") Long id, @RequestParam("active") boolean active) {
|
||||||
|
|||||||
+8
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.workflow.controller;
|
package org.dromara.workflow.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
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.common.web.core.BaseController;
|
||||||
import org.dromara.warm.flow.core.service.InsService;
|
import org.dromara.warm.flow.core.service.InsService;
|
||||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
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.FlowCancelBo;
|
||||||
import org.dromara.workflow.domain.bo.FlowInstanceBo;
|
import org.dromara.workflow.domain.bo.FlowInstanceBo;
|
||||||
import org.dromara.workflow.domain.bo.FlowInvalidBo;
|
import org.dromara.workflow.domain.bo.FlowInvalidBo;
|
||||||
@@ -43,6 +45,7 @@ public class FlwInstanceController extends BaseController {
|
|||||||
* @param flowInstanceBo 流程实例
|
* @param flowInstanceBo 流程实例
|
||||||
* @param pageQuery 分页
|
* @param pageQuery 分页
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE)
|
||||||
@GetMapping("/pageByRunning")
|
@GetMapping("/pageByRunning")
|
||||||
public TableDataInfo<FlowInstanceVo> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
|
public TableDataInfo<FlowInstanceVo> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
|
||||||
return flwInstanceService.selectRunningInstanceList(flowInstanceBo, pageQuery);
|
return flwInstanceService.selectRunningInstanceList(flowInstanceBo, pageQuery);
|
||||||
@@ -54,6 +57,7 @@ public class FlwInstanceController extends BaseController {
|
|||||||
* @param flowInstanceBo 流程实例
|
* @param flowInstanceBo 流程实例
|
||||||
* @param pageQuery 分页
|
* @param pageQuery 分页
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE)
|
||||||
@GetMapping("/pageByFinish")
|
@GetMapping("/pageByFinish")
|
||||||
public TableDataInfo<FlowInstanceVo> selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
|
public TableDataInfo<FlowInstanceVo> selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
|
||||||
return flwInstanceService.selectFinishInstanceList(flowInstanceBo, pageQuery);
|
return flwInstanceService.selectFinishInstanceList(flowInstanceBo, pageQuery);
|
||||||
@@ -74,6 +78,7 @@ public class FlwInstanceController extends BaseController {
|
|||||||
*
|
*
|
||||||
* @param businessIds 业务id
|
* @param businessIds 业务id
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE)
|
||||||
@DeleteMapping("/deleteByBusinessIds/{businessIds}")
|
@DeleteMapping("/deleteByBusinessIds/{businessIds}")
|
||||||
public R<Void> deleteByBusinessIds(@PathVariable("businessIds") List<Long> businessIds) {
|
public R<Void> deleteByBusinessIds(@PathVariable("businessIds") List<Long> businessIds) {
|
||||||
return toAjax(flwInstanceService.deleteByBusinessIds(businessIds));
|
return toAjax(flwInstanceService.deleteByBusinessIds(businessIds));
|
||||||
@@ -107,6 +112,7 @@ public class FlwInstanceController extends BaseController {
|
|||||||
* @param active 激活/挂起
|
* @param active 激活/挂起
|
||||||
*/
|
*/
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE)
|
||||||
@PutMapping("/active/{id}")
|
@PutMapping("/active/{id}")
|
||||||
public R<Boolean> active(@PathVariable("id") Long id, @RequestParam("active") boolean active) {
|
public R<Boolean> active(@PathVariable("id") Long id, @RequestParam("active") boolean active) {
|
||||||
return R.ok(active ? insService.active(id) : insService.unActive(id));
|
return R.ok(active ? insService.active(id) : insService.unActive(id));
|
||||||
@@ -146,6 +152,7 @@ public class FlwInstanceController extends BaseController {
|
|||||||
*
|
*
|
||||||
* @param instanceId 流程实例id
|
* @param instanceId 流程实例id
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE)
|
||||||
@GetMapping("/instanceVariable/{instanceId}")
|
@GetMapping("/instanceVariable/{instanceId}")
|
||||||
public R<Map<String, Object>> instanceVariable(@PathVariable("instanceId") Long instanceId) {
|
public R<Map<String, Object>> instanceVariable(@PathVariable("instanceId") Long instanceId) {
|
||||||
return R.ok(flwInstanceService.instanceVariable(instanceId));
|
return R.ok(flwInstanceService.instanceVariable(instanceId));
|
||||||
@@ -158,6 +165,7 @@ public class FlwInstanceController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@Log(title = "流程实例管理", businessType = BusinessType.INSERT)
|
@Log(title = "流程实例管理", businessType = BusinessType.INSERT)
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.INSTANCE_MANAGE)
|
||||||
@PostMapping("/invalid")
|
@PostMapping("/invalid")
|
||||||
public R<Boolean> invalid(@Validated @RequestBody FlowInvalidBo bo) {
|
public R<Boolean> invalid(@Validated @RequestBody FlowInvalidBo bo) {
|
||||||
return R.ok(flwInstanceService.processInvalid(bo));
|
return R.ok(flwInstanceService.processInvalid(bo));
|
||||||
|
|||||||
+5
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.workflow.controller;
|
package org.dromara.workflow.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.core.domain.dto.StartProcessReturnDTO;
|
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.core.entity.Node;
|
||||||
import org.dromara.warm.flow.orm.entity.FlowNode;
|
import org.dromara.warm.flow.orm.entity.FlowNode;
|
||||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||||
|
import org.dromara.workflow.common.constant.FlowPermissionConstant;
|
||||||
import org.dromara.workflow.domain.bo.*;
|
import org.dromara.workflow.domain.bo.*;
|
||||||
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
|
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
|
||||||
import org.dromara.workflow.domain.vo.FlowTaskVo;
|
import org.dromara.workflow.domain.vo.FlowTaskVo;
|
||||||
@@ -91,6 +93,7 @@ public class FlwTaskController extends BaseController {
|
|||||||
* @param flowTaskBo 参数
|
* @param flowTaskBo 参数
|
||||||
* @param pageQuery 分页
|
* @param pageQuery 分页
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.TASK_MANAGE)
|
||||||
@GetMapping("/pageByAllTaskWait")
|
@GetMapping("/pageByAllTaskWait")
|
||||||
public TableDataInfo<FlowTaskVo> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
|
public TableDataInfo<FlowTaskVo> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
|
||||||
return flwTaskService.pageByAllTaskWait(flowTaskBo, pageQuery);
|
return flwTaskService.pageByAllTaskWait(flowTaskBo, pageQuery);
|
||||||
@@ -102,6 +105,7 @@ public class FlwTaskController extends BaseController {
|
|||||||
* @param flowTaskBo 参数
|
* @param flowTaskBo 参数
|
||||||
* @param pageQuery 分页
|
* @param pageQuery 分页
|
||||||
*/
|
*/
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.TASK_MANAGE)
|
||||||
@GetMapping("/pageByAllTaskFinish")
|
@GetMapping("/pageByAllTaskFinish")
|
||||||
public TableDataInfo<FlowHisTaskVo> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
|
public TableDataInfo<FlowHisTaskVo> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
|
||||||
return flwTaskService.pageByAllTaskFinish(flowTaskBo, pageQuery);
|
return flwTaskService.pageByAllTaskFinish(flowTaskBo, pageQuery);
|
||||||
@@ -171,6 +175,7 @@ public class FlwTaskController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
|
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
|
||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
|
@SaCheckPermission(FlowPermissionConstant.TASK_MANAGE)
|
||||||
@PutMapping("/updateAssignee/{userId}")
|
@PutMapping("/updateAssignee/{userId}")
|
||||||
public R<Void> updateAssignee(@RequestBody List<Long> taskIdList, @PathVariable("userId") String userId) {
|
public R<Void> updateAssignee(@RequestBody List<Long> taskIdList, @PathVariable("userId") String userId) {
|
||||||
return toAjax(flwTaskService.updateAssignee(taskIdList, userId));
|
return toAjax(flwTaskService.updateAssignee(taskIdList, userId));
|
||||||
|
|||||||
+16
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.workflow.service.impl;
|
package org.dromara.workflow.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.convert.Convert;
|
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.FlowHisTaskMapper;
|
||||||
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
|
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
|
||||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||||
|
import org.dromara.workflow.common.constant.FlowPermissionConstant;
|
||||||
import org.dromara.workflow.common.enums.TaskStatusEnum;
|
import org.dromara.workflow.common.enums.TaskStatusEnum;
|
||||||
import org.dromara.workflow.domain.bo.FlowCancelBo;
|
import org.dromara.workflow.domain.bo.FlowCancelBo;
|
||||||
import org.dromara.workflow.domain.bo.FlowInstanceBo;
|
import org.dromara.workflow.domain.bo.FlowInstanceBo;
|
||||||
@@ -214,6 +216,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
|||||||
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
|
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
instances.forEach(this::checkInstanceOwner);
|
||||||
// 获取定义信息
|
// 获取定义信息
|
||||||
Map<Long, Definition> definitionMap = defService.getByIds(
|
Map<Long, Definition> definitionMap = defService.getByIds(
|
||||||
StreamUtils.toList(instances, Instance::getDefinitionId)).stream()
|
StreamUtils.toList(instances, Instance::getDefinitionId)).stream()
|
||||||
@@ -246,6 +249,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
|||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
|
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
|
||||||
}
|
}
|
||||||
|
checkInstanceOwner(instance);
|
||||||
Definition definition = defService.getById(instance.getDefinitionId());
|
Definition definition = defService.getById(instance.getDefinitionId());
|
||||||
if (definition == null) {
|
if (definition == null) {
|
||||||
throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF);
|
throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF);
|
||||||
@@ -267,6 +271,18 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
|||||||
return true;
|
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
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.xg.controller;
|
package org.dromara.xg.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -126,7 +127,7 @@ public class XgStuColBatchClassController extends BaseController {
|
|||||||
List<Long> classIds = xgStuColBatchClassMapper.selectClassIdsByBatchId(batchId);
|
List<Long> classIds = xgStuColBatchClassMapper.selectClassIdsByBatchId(batchId);
|
||||||
return R.ok(classIds);
|
return R.ok(classIds);
|
||||||
} catch (Exception e) {
|
} 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);
|
List<XgStuColBatchClassVo> scopeDetail = xgStuColBatchClassMapper.selectScopeDetailByBatchId(batchId);
|
||||||
return R.ok(scopeDetail);
|
return R.ok(scopeDetail);
|
||||||
} catch (Exception e) {
|
} 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) {
|
} 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);
|
batchName);
|
||||||
return R.ok(batchList);
|
return R.ok(batchList);
|
||||||
} catch (Exception e) {
|
} 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);
|
return R.ok(batchList);
|
||||||
} catch (Exception e) {
|
} 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);
|
return R.ok(filteredTree);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("获取批次组织权限范围失败:" + e.getMessage());
|
return R.fail("获取批次组织权限范围失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.xg.controller;
|
package org.dromara.xg.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -126,7 +127,7 @@ public class XgStudentLeaveController extends BaseController {
|
|||||||
checkDateTime);
|
checkDateTime);
|
||||||
return R.ok(result);
|
return R.ok(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("参数格式错误:" + e.getMessage());
|
return R.fail("参数格式错误:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+24
@@ -44,6 +44,7 @@ import org.dromara.common.core.utils.SpringUtils;
|
|||||||
import org.dromara.common.core.utils.MapstructUtils;
|
import org.dromara.common.core.utils.MapstructUtils;
|
||||||
import org.dromara.common.satoken.utils.LoginHelper;
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.dromara.xg.domain.vo.XgStudentMsgVo;
|
import org.dromara.xg.domain.vo.XgStudentMsgVo;
|
||||||
|
import org.dromara.xg.domain.vo.StudentPwdResetVo;
|
||||||
import org.dromara.xg.domain.bo.XgStudentMsgBo;
|
import org.dromara.xg.domain.bo.XgStudentMsgBo;
|
||||||
import org.dromara.xg.domain.bo.XgStudentMsgAdvancedSearchBo;
|
import org.dromara.xg.domain.bo.XgStudentMsgAdvancedSearchBo;
|
||||||
import org.dromara.xg.service.IXgStudentMsgService;
|
import org.dromara.xg.service.IXgStudentMsgService;
|
||||||
@@ -660,6 +661,29 @@ public class XgStudentMsgController extends BaseController {
|
|||||||
return R.ok(studentInfo);
|
return R.ok(studentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号查询学生基本信息(重置密码页)
|
||||||
|
* 须声明在 /{id} 之前,避免被路径变量吞掉
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("studentMsg:studentPwdReset:query")
|
||||||
|
@GetMapping("/queryByStudentNo/{studentNo}")
|
||||||
|
public R<StudentPwdResetVo> queryByStudentNo(
|
||||||
|
@NotBlank(message = "学号不能为空") @PathVariable("studentNo") String studentNo) {
|
||||||
|
return R.ok(xgStudentMsgService.queryForPwdResetByStudentNo(studentNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号重置学生登录密码为随机临时密码,返回临时密码
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("studentMsg:studentPwdReset:reset")
|
||||||
|
@Log(title = "重置学生密码", businessType = BusinessType.UPDATE, isSaveResponseData = false)
|
||||||
|
@RepeatSubmit()
|
||||||
|
@PutMapping("/resetStudentPassword/{studentNo}")
|
||||||
|
public R<String> resetStudentPassword(
|
||||||
|
@NotBlank(message = "学号不能为空") @PathVariable("studentNo") String studentNo) {
|
||||||
|
return R.ok("操作成功", xgStudentMsgService.resetStudentPasswordByStudentNo(studentNo));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取学生信息详细信息
|
* 获取学生信息详细信息
|
||||||
*
|
*
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package org.dromara.xg.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置学生密码:按学号查询展示信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class StudentPwdResetVo implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 学生ID(与 sys_user.user_id 相同) */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 学号 */
|
||||||
|
private String studentNo;
|
||||||
|
|
||||||
|
/** 姓名 */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** 班级名称 */
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
/** 班主任姓名 */
|
||||||
|
private String classTeacherName;
|
||||||
|
}
|
||||||
+1
-1
@@ -119,7 +119,7 @@ public interface IXgHolidayStayService {
|
|||||||
Boolean revoke(XgHolidayStayAuditBo auditBo);
|
Boolean revoke(XgHolidayStayAuditBo auditBo);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按检查时刻查询学生当前有效的假期留宿(已通过,且日期落在留宿起止日内,结束日当天整天有效)
|
* 按检查时刻查询学生当前有效的假期留宿(已通过,且检查时刻落在留宿起止 datetime 内,含边界)
|
||||||
*
|
*
|
||||||
* @param studentIds 学生ID列表
|
* @param studentIds 学生ID列表
|
||||||
* @param checkDate 检查时刻
|
* @param checkDate 检查时刻
|
||||||
|
|||||||
+17
@@ -3,6 +3,7 @@ package org.dromara.xg.service;
|
|||||||
import org.dromara.xg.domain.vo.XgStudentMsgExportVo;
|
import org.dromara.xg.domain.vo.XgStudentMsgExportVo;
|
||||||
import org.dromara.xg.domain.vo.XgStudentMsgVo;
|
import org.dromara.xg.domain.vo.XgStudentMsgVo;
|
||||||
import org.dromara.xg.domain.vo.XgStudentTreeVo;
|
import org.dromara.xg.domain.vo.XgStudentTreeVo;
|
||||||
|
import org.dromara.xg.domain.vo.StudentPwdResetVo;
|
||||||
import org.dromara.xg.domain.bo.XgStudentMsgBo;
|
import org.dromara.xg.domain.bo.XgStudentMsgBo;
|
||||||
import org.dromara.xg.domain.bo.XgStudentMsgAdvancedSearchBo;
|
import org.dromara.xg.domain.bo.XgStudentMsgAdvancedSearchBo;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
@@ -176,4 +177,20 @@ public interface IXgStudentMsgService {
|
|||||||
* @return 学生信息树
|
* @return 学生信息树
|
||||||
*/
|
*/
|
||||||
List<XgStudentTreeVo> getCurrentUserStudentTree();
|
List<XgStudentTreeVo> getCurrentUserStudentTree();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号查询学生基本信息(重置密码页展示)
|
||||||
|
*
|
||||||
|
* @param studentNo 学号
|
||||||
|
* @return 姓名/学号/班级/班主任
|
||||||
|
*/
|
||||||
|
StudentPwdResetVo queryForPwdResetByStudentNo(String studentNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号重置学生登录密码为随机临时密码,并踢出该学生已登录会话
|
||||||
|
*
|
||||||
|
* @param studentNo 学号
|
||||||
|
* @return 临时密码
|
||||||
|
*/
|
||||||
|
String resetStudentPasswordByStudentNo(String studentNo);
|
||||||
}
|
}
|
||||||
+2
-2
@@ -2579,9 +2579,9 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
|
|||||||
leaveInfoMap.put("leaveEndTime", leaveInfo.getEndTime());
|
leaveInfoMap.put("leaveEndTime", leaveInfo.getEndTime());
|
||||||
studentInfo.put("leaveInfo", leaveInfoMap);
|
studentInfo.put("leaveInfo", leaveInfoMap);
|
||||||
}
|
}
|
||||||
// 请假优先;无请假时再挂假期留宿
|
// 请假与假期留宿可同时生效,一并返回供点名双状态展示
|
||||||
XgHolidayStayVo holidayStay = holidayStayMap.get(studentIdValue);
|
XgHolidayStayVo holidayStay = holidayStayMap.get(studentIdValue);
|
||||||
if (leaveInfo == null && holidayStay != null) {
|
if (holidayStay != null) {
|
||||||
Map<String, Object> holidayStayInfoMap = new LinkedHashMap<>();
|
Map<String, Object> holidayStayInfoMap = new LinkedHashMap<>();
|
||||||
holidayStayInfoMap.put("holidayStayId", holidayStay.getId());
|
holidayStayInfoMap.put("holidayStayId", holidayStay.getId());
|
||||||
holidayStayInfoMap.put("holidayType", holidayStay.getHolidayType());
|
holidayStayInfoMap.put("holidayType", holidayStay.getHolidayType());
|
||||||
|
|||||||
+10
-4
@@ -343,15 +343,21 @@ public class XgDormRollCallServiceImpl implements IXgDormRollCallService {
|
|||||||
|
|
||||||
for (XgDormRollCallDetail detail : allStudentDetails) {
|
for (XgDormRollCallDetail detail : allStudentDetails) {
|
||||||
String status = detail.getRollCallStatus();
|
String status = detail.getRollCallStatus();
|
||||||
if (!"1".equals(status) && !"2".equals(status) && !"4".equals(status)) {
|
boolean isLeave = "1".equals(status) || detail.getLeaveId() != null;
|
||||||
|
boolean isHolidayStay = "4".equals(status) || detail.getHolidayStayId() != null;
|
||||||
|
boolean isAbsent = "2".equals(status);
|
||||||
|
if (!isLeave && !isHolidayStay && !isAbsent) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
DormRollCallTodayDetailItemVo item = toTodayDetailItem(detail, rollCallMap.get(detail.getRollCallId()));
|
DormRollCallTodayDetailItemVo item = toTodayDetailItem(detail, rollCallMap.get(detail.getRollCallId()));
|
||||||
if ("1".equals(status)) {
|
// 请假与假期留宿可同时存在,分别计入两类明细
|
||||||
|
if (isLeave) {
|
||||||
leaveDetails.add(item);
|
leaveDetails.add(item);
|
||||||
} else if ("4".equals(status)) {
|
}
|
||||||
|
if (isHolidayStay) {
|
||||||
holidayStayDetails.add(item);
|
holidayStayDetails.add(item);
|
||||||
} else {
|
}
|
||||||
|
if (isAbsent && !isLeave && !isHolidayStay) {
|
||||||
absentDetails.add(item);
|
absentDetails.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-12
@@ -39,10 +39,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.LocalTime;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1006,7 +1003,8 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 按检查时刻查询学生当前有效的假期留宿。
|
* 按检查时刻查询学生当前有效的假期留宿。
|
||||||
* 条件:已通过;检查日落在 [applyStartDate, applyEndDate](按日期比较,结束日当天整天有效)。
|
* 条件:已通过;检查时刻落在 [applyStartDate, applyEndDate](按 datetime 比较,与请假判定一致)。
|
||||||
|
* 例:结束为 10-25 08:00,则 25 日 21 点点名不再显示假期留宿。
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Map<Long, XgHolidayStayVo> queryStudentHolidayStaysByCheckDate(List<Long> studentIds, LocalDateTime checkDate) {
|
public Map<Long, XgHolidayStayVo> queryStudentHolidayStaysByCheckDate(List<Long> studentIds, LocalDateTime checkDate) {
|
||||||
@@ -1014,18 +1012,13 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService {
|
|||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoneId zone = ZoneId.systemDefault();
|
|
||||||
LocalDate checkDay = checkDate.toLocalDate();
|
|
||||||
Date dayStart = Date.from(checkDay.atStartOfDay(zone).toInstant());
|
|
||||||
Date dayEnd = Date.from(LocalDateTime.of(checkDay, LocalTime.MAX).atZone(zone).toInstant());
|
|
||||||
|
|
||||||
LambdaQueryWrapper<XgHolidayStay> lqw = Wrappers.lambdaQuery();
|
LambdaQueryWrapper<XgHolidayStay> lqw = Wrappers.lambdaQuery();
|
||||||
lqw.in(XgHolidayStay::getStudentId, studentIds);
|
lqw.in(XgHolidayStay::getStudentId, studentIds);
|
||||||
// 仅已通过
|
// 仅已通过
|
||||||
lqw.eq(XgHolidayStay::getApplyStatus, "2");
|
lqw.eq(XgHolidayStay::getApplyStatus, "2");
|
||||||
// 开始日 <= 检查日末,结束日 >= 检查日初(结束日当天整天有效)
|
// 与请假一致:按时刻落在起止区间内(含边界)
|
||||||
lqw.le(XgHolidayStay::getApplyStartDate, dayEnd);
|
lqw.le(XgHolidayStay::getApplyStartDate, checkDate);
|
||||||
lqw.ge(XgHolidayStay::getApplyEndDate, dayStart);
|
lqw.ge(XgHolidayStay::getApplyEndDate, checkDate);
|
||||||
lqw.isNotNull(XgHolidayStay::getApplyStartDate);
|
lqw.isNotNull(XgHolidayStay::getApplyStartDate);
|
||||||
lqw.isNotNull(XgHolidayStay::getApplyEndDate);
|
lqw.isNotNull(XgHolidayStay::getApplyEndDate);
|
||||||
lqw.orderByDesc(XgHolidayStay::getCreateTime);
|
lqw.orderByDesc(XgHolidayStay::getCreateTime);
|
||||||
|
|||||||
+1
-1
@@ -834,6 +834,7 @@ public class XgStudentMsgBatchServiceImpl implements IXgStudentMsgBatchService {
|
|||||||
// 使用前台传递的id
|
// 使用前台传递的id
|
||||||
if (bo.getId() != null) {
|
if (bo.getId() != null) {
|
||||||
add.setId(bo.getId());
|
add.setId(bo.getId());
|
||||||
|
}
|
||||||
add.setDeleted(0L);
|
add.setDeleted(0L);
|
||||||
add.setCreateBy(currentUserId);
|
add.setCreateBy(currentUserId);
|
||||||
add.setCreateTime(new Date());
|
add.setCreateTime(new Date());
|
||||||
@@ -843,7 +844,6 @@ public class XgStudentMsgBatchServiceImpl implements IXgStudentMsgBatchService {
|
|||||||
add.setBatchId(bo.getBatchId());
|
add.setBatchId(bo.getBatchId());
|
||||||
add.setInfoStatus("3");
|
add.setInfoStatus("3");
|
||||||
add.setDataSource("manual");
|
add.setDataSource("manual");
|
||||||
}
|
|
||||||
|
|
||||||
boolean insertResult = baseMapper.insert(add) > 0;
|
boolean insertResult = baseMapper.insert(add) > 0;
|
||||||
|
|
||||||
|
|||||||
+122
@@ -1,10 +1,12 @@
|
|||||||
package org.dromara.xg.service.impl;
|
package org.dromara.xg.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
import org.dromara.common.core.utils.MapstructUtils;
|
import org.dromara.common.core.utils.MapstructUtils;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
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.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
@@ -25,6 +27,8 @@ import org.dromara.system.mapper.SysDeptMapper;
|
|||||||
import org.dromara.system.domain.bo.SysUserBo;
|
import org.dromara.system.domain.bo.SysUserBo;
|
||||||
import org.dromara.system.domain.SysDept;
|
import org.dromara.system.domain.SysDept;
|
||||||
import org.dromara.system.domain.vo.SysDeptVo;
|
import org.dromara.system.domain.vo.SysDeptVo;
|
||||||
|
import org.dromara.system.domain.vo.SysUserVo;
|
||||||
|
import org.dromara.xg.domain.vo.StudentPwdResetVo;
|
||||||
import cn.hutool.crypto.digest.BCrypt;
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1557,4 +1561,122 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService {
|
|||||||
private Long getCurrentUserCollegeId() {
|
private Long getCurrentUserCollegeId() {
|
||||||
return currentUserCollegeHelper.resolveCurrentUserCollegeId();
|
return currentUserCollegeHelper.resolveCurrentUserCollegeId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学院级账号只能操作本学院学生(一年级代理学院可操作全部学生);未归属学院的校级账号不受限
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
if (StringUtils.isBlank(studentNo)) {
|
||||||
|
throw new ServiceException("学号不能为空");
|
||||||
|
}
|
||||||
|
XgStudentMsg student = baseMapper.selectOne(
|
||||||
|
Wrappers.<XgStudentMsg>lambdaQuery()
|
||||||
|
.eq(XgStudentMsg::getStudentNo, studentNo.trim())
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
if (student == null) {
|
||||||
|
throw new ServiceException("未找到学号对应的学生信息");
|
||||||
|
}
|
||||||
|
checkStudentInCurrentUserCollege(student);
|
||||||
|
StudentPwdResetVo vo = new StudentPwdResetVo();
|
||||||
|
vo.setId(student.getId());
|
||||||
|
vo.setStudentNo(student.getStudentNo());
|
||||||
|
vo.setName(student.getName());
|
||||||
|
vo.setClassName(student.getClassName());
|
||||||
|
vo.setClassTeacherName(student.getClassTeacherName());
|
||||||
|
// 学生档案班主任为空时,回退班级表
|
||||||
|
if (StringUtils.isBlank(vo.getClassTeacherName()) && StringUtils.isNotBlank(student.getClassId())) {
|
||||||
|
try {
|
||||||
|
XgClassMsg classMsg = xgClassMsgMapper.selectById(Long.valueOf(student.getClassId()));
|
||||||
|
if (classMsg != null && StringUtils.isNotBlank(classMsg.getClassTeacherName())) {
|
||||||
|
vo.setClassTeacherName(classMsg.getClassTeacherName());
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(vo.getClassName()) && classMsg != null) {
|
||||||
|
vo.setClassName(classMsg.getClassName());
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// classId 非数字时忽略回退
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resetStudentPasswordByStudentNo(String studentNo) {
|
||||||
|
if (StringUtils.isBlank(studentNo)) {
|
||||||
|
throw new ServiceException("学号不能为空");
|
||||||
|
}
|
||||||
|
String no = studentNo.trim();
|
||||||
|
XgStudentMsg student = baseMapper.selectOne(
|
||||||
|
Wrappers.<XgStudentMsg>lambdaQuery()
|
||||||
|
.eq(XgStudentMsg::getStudentNo, no)
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
if (student == null) {
|
||||||
|
throw new ServiceException("未找到学号对应的学生信息");
|
||||||
|
}
|
||||||
|
if (student.getId() == null) {
|
||||||
|
throw new ServiceException("学生数据异常,缺少主键");
|
||||||
|
}
|
||||||
|
checkStudentInCurrentUserCollege(student);
|
||||||
|
SysUserVo user = DataPermissionHelper.ignore(() -> sysUserService.selectUserById(student.getId()));
|
||||||
|
if (user == null) {
|
||||||
|
throw new ServiceException("该学生没有登录账号,无法重置密码");
|
||||||
|
}
|
||||||
|
if (!"2".equals(user.getAccountType())) {
|
||||||
|
throw new ServiceException("该账号不是学生账号,禁止重置");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(user.getUserName()) && !no.equals(user.getUserName())) {
|
||||||
|
throw new ServiceException("学号与登录账号不一致,请联系管理员");
|
||||||
|
}
|
||||||
|
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 tempPassword;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+4
@@ -138,6 +138,10 @@ public class XljkConsultationAppointmentController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/submit")
|
@PostMapping("/submit")
|
||||||
public R<Boolean> submitAppointment(@RequestBody XljkConsultationAppointmentBo bo) {
|
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);
|
boolean result = service.submitAppointment(bo);
|
||||||
if (result) {
|
if (result) {
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
|
|||||||
+5
@@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -64,6 +65,10 @@ public class XljkConsultationNoShowController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/check-limitation")
|
@GetMapping("/check-limitation")
|
||||||
public R<Map<String, Object>> checkLimitation(@RequestParam("studentId") Long studentId) {
|
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));
|
return R.ok(service.checkLimitation(studentId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
@@ -1,5 +1,7 @@
|
|||||||
package org.dromara.xskh.mapper;
|
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.common.mybatis.core.mapper.BaseMapperPlus;
|
||||||
import org.dromara.xskh.domain.XskhAssessmentApplicationApproval;
|
import org.dromara.xskh.domain.XskhAssessmentApplicationApproval;
|
||||||
import org.dromara.xskh.domain.vo.XskhAssessmentApplicationApprovalVo;
|
import org.dromara.xskh.domain.vo.XskhAssessmentApplicationApprovalVo;
|
||||||
@@ -26,4 +28,16 @@ public interface XskhAssessmentApplicationApprovalMapper
|
|||||||
List<org.dromara.xskh.domain.vo.XskhAssessmentSelfScoreVo> selectSelfScoresWithIndicatorInfo(
|
List<org.dromara.xskh.domain.vo.XskhAssessmentSelfScoreVo> selectSelfScoresWithIndicatorInfo(
|
||||||
Long applicationId);
|
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
@@ -1,7 +1,9 @@
|
|||||||
package org.dromara.xskh.service.impl;
|
package org.dromara.xskh.service.impl;
|
||||||
|
|
||||||
import org.dromara.common.core.utils.MapstructUtils;
|
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.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.TableDataInfo;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@@ -55,6 +57,9 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
@Override
|
@Override
|
||||||
public XskhAssessmentApplicationApprovalVo queryById(Long id) {
|
public XskhAssessmentApplicationApprovalVo queryById(Long id) {
|
||||||
XskhAssessmentApplicationApprovalVo vo = baseMapper.selectVoById(id);
|
XskhAssessmentApplicationApprovalVo vo = baseMapper.selectVoById(id);
|
||||||
|
if (vo != null && isCurrentUserStudent() && !StringUtils.equals(vo.getStudentNo(), LoginHelper.getUsername())) {
|
||||||
|
throw new ServiceException("无权查看他人的考核申请");
|
||||||
|
}
|
||||||
if (vo != null) {
|
if (vo != null) {
|
||||||
// 查询完整的自评得分列表(包含指标表关联数据)
|
// 查询完整的自评得分列表(包含指标表关联数据)
|
||||||
List<XskhAssessmentSelfScoreVo> selfScoreVos = baseMapper.selectSelfScoresWithIndicatorInfo(id);
|
List<XskhAssessmentSelfScoreVo> selfScoreVos = baseMapper.selectSelfScoresWithIndicatorInfo(id);
|
||||||
@@ -110,6 +115,14 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Long insertByBo(XskhAssessmentApplicationApprovalBo bo) {
|
public Long insertByBo(XskhAssessmentApplicationApprovalBo bo) {
|
||||||
XskhAssessmentApplicationApproval add = MapstructUtils.convert(bo, XskhAssessmentApplicationApproval.class);
|
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);
|
validEntityBeforeSave(add);
|
||||||
// 新增时默认状态为草稿
|
// 新增时默认状态为草稿
|
||||||
add.setStatus("1");
|
add.setStatus("1");
|
||||||
@@ -130,7 +143,23 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Boolean updateByBo(XskhAssessmentApplicationApprovalBo bo) {
|
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);
|
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);
|
validEntityBeforeSave(update);
|
||||||
boolean flag = baseMapper.updateById(update) > 0;
|
boolean flag = baseMapper.updateById(update) > 0;
|
||||||
if (flag) {
|
if (flag) {
|
||||||
@@ -149,6 +178,32 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
// TODO 做一些数据校验,如唯一约束
|
// 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) {
|
for (Long id : ids) {
|
||||||
XskhAssessmentApplicationApproval application = baseMapper.selectById(id);
|
XskhAssessmentApplicationApproval application = baseMapper.selectById(id);
|
||||||
|
if (application != null) {
|
||||||
|
checkOwner(application);
|
||||||
|
}
|
||||||
if (application != null && !"1".equals(application.getStatus())) {
|
if (application != null && !"1".equals(application.getStatus())) {
|
||||||
throw new RuntimeException("只有草稿状态的考核申请可以删除");
|
throw new RuntimeException("只有草稿状态的考核申请可以删除");
|
||||||
}
|
}
|
||||||
@@ -181,6 +239,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
throw new RuntimeException("考核申请不存在");
|
throw new RuntimeException("考核申请不存在");
|
||||||
}
|
}
|
||||||
|
checkOwner(existing);
|
||||||
if (!"1".equals(existing.getStatus()) && !"2".equals(existing.getStatus())
|
if (!"1".equals(existing.getStatus()) && !"2".equals(existing.getStatus())
|
||||||
&& !"3".equals(existing.getStatus())) {
|
&& !"3".equals(existing.getStatus())) {
|
||||||
throw new RuntimeException("只有草稿、已提交、班主任审核状态的考核申请可以保存");
|
throw new RuntimeException("只有草稿、已提交、班主任审核状态的考核申请可以保存");
|
||||||
@@ -201,6 +260,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
if (application == null) {
|
if (application == null) {
|
||||||
throw new RuntimeException("考核申请不存在");
|
throw new RuntimeException("考核申请不存在");
|
||||||
}
|
}
|
||||||
|
checkOwner(application);
|
||||||
if (!"1".equals(application.getStatus()) && !"2".equals(application.getStatus())
|
if (!"1".equals(application.getStatus()) && !"2".equals(application.getStatus())
|
||||||
&& !"3".equals(application.getStatus())) {
|
&& !"3".equals(application.getStatus())) {
|
||||||
throw new RuntimeException("只有草稿、已提交、班主任审核状态的考核申请可以提交");
|
throw new RuntimeException("只有草稿、已提交、班主任审核状态的考核申请可以提交");
|
||||||
@@ -221,6 +281,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
if (application == null) {
|
if (application == null) {
|
||||||
throw new RuntimeException("考核申请不存在");
|
throw new RuntimeException("考核申请不存在");
|
||||||
}
|
}
|
||||||
|
checkOwner(application);
|
||||||
if (!"1".equals(application.getStatus())) {
|
if (!"1".equals(application.getStatus())) {
|
||||||
throw new RuntimeException("只有草稿状态的考核申请可以删除");
|
throw new RuntimeException("只有草稿状态的考核申请可以删除");
|
||||||
}
|
}
|
||||||
@@ -283,6 +344,7 @@ public class XskhAssessmentApplicationApprovalServiceImpl implements IXskhAssess
|
|||||||
wrapper.eq(XskhAssessmentApplicationApproval::getScheduleId, scheduleId);
|
wrapper.eq(XskhAssessmentApplicationApproval::getScheduleId, scheduleId);
|
||||||
XskhAssessmentApplicationApproval application = baseMapper.selectOne(wrapper);
|
XskhAssessmentApplicationApproval application = baseMapper.selectOne(wrapper);
|
||||||
if (application != null) {
|
if (application != null) {
|
||||||
|
checkOwner(application);
|
||||||
return queryById(application.getId());
|
return queryById(application.getId());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+7
-6
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.zsjy.controller;
|
package org.dromara.zsjy.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
@@ -130,7 +131,7 @@ public class InternshipInfoController extends BaseController {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取学生实习信息异常,用户ID: {},错误信息: {}", LoginHelper.getUserId(), e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("新增实习信息异常,学生ID:{},错误信息:{}", addBO.getStudentId(), e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("修改实习信息异常,实习信息ID:{},错误信息:{}", editBO.getInternshipId(), e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("删除实习信息异常,IDs:{},错误信息:{}", internshipIds, e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("获取当前学生实习信息失败:{}", e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("实习信息变更异常,原实习ID:{},错误信息:{}",
|
log.error("实习信息变更异常,原实习ID:{},错误信息:{}",
|
||||||
changeBO.getInternshipId(), e.getMessage(), e);
|
changeBO.getInternshipId(), e.getMessage(), e);
|
||||||
return R.fail("实习信息变更失败:" + e.getMessage());
|
return R.fail("实习信息变更失败:" + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.zsjy.controller;
|
package org.dromara.zsjy.controller;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -179,7 +180,7 @@ public class ZsjyEnrollmentInfoController extends BaseController {
|
|||||||
List<ZsjyEnrollmentDeptVo> colleges = enrollmentMajorService.selectOpenColleges();
|
List<ZsjyEnrollmentDeptVo> colleges = enrollmentMajorService.selectOpenColleges();
|
||||||
return R.ok(colleges);
|
return R.ok(colleges);
|
||||||
} catch (Exception e) {
|
} 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);
|
List<ZsjyEnrollmentDeptVo> majors = enrollmentMajorService.selectOpenMajorsByCollegeId(collegeId);
|
||||||
return R.ok(majors);
|
return R.ok(majors);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return R.fail("获取专业列表异常: " + e.getMessage());
|
return R.fail("获取专业列表异常: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.constraints.*;
|
import jakarta.validation.constraints.*;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.annotation.SaMode;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
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)
|
@Log(title = "新生录取名册", businessType = BusinessType.EXPORT)
|
||||||
@PostMapping("/exportRegister")
|
@PostMapping("/exportRegister")
|
||||||
public void exportRegister(ZsjyEnrollmentProcessBo bo, HttpServletResponse response) {
|
public void exportRegister(ZsjyEnrollmentProcessBo bo, HttpServletResponse response) {
|
||||||
|
|||||||
+5
-4
@@ -1,5 +1,6 @@
|
|||||||
package org.dromara.zsjy.service.impl;
|
package org.dromara.zsjy.service.impl;
|
||||||
|
|
||||||
|
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
@@ -156,7 +157,7 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("学生签到失败: {}", e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("获取考勤日历数据失败: {}", e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("获取考勤统计数据失败: {}", e.getMessage(), 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) {
|
} catch (Exception e) {
|
||||||
log.error("获取学生月度统计数据失败: studentId={}, year={}, month={}, error={}",
|
log.error("获取学生月度统计数据失败: studentId={}, year={}, month={}, error={}",
|
||||||
studentId, year, month, e.getMessage(), e);
|
studentId, year, month, e.getMessage(), e);
|
||||||
return R.fail("获取学生月度统计数据失败: " + e.getMessage());
|
return R.fail("获取学生月度统计数据失败: " + GlobalExceptionHandler.safeMessage(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- 重置学生密码:菜单 + 按钮权限
|
||||||
|
-- 日期:2026-10-26
|
||||||
|
-- 父菜单:学生管理 (1959499382894141441)
|
||||||
|
-- Web 页面:xg/xsjcxx/studentMsg/studentPwdReset/index
|
||||||
|
-- 移动端首页入口权限:student:pwdreset:listapp
|
||||||
|
-- 重置后返回随机临时密码(仅显示一次),并踢出该学生已登录会话
|
||||||
|
-- 执行:开发/生产库均可幂等执行;执行后给角色勾选菜单或跑下方角色授权段
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- 1. 菜单页(学生管理下级)
|
||||||
|
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 2026102610000000101, '重置学生密码', 1959499382894141441, 16, 'studentPwdReset',
|
||||||
|
'xg/xsjcxx/studentMsg/studentPwdReset/index', NULL,
|
||||||
|
1, 0, 'C', '0', '0', 'studentMsg:studentPwdReset:list', 'password',
|
||||||
|
103, 1, NOW(), '按学号查询并重置学生登录密码'
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `menu_id` = 2026102610000000101);
|
||||||
|
|
||||||
|
-- 2. 按钮:查询
|
||||||
|
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 2026102610000000102, '查询学生', 2026102610000000101, 1, '#', '', NULL,
|
||||||
|
1, 0, 'F', '0', '0', 'studentMsg:studentPwdReset:query', '#',
|
||||||
|
103, 1, NOW(), '按学号查询学生信息'
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `menu_id` = 2026102610000000102);
|
||||||
|
|
||||||
|
-- 3. 按钮:重置密码
|
||||||
|
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 2026102610000000103, '确认重置', 2026102610000000101, 2, '#', '', NULL,
|
||||||
|
1, 0, 'F', '0', '0', 'studentMsg:studentPwdReset:reset', '#',
|
||||||
|
103, 1, NOW(), '确认重置学生密码'
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `menu_id` = 2026102610000000103);
|
||||||
|
|
||||||
|
-- 4. 移动端首页入口权限(挂在同一菜单页下,供 hasPermi 控制入口可见)
|
||||||
|
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 2026102610000000104, '重置学生密码(移动端)', 2026102610000000101, 3, '', '', NULL,
|
||||||
|
1, 0, 'F', '0', '0', 'student:pwdreset:listapp', '#',
|
||||||
|
103, 1, NOW(), '移动端首页入口权限'
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `menu_id` = 2026102610000000104);
|
||||||
|
|
||||||
|
-- 5. 给已有「学生信息」菜单权限的角色补授权(幂等)
|
||||||
|
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||||
|
SELECT DISTINCT rm.role_id, m.menu_id
|
||||||
|
FROM `sys_role_menu` rm
|
||||||
|
CROSS JOIN (
|
||||||
|
SELECT 2026102610000000101 AS menu_id UNION ALL
|
||||||
|
SELECT 2026102610000000102 UNION ALL
|
||||||
|
SELECT 2026102610000000103 UNION ALL
|
||||||
|
SELECT 2026102610000000104
|
||||||
|
) m
|
||||||
|
WHERE rm.menu_id = 1948289114628902914
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_role_menu` x
|
||||||
|
WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 核对
|
||||||
|
SELECT menu_id, menu_name, parent_id, path, component, perms, menu_type
|
||||||
|
FROM sys_menu
|
||||||
|
WHERE menu_id BETWEEN 2026102610000000101 AND 2026102610000000104
|
||||||
|
ORDER BY menu_id;
|
||||||
@@ -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;
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
# 杭州第一技师 — Drone Tag 三端打包设计
|
# 杭州第一技师 — Drone Tag 打包设计
|
||||||
|
|
||||||
## 背景
|
## 背景
|
||||||
|
|
||||||
在测试机既有 Gogs + Drone(`101.35.245.122:18088`)上,为仓库 `hzdyjs-xg/all-project` 增加 **仅 tag 触发** 的自动打包,产出后端 / Web / 移动端 H5 三份独立产物,并复用宿主机 Maven 本地仓库以加速 Java 构建。
|
在测试机既有 Gogs + Drone(`101.35.245.122:18088`)上,为仓库 `hzdyjs-xg/all-project` 增加 **仅 tag 触发** 的自动打包。因测试机约 3.6G 内存不足以完成 web Vite 生产构建(实测约 36 分钟后 OOM),**CI 当前只打包 backend + mobile-h5**;web 暂缓,后续另议。
|
||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
- Push Git tag → 自动顺序构建三端并落盘
|
- Push Git tag → 自动顺序构建 backend、mobile-h5 并落盘
|
||||||
- 产物可按 tag 并存,便于回溯
|
- 产物可按 tag 并存,便于回溯
|
||||||
- Maven 依赖缓存在宿主机,不因容器销毁而丢失
|
- Maven 依赖缓存在宿主机,不因容器销毁而丢失
|
||||||
|
|
||||||
## 非目标
|
## 非目标
|
||||||
|
|
||||||
- 不自动部署到业务机
|
- 不自动部署到业务机
|
||||||
|
- CI 暂不打包 web(可本地 `npm run build:prod` / `build:ci`)
|
||||||
- 不打微信小程序(`build:mp-weixin`);移动端按 `pnpm run build`(H5)
|
- 不打微信小程序(`build:mp-weixin`);移动端按 `pnpm run build`(H5)
|
||||||
- 不合成单一总包
|
- 不合成单一总包
|
||||||
|
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
```
|
```
|
||||||
/root/hzdyjs-releases/<tag>/
|
/root/hzdyjs-releases/<tag>/
|
||||||
backend-<tag>.jar
|
backend-<tag>.jar
|
||||||
web-<tag>.tar.gz
|
|
||||||
mobile-h5-<tag>.tar.gz
|
mobile-h5-<tag>.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -42,13 +42,9 @@
|
|||||||
`mvn clean package -Pprod -Dmaven.test.skip=true -Dmaven.compiler.parameters=true -Dmaven.repo.local=/maven-repo`
|
`mvn clean package -Pprod -Dmaven.test.skip=true -Dmaven.compiler.parameters=true -Dmaven.repo.local=/maven-repo`
|
||||||
源文件:`backend/ruoyi-admin/target/ruoyi-admin.jar` → `backend-<tag>.jar`
|
源文件:`backend/ruoyi-admin/target/ruoyi-admin.jar` → `backend-<tag>.jar`
|
||||||
|
|
||||||
2. **web**(`node:20-bookworm`)
|
2. **mobile**(`node:20-bookworm` + corepack/pnpm)
|
||||||
`npm install` → `npm run build:prod`
|
`pnpm install --frozen-lockfile`(仓库纳入 `mobile/pnpm-lock.yaml`;无效 `pnpm-workspace.yaml` 已删除)
|
||||||
(`web/package-lock.json` 被 gitignore,仓库无 lock,不能用 `npm ci`)
|
→ `pnpm run build`(H5);CI 设置 `PUPPETEER_SKIP_DOWNLOAD=1`
|
||||||
打包 `web/dist` → `web-<tag>.tar.gz`
|
|
||||||
|
|
||||||
3. **mobile**(`node:20-bookworm` + corepack/pnpm)
|
|
||||||
`pnpm install --frozen-lockfile` → `pnpm run build`(即 H5)
|
|
||||||
打包 `mobile/dist/build/h5` → `mobile-h5-<tag>.tar.gz`
|
打包 `mobile/dist/build/h5` → `mobile-h5-<tag>.tar.gz`
|
||||||
|
|
||||||
## Maven 公共缓存
|
## Maven 公共缓存
|
||||||
@@ -72,11 +68,11 @@
|
|||||||
|
|
||||||
## 失败策略
|
## 失败策略
|
||||||
|
|
||||||
任一步失败则整次构建失败;不部分覆盖同 tag 目录中已有文件以外的内容时,实现上可先 `mkdir -p` 再逐步写入——若中途失败,该 tag 目录可能不完整,以 Drone 构建状态为准。
|
任一步失败则整次构建失败;实现上可先 `mkdir -p` 再逐步写入——若中途失败,该 tag 目录可能不完整,以 Drone 构建状态为准。
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|
||||||
1. 推送测试 tag(如 `ci-pkg-test-YYYYMMDDHHMM`)
|
1. 推送测试 tag(如 `ci-pkg-test-YYYYMMDDHHMM`)
|
||||||
2. Drone 构建 success
|
2. Drone 构建 success
|
||||||
3. 三文件均存在于 `/root/hzdyjs-releases/<tag>/`,且 jar/tar 非空
|
3. `backend-*.jar` 与 `mobile-h5-*.tar.gz` 均存在于 `/root/hzdyjs-releases/<tag>/`,且非空(无 web 包)
|
||||||
4. 第二次同仓库构建时 Maven 日志出现大量依赖来自本地仓库(明显快于首次)
|
4. 第二次同仓库构建时 Maven 日志出现大量依赖来自本地仓库(明显快于首次)
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
## API模块列表
|
## API模块列表
|
||||||
- [学生信息管理API](./学生信息管理API.md)
|
- [学生信息管理API](./学生信息管理API.md)
|
||||||
|
- [学生密码重置API](./学生密码重置API.md) - **新增**:按学号查询并重置学生登录密码为固定默认值(2026-10-26)
|
||||||
- [学生信息收集批次管理API](./学生信息收集批次管理API.md)
|
- [学生信息收集批次管理API](./学生信息收集批次管理API.md)
|
||||||
- [学生信息收集批次API](./学生信息收集批次API.md) - **重要更新**:修改列表排序方式为按创建时间倒序(2025-01-03)
|
- [学生信息收集批次API](./学生信息收集批次API.md) - **重要更新**:修改列表排序方式为按创建时间倒序(2025-01-03)
|
||||||
- [班级信息管理API](./班级信息管理API.md)
|
- [班级信息管理API](./班级信息管理API.md)
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# 学生密码重置 API 接口文档
|
||||||
|
|
||||||
|
## 模块概述
|
||||||
|
|
||||||
|
**模块名称**:Student Password Reset / 学生密码重置
|
||||||
|
**基础路径**:`/studentMsg/studentMsg`
|
||||||
|
**版本**:v1.0
|
||||||
|
**负责人**:学工模块
|
||||||
|
**更新时间**:2026-10-26
|
||||||
|
|
||||||
|
### 模块功能
|
||||||
|
|
||||||
|
学工老师按学号查询学生基本信息后,一键将对应学生登录账号密码重置为服务端随机生成的临时密码(2026-10-26 安全整改:取消固定默认密码)。
|
||||||
|
**不复用** `system:user:resetPwd`,避免把系统用户管理权下放给学工老师。
|
||||||
|
|
||||||
|
**核心约束**:
|
||||||
|
- 仅允许重置 `sys_user.account_type = '2'`(学生账号)
|
||||||
|
- 要求 `user_id = xg_student_msg.id`,且 `user_name` 与学号一致(若有用户名)
|
||||||
|
- 临时密码由服务端 SecureRandom 生成(10 位,含大小写、数字、特殊字符),前端不可传入自定义密码
|
||||||
|
- 数据范围:超管、校级账号(无学院)可操作全部学生;学院账号只能查询/重置本学院(或一年级代理学院)的学生
|
||||||
|
- 重置后立即踢下线该学生的所有在线会话
|
||||||
|
- 操作日志不记录响应体,临时密码不落库日志
|
||||||
|
|
||||||
|
**临时密码**:仅在本次接口响应 `data` 中返回一次,由老师当面告知学生,学生登录后应自行修改。
|
||||||
|
|
||||||
|
### 权限说明
|
||||||
|
|
||||||
|
| 权限标识 | 说明 |
|
||||||
|
| -------- | ---- |
|
||||||
|
| `studentMsg:studentPwdReset:list` | Web 菜单页可见 |
|
||||||
|
| `studentMsg:studentPwdReset:query` | 按学号查询 |
|
||||||
|
| `studentMsg:studentPwdReset:reset` | 确认重置密码 |
|
||||||
|
| `student:pwdreset:listapp` | 移动端首页入口可见 |
|
||||||
|
|
||||||
|
菜单脚本:`backend/script/sql/update/update_20261026_student_pwd_reset_menu.sql`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口概览
|
||||||
|
|
||||||
|
| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 |
|
||||||
|
| -------- | --------- | ---- | -------- | -------- |
|
||||||
|
| 按学号查询学生 | GET | `/studentMsg/studentMsg/queryByStudentNo/{studentNo}` | `studentMsg:studentPwdReset:query` | 返回姓名、学号、班级、班主任 |
|
||||||
|
| 重置学生密码 | PUT | `/studentMsg/studentMsg/resetStudentPassword/{studentNo}` | `studentMsg:studentPwdReset:reset` | 重置为随机临时密码 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 详细接口说明
|
||||||
|
|
||||||
|
### 1. 按学号查询学生(重置密码页)
|
||||||
|
|
||||||
|
- **接口路径**:`GET /studentMsg/studentMsg/queryByStudentNo/{studentNo}`
|
||||||
|
- **权限标识**:`studentMsg:studentPwdReset:query`
|
||||||
|
- **功能描述**:按学号查询学生档案,用于重置前确认身份;班主任姓名优先取学生档案,为空时回退班级表
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| ------ | ---- | ---- | ---- |
|
||||||
|
| studentNo | string | 是 | 学号 |
|
||||||
|
|
||||||
|
#### 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1234567890123456789,
|
||||||
|
"studentNo": "202513063048",
|
||||||
|
"name": "张三",
|
||||||
|
"className": "计应2401",
|
||||||
|
"classTeacherName": "李老师"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 错误场景
|
||||||
|
|
||||||
|
| 场景 | 提示 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 学号为空 | 学号不能为空 |
|
||||||
|
| 学号不存在 | 未找到学号对应的学生信息 |
|
||||||
|
| 无权限 | 403 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 重置学生密码
|
||||||
|
|
||||||
|
- **接口路径**:`PUT /studentMsg/studentMsg/resetStudentPassword/{studentNo}`
|
||||||
|
- **权限标识**:`studentMsg:studentPwdReset:reset`
|
||||||
|
- **功能描述**:将学号对应学生登录密码重置为随机临时密码并踢下线;写操作日志「重置学生密码」(不记录响应)
|
||||||
|
- **防重复提交**:`@RepeatSubmit`
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| ------ | ---- | ---- | ---- |
|
||||||
|
| studentNo | string | 是 | 学号 |
|
||||||
|
|
||||||
|
#### 请求体
|
||||||
|
|
||||||
|
无。临时密码由服务端随机生成并 BCrypt 后写入,不允许客户端传密。
|
||||||
|
|
||||||
|
#### 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "操作成功",
|
||||||
|
"data": "Abcdef@123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 错误场景
|
||||||
|
|
||||||
|
| 场景 | 提示 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 学号不存在 | 未找到学号对应的学生信息 |
|
||||||
|
| 无登录账号 | 该学生没有登录账号,无法重置密码 |
|
||||||
|
| 非学生账号 | 该账号不是学生账号,禁止重置 |
|
||||||
|
| 学号与登录名不一致 | 学号与登录账号不一致,请联系管理员 |
|
||||||
|
| 非本学院学生 | 只能操作本学院的学生 |
|
||||||
|
| 无权限 | 403 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端与移动端
|
||||||
|
|
||||||
|
| 端 | 路径 / 入口 |
|
||||||
|
| -- | ----------- |
|
||||||
|
| Web | `web/src/views/xg/xsjcxx/studentMsg/studentPwdReset/index.vue` |
|
||||||
|
| Web API | `web/src/api/xg/xsjcxx/studentMsg/studentMsg/index.ts` → `queryStudentForPwdReset` / `resetStudentPassword` |
|
||||||
|
| 移动端页面 | `mobile/src/pages/xg/xsjcxx/studentPwdReset/index.vue` |
|
||||||
|
| 移动端 API | `mobile/src/api/studentPwdReset.js` |
|
||||||
|
| 移动端首页 | 学工管理 →「重置学生密码」(`student:pwdreset:listapp`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验收要点
|
||||||
|
|
||||||
|
1. 有权限账号:学号查得到 → 重置成功 → 该学号可用默认密码登录
|
||||||
|
2. 无权限账号:接口 403
|
||||||
|
3. 不存在学号 / 无用户 / 非学生账号:友好错误,不改密
|
||||||
|
4. Web、移动端流程一致:输入学号 → 查询 → 确认 → 重置
|
||||||
@@ -7,16 +7,24 @@
|
|||||||
|
|
||||||
## 有效留宿判定(后端统一)
|
## 有效留宿判定(后端统一)
|
||||||
|
|
||||||
接口:`getRoomStudents` 在返回学生信息时附加 `holidayStayInfo`(有有效请假时不返回,请假优先)。
|
接口:`getRoomStudents` 在返回学生信息时附加 `leaveInfo` / `holidayStayInfo`(**可同时返回**)。
|
||||||
|
|
||||||
条件:
|
条件(假期留宿):
|
||||||
|
|
||||||
1. `apply_status = '2'`(已通过)
|
1. `apply_status = '2'`(已通过)
|
||||||
2. 检查日落在 `[apply_start_date, apply_end_date]`(按日期比较,**结束日当天整天有效**)
|
2. 检查**时刻**落在 `[apply_start_date, apply_end_date]`(按 datetime 比较,与请假一致,含边界)
|
||||||
3. 同一学生多条记录时取最新一条
|
3. 同一学生多条记录时取最新一条
|
||||||
|
|
||||||
|
例:留宿结束为 `2026-10-25 08:00:00`,则 25 日 21 点宿舍点名**不再**显示「假期留宿」,恢复默认「在寝」。
|
||||||
|
|
||||||
|
若同一学生当前既有有效请假又有有效假期留宿:点名页**同时展示两个标签**;落库主状态仍为请假(`1`),并同时写入 `leave_*` 与 `holiday_*` 字段;今日汇总两类明细均可计入该生。
|
||||||
|
|
||||||
假期结束后,接口不再返回 `holidayStayInfo`,前端恢复默认「在寝」可切换。
|
假期结束后,接口不再返回 `holidayStayInfo`,前端恢复默认「在寝」可切换。
|
||||||
|
|
||||||
|
> **变更说明(2026-10-26)**:
|
||||||
|
> 1. 原先按「结束日当天整天有效」判定 → 已改为按时刻判定。
|
||||||
|
> 2. 原先请假优先时不返回假期留宿 → 已改为两者可同时展示。
|
||||||
|
|
||||||
## 点名状态字典 `xg_dorm_roll_call_status`
|
## 点名状态字典 `xg_dorm_roll_call_status`
|
||||||
|
|
||||||
| 值 | 含义 |
|
| 值 | 含义 |
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ npm install
|
|||||||
npm run build:prod
|
npm run build:prod
|
||||||
```
|
```
|
||||||
|
|
||||||
|
构建脚本将 Node 堆上限设为 6144MB。4096MB 会在 transform 阶段被活对象占满(约 4091MB,GC 几乎无法回收)并报 `JavaScript heap out of memory`。本机物理内存为 8GB,因此不用 8192MB。同时限制 Rollup 并行读文件数,避免打包时把文件一次性读进内存。Element Plus 与 animate.css 改为普通 CSS 引入,不再经由 Sass 编译。构建已关闭控制台 gzip 体积统计,且小于 10KB 的分包不再预生成 `.gz`(较大的 js/css 仍会生成,供 nginx `gzip_static` 使用)。Element Plus 样式全量引入,打包时不再按组件重复引入样式。
|
||||||
|
|
||||||
### 步骤2:验证构建产物
|
### 步骤2:验证构建产物
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Generated
+14966
File diff suppressed because it is too large.
Load diff
@@ -1,5 +0,0 @@
|
|||||||
allowBuilds:
|
|
||||||
'@parcel/watcher': set this to true or false
|
|
||||||
core-js: set this to true or false
|
|
||||||
fsevents: set this to true or false
|
|
||||||
puppeteer: set this to true or false
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 重置学生密码 API
|
||||||
|
*/
|
||||||
|
import { request } from '@/common/request.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号查询学生基本信息(重置密码页)
|
||||||
|
* @param {string} studentNo 学号
|
||||||
|
*/
|
||||||
|
export const queryStudentForPwdReset = (studentNo) => {
|
||||||
|
return request({
|
||||||
|
url: `/studentMsg/studentMsg/queryByStudentNo/${encodeURIComponent(studentNo)}`,
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号重置学生登录密码为随机临时密码,data 为临时密码
|
||||||
|
* @param {string} studentNo 学号
|
||||||
|
*/
|
||||||
|
export const resetStudentPassword = (studentNo) => {
|
||||||
|
return request({
|
||||||
|
url: `/studentMsg/studentMsg/resetStudentPassword/${encodeURIComponent(studentNo)}`,
|
||||||
|
method: 'PUT'
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -65,6 +65,14 @@
|
|||||||
"navigationBarTextStyle": "white"
|
"navigationBarTextStyle": "white"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/xg/xsjcxx/studentPwdReset/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "重置学生密码",
|
||||||
|
"navigationBarBackgroundColor": "#0051d9",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/xg/xsjcxx/xsws/index",
|
"path": "pages/xg/xsjcxx/xsws/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -86,6 +86,15 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ServiceItem>
|
</ServiceItem>
|
||||||
|
<ServiceItem
|
||||||
|
v-if="hasPermi(['student:pwdreset:listapp'])"
|
||||||
|
title="重置学生密码"
|
||||||
|
@click="gotoStudentPwdReset"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<u-icon name="lock-open" size="24" color="#1890ff" />
|
||||||
|
</template>
|
||||||
|
</ServiceItem>
|
||||||
<!-- 卡片结束 -->
|
<!-- 卡片结束 -->
|
||||||
<!-- <ServiceItem
|
<!-- <ServiceItem
|
||||||
v-if="hasPermi(['student:dorm:list'])"
|
v-if="hasPermi(['student:dorm:list'])"
|
||||||
@@ -776,6 +785,7 @@ export default {
|
|||||||
hasStudentServices() {
|
hasStudentServices() {
|
||||||
return (
|
return (
|
||||||
this.hasPermi(['student:info:listapp']) ||
|
this.hasPermi(['student:info:listapp']) ||
|
||||||
|
this.hasPermi(['student:pwdreset:listapp']) ||
|
||||||
this.hasPermi(['student:dorm:list']) ||
|
this.hasPermi(['student:dorm:list']) ||
|
||||||
this.hasPermi(['student:leave:listapp']) ||
|
this.hasPermi(['student:leave:listapp']) ||
|
||||||
this.hasPermi(['student:psychology:list']) ||
|
this.hasPermi(['student:psychology:list']) ||
|
||||||
@@ -1203,6 +1213,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 跳转到重置学生密码页面
|
||||||
|
gotoStudentPwdReset() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/xg/xsjcxx/studentPwdReset/index',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// 跳转到假期留宿申请(学生)
|
// 跳转到假期留宿申请(学生)
|
||||||
gotoHolidayStayApply() {
|
gotoHolidayStayApply() {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<view class="pwd-reset-page">
|
||||||
|
<view class="search-card">
|
||||||
|
<view class="form-row">
|
||||||
|
<text class="label">学号</text>
|
||||||
|
<u-input
|
||||||
|
v-model="studentNo"
|
||||||
|
placeholder="请输入学生学号"
|
||||||
|
clearable
|
||||||
|
maxlength="32"
|
||||||
|
border="surround"
|
||||||
|
:custom-style="{ background: '#fff' }"
|
||||||
|
@confirm="handleQuery"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
text="查询"
|
||||||
|
:loading="queryLoading"
|
||||||
|
:custom-style="{ marginTop: '24rpx' }"
|
||||||
|
@click="handleQuery"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="student" class="info-card">
|
||||||
|
<view class="card-title">学生信息</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">姓名</text>
|
||||||
|
<text class="info-value">{{ student.name || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">学号</text>
|
||||||
|
<text class="info-value">{{ student.studentNo || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">班级</text>
|
||||||
|
<text class="info-value">{{ student.className || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">班主任</text>
|
||||||
|
<text class="info-value">{{ student.classTeacherName || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<u-button
|
||||||
|
type="error"
|
||||||
|
text="重置密码"
|
||||||
|
:loading="resetLoading"
|
||||||
|
:custom-style="{ marginTop: '32rpx' }"
|
||||||
|
@click="handleReset"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="queried && !queryLoading" class="empty-tip">
|
||||||
|
<u-empty mode="search" text="请输入学号后查询" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
queryStudentForPwdReset,
|
||||||
|
resetStudentPassword
|
||||||
|
} from '@/api/studentPwdReset.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
studentNo: '',
|
||||||
|
student: null,
|
||||||
|
queried: false,
|
||||||
|
queryLoading: false,
|
||||||
|
resetLoading: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleQuery() {
|
||||||
|
const no = (this.studentNo || '').trim();
|
||||||
|
if (!no) {
|
||||||
|
uni.showToast({ title: '请输入学号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queryLoading = true;
|
||||||
|
this.queried = true;
|
||||||
|
this.student = null;
|
||||||
|
try {
|
||||||
|
const res = await queryStudentForPwdReset(no);
|
||||||
|
const payload =
|
||||||
|
res && res.data && typeof res.data === 'object' && !Array.isArray(res.data)
|
||||||
|
? res.data
|
||||||
|
: res;
|
||||||
|
if (!payload || (!payload.studentNo && !payload.name && payload.id == null)) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (res && res.msg) || '未查询到学生信息',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
this.student = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.student = payload;
|
||||||
|
this.studentNo = no;
|
||||||
|
} catch (e) {
|
||||||
|
this.student = null;
|
||||||
|
} finally {
|
||||||
|
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' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = this.student.name || '';
|
||||||
|
const no = this.student.studentNo;
|
||||||
|
uni.showModal({
|
||||||
|
title: '重置密码确认',
|
||||||
|
content: `确认将学号「${no}」(${name})的登录密码重置为随机临时密码?重置后该学生需重新登录。`,
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
success: async (modalRes) => {
|
||||||
|
if (!modalRes.confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.resetLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await resetStudentPassword(no);
|
||||||
|
this.showResetResult(String((res && res.data) || ''));
|
||||||
|
} catch (e) {
|
||||||
|
// 错误提示由 request 拦截器处理
|
||||||
|
} finally {
|
||||||
|
this.resetLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.pwd-reset-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f6f8;
|
||||||
|
padding: 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card,
|
||||||
|
.info-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row .label {
|
||||||
|
width: 80rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 18rpx 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #909399;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #303133;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
padding-top: 80rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,14 +18,23 @@
|
|||||||
<view class="student-no">{{ bed.studentNo }}</view>
|
<view class="student-no">{{ bed.studentNo }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="empty-bed">空床位</view>
|
<view v-else class="empty-bed">空床位</view>
|
||||||
<view v-if="bed.isLeaveAuto" class="leave-tag">
|
<view v-if="bed.isLeaveAuto || bed.isHolidayStayAuto" class="leave-tag">
|
||||||
<u-tag text="请假" type="warning" size="mini"></u-tag>
|
<!-- 只展示状态名,不展示请假类型(如「其他假」) -->
|
||||||
|
<u-tag v-if="bed.isLeaveAuto" text="请假" type="warning" size="mini"></u-tag>
|
||||||
|
<u-tag
|
||||||
|
v-if="bed.isHolidayStayAuto"
|
||||||
|
text="假期留宿"
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
:custom-style="{ marginLeft: bed.isLeaveAuto ? '8rpx' : '0' }"
|
||||||
|
></u-tag>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="bed.isHolidayStayAuto" class="leave-tag">
|
<view
|
||||||
<u-tag text="假期留宿" type="primary" size="mini"></u-tag>
|
v-if="!bed.isLeaveAuto && !bed.isHolidayStayAuto"
|
||||||
</view>
|
class="status-label"
|
||||||
<view class="status-label" :class="getStatusColorClass(bed.rollCallStatus)">
|
:class="getStatusColorClass(bed)"
|
||||||
{{ getStatusLabel(bed.rollCallStatus) }}
|
>
|
||||||
|
{{ getStatusLabel(bed) }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -112,11 +121,21 @@ export default {
|
|||||||
let present = 0, leave = 0, holidayStay = 0, absent = 0, empty = 0;
|
let present = 0, leave = 0, holidayStay = 0, absent = 0, empty = 0;
|
||||||
this.bedDetails.forEach((bed) => {
|
this.bedDetails.forEach((bed) => {
|
||||||
const status = bed.rollCallStatus;
|
const status = bed.rollCallStatus;
|
||||||
if (status === '0') present++;
|
if (status === '3') {
|
||||||
else if (status === '1') leave++;
|
empty++;
|
||||||
else if (status === '4') holidayStay++;
|
return;
|
||||||
else if (status === '2') absent++;
|
}
|
||||||
else if (status === '3') empty++;
|
if (status === '2') {
|
||||||
|
absent++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === '0') {
|
||||||
|
present++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 请假/留宿可同时计入(同一人可同时出现在两类统计)
|
||||||
|
if (bed.isLeaveAuto || status === '1') leave++;
|
||||||
|
if (bed.isHolidayStayAuto || status === '4') holidayStay++;
|
||||||
});
|
});
|
||||||
return { present, leave, holidayStay, absent, empty };
|
return { present, leave, holidayStay, absent, empty };
|
||||||
},
|
},
|
||||||
@@ -215,22 +234,28 @@ export default {
|
|||||||
|
|
||||||
if (!bed.studentId) {
|
if (!bed.studentId) {
|
||||||
bed.rollCallStatus = '3';
|
bed.rollCallStatus = '3';
|
||||||
} else if (item.leaveInfo) {
|
} else {
|
||||||
// 请假优先
|
if (item.leaveInfo) {
|
||||||
bed.rollCallStatus = '1';
|
|
||||||
bed.isLeaveAuto = true;
|
bed.isLeaveAuto = true;
|
||||||
bed.leaveId = item.leaveInfo.leaveId;
|
bed.leaveId = item.leaveInfo.leaveId;
|
||||||
bed.leaveTypeName = item.leaveInfo.leaveTypeName;
|
bed.leaveTypeName = item.leaveInfo.leaveTypeName;
|
||||||
bed.leaveStartTime = item.leaveInfo.leaveStartTime;
|
bed.leaveStartTime = item.leaveInfo.leaveStartTime;
|
||||||
bed.leaveEndTime = item.leaveInfo.leaveEndTime;
|
bed.leaveEndTime = item.leaveInfo.leaveEndTime;
|
||||||
} else if (item.holidayStayInfo) {
|
}
|
||||||
bed.rollCallStatus = '4';
|
if (item.holidayStayInfo) {
|
||||||
bed.isHolidayStayAuto = true;
|
bed.isHolidayStayAuto = true;
|
||||||
bed.holidayStayId = item.holidayStayInfo.holidayStayId;
|
bed.holidayStayId = item.holidayStayInfo.holidayStayId;
|
||||||
bed.holidayType = item.holidayStayInfo.holidayType;
|
bed.holidayType = item.holidayStayInfo.holidayType;
|
||||||
bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
|
bed.holidayStartDate = item.holidayStayInfo.applyStartDate;
|
||||||
bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
|
bed.holidayEndDate = item.holidayStayInfo.applyEndDate;
|
||||||
}
|
}
|
||||||
|
// 主状态:请假优先落库;仅留宿时为 4;两者都有时仍为 1,但双标签都展示
|
||||||
|
if (bed.isLeaveAuto) {
|
||||||
|
bed.rollCallStatus = '1';
|
||||||
|
} else if (bed.isHolidayStayAuto) {
|
||||||
|
bed.rollCallStatus = '4';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return bed;
|
return bed;
|
||||||
});
|
});
|
||||||
@@ -250,6 +275,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getBedCardClass(bed) {
|
getBedCardClass(bed) {
|
||||||
|
if (bed.isLeaveAuto && bed.isHolidayStayAuto) return 'bed-leave bed-holiday-stay-both';
|
||||||
if (bed.rollCallStatus === '0') return 'bed-present';
|
if (bed.rollCallStatus === '0') return 'bed-present';
|
||||||
if (bed.rollCallStatus === '1') return 'bed-leave';
|
if (bed.rollCallStatus === '1') return 'bed-leave';
|
||||||
if (bed.rollCallStatus === '4') return 'bed-holiday-stay';
|
if (bed.rollCallStatus === '4') return 'bed-holiday-stay';
|
||||||
@@ -258,7 +284,9 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
getStatusLabel(status) {
|
getStatusLabel(bed) {
|
||||||
|
if (bed && bed.isLeaveAuto && bed.isHolidayStayAuto) return '请假+假期留宿';
|
||||||
|
const status = typeof bed === 'string' ? bed : bed && bed.rollCallStatus;
|
||||||
if (status === '0') return '在寝';
|
if (status === '0') return '在寝';
|
||||||
if (status === '1') return '请假';
|
if (status === '1') return '请假';
|
||||||
if (status === '4') return '假期留宿';
|
if (status === '4') return '假期留宿';
|
||||||
@@ -267,7 +295,9 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
getStatusColorClass(status) {
|
getStatusColorClass(bed) {
|
||||||
|
if (bed && bed.isLeaveAuto && bed.isHolidayStayAuto) return 'color-leave';
|
||||||
|
const status = typeof bed === 'string' ? bed : bed && bed.rollCallStatus;
|
||||||
if (status === '0') return 'color-present';
|
if (status === '0') return 'color-present';
|
||||||
if (status === '1') return 'color-leave';
|
if (status === '1') return 'color-leave';
|
||||||
if (status === '4') return 'color-holiday-stay';
|
if (status === '4') return 'color-holiday-stay';
|
||||||
@@ -277,12 +307,14 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleBedClick(bed) {
|
handleBedClick(bed) {
|
||||||
if (bed.isLeaveAuto) {
|
if (bed.isLeaveAuto || bed.isHolidayStayAuto) {
|
||||||
uni.showToast({ title: '该学生处于请假状态,不可修改', icon: 'none' });
|
const tip =
|
||||||
return;
|
bed.isLeaveAuto && bed.isHolidayStayAuto
|
||||||
}
|
? '该学生同时处于请假与假期留宿,不可修改'
|
||||||
if (bed.isHolidayStayAuto) {
|
: bed.isLeaveAuto
|
||||||
uni.showToast({ title: '该学生处于假期留宿状态,不可修改', icon: 'none' });
|
? '该学生处于请假状态,不可修改'
|
||||||
|
: '该学生处于假期留宿状态,不可修改';
|
||||||
|
uni.showToast({ title: tip, icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!bed.studentId) {
|
if (!bed.studentId) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
const SERVICE_ROUTES = {
|
const SERVICE_ROUTES = {
|
||||||
// 学工管理
|
// 学工管理
|
||||||
'student:info': '/pages/xg/xsjcxx/xsjcxx/index',
|
'student:info': '/pages/xg/xsjcxx/xsjcxx/index',
|
||||||
|
'student:pwdreset': '/pages/xg/xsjcxx/studentPwdReset/index',
|
||||||
'student:dorm': '/pages/student/dorm/index',
|
'student:dorm': '/pages/student/dorm/index',
|
||||||
'student:leave': '/pages/xg/xsjcxx/xsqj/leave/index',
|
'student:leave': '/pages/xg/xsjcxx/xsqj/leave/index',
|
||||||
'student:psychology': '/pages/student/psychology/index',
|
'student:psychology': '/pages/student/psychology/index',
|
||||||
@@ -48,6 +49,7 @@ const SERVICE_ROUTES = {
|
|||||||
// 服务名称映射
|
// 服务名称映射
|
||||||
const SERVICE_NAMES = {
|
const SERVICE_NAMES = {
|
||||||
学生信息: 'student:info',
|
学生信息: 'student:info',
|
||||||
|
重置学生密码: 'student:pwdreset',
|
||||||
宿舍管理: 'student:dorm',
|
宿舍管理: 'student:dorm',
|
||||||
学生请假: 'student:leave',
|
学生请假: 'student:leave',
|
||||||
心理健康: 'student:psychology',
|
心理健康: 'student:psychology',
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite serve --mode development",
|
"dev": "vite serve --mode development",
|
||||||
"build:prod": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode production",
|
"build:prod": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode production",
|
||||||
"build:ci": "CI=true VITE_LOW_MEM=1 VITE_BUILD_COMPRESS= node --max-old-space-size=5120 node_modules/vite/bin/vite.js build --mode production",
|
"build:ci": "CI=true VITE_LOW_MEM=1 VITE_BUILD_COMPRESS= node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode production",
|
||||||
"build:dev": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode development",
|
"build:dev": "node --max-old-space-size=6144 node_modules/vite/bin/vite.js build --mode development",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint:eslint": "eslint",
|
"lint:eslint": "eslint",
|
||||||
|
|||||||
@@ -117,3 +117,31 @@ export const getCollegeLeaders = (studentId: string | number): AxiosPromise<{ da
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface StudentPwdResetVO {
|
||||||
|
id?: string | number;
|
||||||
|
studentNo?: string;
|
||||||
|
name?: string;
|
||||||
|
className?: string;
|
||||||
|
classTeacherName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号查询学生(重置密码页)
|
||||||
|
*/
|
||||||
|
export const queryStudentForPwdReset = (studentNo: string): AxiosPromise<StudentPwdResetVO> => {
|
||||||
|
return request({
|
||||||
|
url: `/studentMsg/studentMsg/queryByStudentNo/${encodeURIComponent(studentNo)}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按学号重置学生密码为随机临时密码,data 为临时密码
|
||||||
|
*/
|
||||||
|
export const resetStudentPassword = (studentNo: string) => {
|
||||||
|
return request({
|
||||||
|
url: `/studentMsg/studentMsg/resetStudentPassword/${encodeURIComponent(studentNo)}`,
|
||||||
|
method: 'put'
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -12,8 +12,6 @@
|
|||||||
@use './sidebar.scss';
|
@use './sidebar.scss';
|
||||||
@use './btn.scss';
|
@use './btn.scss';
|
||||||
@use './ruoyi.scss';
|
@use './ruoyi.scss';
|
||||||
@use 'animate.css';
|
|
||||||
@use 'element-plus/dist/index.css';
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ const init = async () => {
|
|||||||
// 如果域名不相等 则重定向处理
|
// 如果域名不相等 则重定向处理
|
||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
if (domain !== 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);
|
const urlFull = new URL(window.location.href);
|
||||||
urlFull.host = domain;
|
urlFull.host = domain;
|
||||||
window.location.href = urlFull.toString();
|
window.location.href = urlFull.toString();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { translateRouteTitle } from './utils/i18n';
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
// global css
|
// global css
|
||||||
import 'virtual:uno.css';
|
import 'virtual:uno.css';
|
||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
import 'animate.css';
|
||||||
import '@/assets/styles/index.scss';
|
import '@/assets/styles/index.scss';
|
||||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
|
to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
|
||||||
/* has token*/
|
/* has token*/
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
|
// 已有 token 时先进首页;若会话已失效,下面 getInfo 失败会清 token 并回登录页
|
||||||
next({ path: '/' });
|
next({ path: '/' });
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
} else if (isWhiteList(to.path)) {
|
} else if (isWhiteList(to.path)) {
|
||||||
@@ -47,9 +48,9 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
// 判断当前用户是否已拉取完user_info信息
|
// 判断当前用户是否已拉取完user_info信息
|
||||||
const [err] = await tos(useUserStore().getInfo());
|
const [err] = await tos(useUserStore().getInfo());
|
||||||
if (err) {
|
if (err) {
|
||||||
|
isRelogin.show = false;
|
||||||
await useUserStore().logout();
|
await useUserStore().logout();
|
||||||
// ElMessage.error(err.message || '获取用户信息失败');
|
next(`/login?redirect=${encodeURIComponent(to.fullPath || '/')}`);
|
||||||
next({ path: '/' });
|
|
||||||
} else {
|
} else {
|
||||||
isRelogin.show = false;
|
isRelogin.show = false;
|
||||||
const accessRoutes = await usePermissionStore().generateRoutes();
|
const accessRoutes = await usePermissionStore().generateRoutes();
|
||||||
|
|||||||
@@ -65,13 +65,18 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 注销
|
// 注销(后端失败也不能挡住清本地 token,否则坏 JWT 会导致永远进不了登录页)
|
||||||
const logout = async (): Promise<void> => {
|
const logout = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
await logoutApi();
|
await logoutApi();
|
||||||
|
} catch {
|
||||||
|
// ignore:token 已失效/签名无效时服务端注销也会失败
|
||||||
|
} finally {
|
||||||
token.value = '';
|
token.value = '';
|
||||||
roles.value = [];
|
roles.value = [];
|
||||||
permissions.value = [];
|
permissions.value = [];
|
||||||
removeToken();
|
removeToken();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setAvatar = (value: string) => {
|
const setAvatar = (value: string) => {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* HTML 安全处理工具:拼接 innerHTML / v-html 前必须经过转义或清洗,防止存储型 XSS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 转义为纯文本,用于把用户输入拼接进 HTML 字符串 */
|
||||||
|
export function escapeHtml(value: unknown): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALLOWED_TAGS = new Set([
|
||||||
|
'p', 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'span', 'div', 'ul', 'ol', 'li',
|
||||||
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'table', 'thead',
|
||||||
|
'tbody', 'tr', 'th', 'td', 'img', 'a', 'hr', 'sub', 'sup', 'font'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ALLOWED_ATTRS = new Set(['style', 'class', 'href', 'src', 'alt', 'title', 'width', 'height', 'colspan', 'rowspan', 'align', 'color', 'target']);
|
||||||
|
|
||||||
|
const SAFE_URL = /^(https?:|mailto:|tel:|\/|#|data:image\/(png|jpe?g|gif|bmp|webp);)/i;
|
||||||
|
|
||||||
|
/** 清洗富文本:只保留白名单标签与属性,移除脚本、事件属性及 javascript: 链接 */
|
||||||
|
export function sanitizeHtml(html: unknown): string {
|
||||||
|
if (html === null || html === undefined || html === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const doc = new DOMParser().parseFromString(`<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;
|
||||||
|
}
|
||||||
@@ -2179,7 +2179,10 @@ const viewImageGallery = (attachment: any) => {
|
|||||||
// 使用Element Plus的图片预览功能
|
// 使用Element Plus的图片预览功能
|
||||||
const previewList = imageList.map((img) => img.filePath);
|
const previewList = imageList.map((img) => img.filePath);
|
||||||
const imageViewer = document.createElement('div');
|
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);
|
document.body.appendChild(imageViewer);
|
||||||
|
|
||||||
// 触发预览
|
// 触发预览
|
||||||
|
|||||||
@@ -713,6 +713,7 @@ import {
|
|||||||
exportResumePdf
|
exportResumePdf
|
||||||
} from '@/api/rs/job';
|
} from '@/api/rs/job';
|
||||||
import { parseTime } from '@/utils/ruoyi';
|
import { parseTime } from '@/utils/ruoyi';
|
||||||
|
import { escapeHtml } from '@/utils/htmlSafe';
|
||||||
import type { JobVO, JobForm, ApplicationVO } from '@/api/rs/job';
|
import type { JobVO, JobForm, ApplicationVO } from '@/api/rs/job';
|
||||||
import { listDept } from '@/api/system/dept';
|
import { listDept } from '@/api/system/dept';
|
||||||
import type { DeptVO } from '@/api/system/dept/types';
|
import type { DeptVO } from '@/api/system/dept/types';
|
||||||
@@ -1770,25 +1771,25 @@ function handleExportResume() {
|
|||||||
|
|
||||||
<div style="margin-bottom: 30px;">
|
<div style="margin-bottom: 30px;">
|
||||||
<h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">基本信息</h2>
|
<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>${escapeHtml(selectedRecord.applicantName || '未提供')}</p>
|
||||||
<p><strong>应聘职位:</strong>${selectedRecord.jobTitle || '未提供'}</p>
|
<p><strong>应聘职位:</strong>${escapeHtml(selectedRecord.jobTitle || '未提供')}</p>
|
||||||
<p><strong>联系电话:</strong>${selectedRecord.contactPhone || '未提供'}</p>
|
<p><strong>联系电话:</strong>${escapeHtml(selectedRecord.contactPhone || '未提供')}</p>
|
||||||
<p><strong>联系邮箱:</strong>${selectedRecord.contactEmail || '未提供'}</p>
|
<p><strong>联系邮箱:</strong>${escapeHtml(selectedRecord.contactEmail || '未提供')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 30px;">
|
<div style="margin-bottom: 30px;">
|
||||||
<h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">教育背景</h2>
|
<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>
|
||||||
|
|
||||||
<div style="margin-bottom: 30px;">
|
<div style="margin-bottom: 30px;">
|
||||||
<h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">工作经验</h2>
|
<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>
|
||||||
|
|
||||||
<div style="margin-bottom: 30px;">
|
<div style="margin-bottom: 30px;">
|
||||||
<h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">自我介绍</h2>
|
<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>
|
||||||
|
|
||||||
<div style="margin-top: 50px; text-align: right; font-size: 12px; color: #999;">
|
<div style="margin-top: 50px; text-align: right; font-size: 12px; color: #999;">
|
||||||
|
|||||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user