新增 CLAUDE.md 文件以提供项目指导,添加 .claudeignore 文件以排除不必要的文件,更新 pom.xml 版本至 3.9.2,修复多个路径遍历和 SQL 注入漏洞,优化字典翻译切面逻辑,增强文件上传和下载的安全性,新增音频文件类型支持,改进动态数据源的安全校验。

This commit is contained in:
geht
2026-05-18 20:05:03 +08:00
parent 67ca5287e2
commit 140f4a816e
589 changed files with 65043 additions and 4682 deletions

View File

@@ -1,74 +0,0 @@
package org.jeecg.modules.airag;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Description: JavaAIFlow增强节点:生成在线word文档
* @Author: chenrui
* @Date: 2025-08-06 16:39
*/
@Slf4j
@Component("jeecgDemoAiWordGen")
public class TestAiGenWordEnhance implements IAiRagEnhanceJava {
@Autowired
IEoaWordTemplateService eoaWordTemplateService;
@Override
public Map<String, Object> process(Map<String, Object> inputParams) {
Object resp = inputParams.get("resp");
String respStr = String.valueOf(resp);
log.info("AI生成word响应内容:{}", respStr);
if(oConvertUtils.isEmpty(respStr)){
throw new JeecgBootException("AI生成内容失败。请稍后再试或查看后台日志。");
}
String mainStr = null;
Matcher matcher = Pattern.compile("\\[.*]", Pattern.DOTALL).matcher(respStr);
if (matcher.find()) {
mainStr = matcher.group();
// 替换中文双引号为英文双引号
mainStr = mainStr.replaceAll("[“”]", "\"");
// 替换 NBSP 为普通空格
mainStr = mainStr.replaceAll("\\u00A0", " ");
log.info("生成word json:{}", mainStr);
// 校验是否为合法 JSON 字符串
try {
JSON.parse(mainStr);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new JeecgBootException("AI生成的内容不是合法的 JSON 字符串,请稍后再试或优化提示词。");
}
}else{
throw new JeecgBootException("AI生成的内容不是合法的 JSON 字符串,请稍后再试或优化提示词。");
}
EoaWordTemplate template = new EoaWordTemplate();
String dateFormat = DateUtils.formatDate();
template.setName("AI生成的简历_"+dateFormat);
template.setCode("AI_GEN_"+System.currentTimeMillis());
template.setHeader("[]");
template.setFooter("[]");
template.setMain(mainStr);
template.setWidth(794);
template.setHeight(1123);
template.setMargins("[100,120,100,120]");
template.setPaperDirection("vertical");
eoaWordTemplateService.save(template);
return Collections.singletonMap("result","success");
}
}

View File

@@ -18,6 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -196,6 +197,17 @@ public class SystemApiController {
return sysBaseApi.getDepartParentIdsByDepIds(depIds);
}
/**
* 通过 userIds 查询部门ID列表
*
* @param userIds
* @return key = userId; value = 用户拥有的部门ID列表
*/
@GetMapping("/getDepartIdsByUserIds")
Map<String, List<String>> getDepartIdsByUserIds(@RequestParam("userIds") Collection<String> userIds) {
return sysBaseApi.getDepartIdsByUserIds(userIds);
}
/**
* 通过用户账号查询部门 name
* @param username
@@ -1124,4 +1136,20 @@ public class SystemApiController {
public void uniPushMsgToUser(@RequestBody PushMessageDTO pushMessageDTO){
sysBaseApi.uniPushMsgToUser(pushMessageDTO);
}
/**
* 根据用户名查询用户主部门信息。
* <p>
* 逻辑取用户的主岗位mainDepPostId再查询该岗位节点在 sys_depart 中的父节点,
* 父节点即为用户的主部门,返回其信息。
* <p>
*
* @param username 用户账号
* @return 主部门信息,若用户未配置主岗位则返回 {@code null}
*/
@GetMapping("/queryMainDepartByUsername")
SysDepartModel queryMainDepartByUsername(@RequestParam("username") String username) {
return sysBaseApi.queryMainDepartByUsername(username);
}
}

View File

@@ -70,10 +70,16 @@ public final class XmlUtils {
*/
public static XMLReader getXmlReader() {
try {
final XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
//update-begin---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞---
final SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
final XMLReader reader = spf.newSAXParser().getXMLReader();
//update-end---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞---
reader.setFeature("http://xml.org/sax/features/namespaces", true);
reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return reader;
} catch (final Exception e) {
throw new RuntimeException("Unable to create XMLReader", e);
@@ -196,6 +202,12 @@ public final class XmlUtils {
spf.setNamespaceAware(true);
spf.setValidating(false);
try {
//update-begin---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞---
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
//update-end---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞---
final SAXParser saxParser = spf.newSAXParser();
final XMLReader xmlReader = saxParser.getXMLReader();
final CustomAttributeHandler handler = new CustomAttributeHandler();

View File

@@ -9,11 +9,13 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiAuth;
import org.jeecg.modules.openapi.entity.OpenApiHeader;
@@ -24,6 +26,7 @@ import org.jeecg.modules.openapi.service.OpenApiService;
import org.jeecg.modules.openapi.swagger.*;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.ISysUserService;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@@ -78,8 +81,13 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
* @param openApi
* @return
*/
@RequiresRoles({"admin"})
@PostMapping(value = "/add")
public Result<?> add(@RequestBody OpenApi openApi) {
if (openApi == null) {
return Result.error("请求参数不能为空");
}
validOriginUrl(openApi.getOriginUrl());
service.save(openApi);
return Result.ok("添加成功!");
}
@@ -90,8 +98,13 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
* @param openApi
* @return
*/
@RequiresRoles({"admin"})
@PutMapping(value = "/edit")
public Result<?> edit(@RequestBody OpenApi openApi) {
if (openApi == null) {
return Result.error("请求参数不能为空");
}
validOriginUrl(openApi.getOriginUrl());
service.updateById(openApi);
return Result.ok("修改成功!");
@@ -103,6 +116,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
* @param id
* @return
*/
@RequiresRoles({"admin"})
@DeleteMapping(value = "/delete")
public Result<?> delete(@RequestParam(name = "id", required = true) String id) {
service.removeById(id);
@@ -115,6 +129,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
* @param ids
* @return
*/
@RequiresRoles({"admin"})
@DeleteMapping(value = "/deleteBatch")
public Result<?> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
@@ -159,6 +174,8 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
}
String url = openApi.getOriginUrl();
// 校验原始接口路径是否合法
validOriginUrl(url);
String method = openApi.getRequestMethod();
String appkey = request.getHeader("appkey");
OpenApiAuth openApiAuth = openApiAuthService.getByAppkey(appkey);
@@ -167,7 +184,17 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
httpHeaders.put("X-Access-Token", Lists.newArrayList(token));
httpHeaders.put("Content-Type",Lists.newArrayList("application/json"));
HttpEntity<String> httpEntity = new HttpEntity<>(json, httpHeaders);
url = RestUtil.getBaseUrl() + url;
//update-begin---author:scott ---date:20260429 for【issues/9590】微服务nginx部署openApi接口访问不到-----------
// originUrl 支持两种形式:
// 1) 相对路径(如 /house/houseTest/list拼接当前请求的 baseUrl
// 使用 CommonUtils.getBaseUrl(request)(而非 RestUtil.getBaseUrl()
// 可读取 X-Gateway-Base-Path 请求头,兼容微服务网关下的真实 base path
// 2) 完整URLhttp(s)://host:port/path直接使用适用于微服务模式下接口部署在其他微服务模块如 erp 7003的场景
String lowerUrl = url.toLowerCase();
if (!lowerUrl.startsWith("http://") && !lowerUrl.startsWith("https://")) {
url = CommonUtils.getBaseUrl(request) + url;
}
//update-end---author:scott ---date:20260429 for【issues/9590】微服务nginx部署openApi接口访问不到-----------
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (HttpMethod.GET.matches(method)
|| HttpMethod.DELETE.matches(method)
@@ -213,6 +240,51 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
return token;
}
/**
* 校验原始接口路径是否合法:
* - 相对路径:必须以 / 开头,不允许 // 和 .. 防止路径穿越
* - 完整URL仅允许 http/https 协议,禁止 file/ftp/gopher/jar/netdoc 等其它协议(用于微服务模式跨模块调用)
*/
private void validOriginUrl(String originUrl) {
if (oConvertUtils.isEmpty(originUrl)) {
throw new JeecgBootBizTipException("原始接口路径不能为空");
}
String decoded;
try {
decoded = java.net.URLDecoder.decode(originUrl, "UTF-8");
// 二次解码,防止 %252f 这类双重编码绕过
decoded = java.net.URLDecoder.decode(decoded, "UTF-8");
} catch (Exception e) {
throw new JeecgBootBizTipException("原始接口路径包含非法字符");
}
//update-begin---author:scott ---date:20260429 for【issues/9590】微服务nginx部署openApi接口访问不到-----------
// 微服务部署时OpenAPI 配置的接口可能位于其他微服务模块(如 erp 7003允许 originUrl 直接配置完整 http(s) URL
String lower = decoded.toLowerCase();
boolean isFullHttpUrl = lower.startsWith("http://") || lower.startsWith("https://");
if (!isFullHttpUrl) {
if (!decoded.startsWith("/")) {
throw new JeecgBootBizTipException("原始接口路径必须以 / 开头,或填写完整的 http(s) URL");
}
if (decoded.startsWith("//") || decoded.startsWith("/\\")) {
throw new JeecgBootBizTipException("原始接口路径不能以 // 或 /\\ 开头");
}
if (lower.contains("://") || lower.startsWith("file:") || lower.startsWith("ftp:") || lower.startsWith("gopher:")
|| lower.startsWith("jar:") || lower.startsWith("netdoc:")) {
throw new JeecgBootBizTipException("原始接口路径仅支持相对路径或 http(s) 完整URL");
}
} else {
// 即便是完整URL也禁止其它危险协议防止 http://x@file:/... 之类的绕过场景)
String afterScheme = lower.substring(lower.indexOf("://") + 3);
if (afterScheme.contains("file:") || afterScheme.contains("ftp:") || afterScheme.contains("gopher:")
|| afterScheme.contains("jar:") || afterScheme.contains("netdoc:")) {
throw new JeecgBootBizTipException("原始接口路径不允许嵌套 file/ftp/gopher/jar/netdoc 等协议");
}
}
if (decoded.contains("..")) {
throw new JeecgBootBizTipException("原始接口路径不能包含 ..");
}
//update-end---author:scott ---date:20260429 for【issues/9590】微服务nginx部署openApi接口访问不到-----------
}
@GetMapping("/json")
public SwaggerModel swaggerModel() {
@@ -382,7 +454,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
SwaggerInfo info = new SwaggerInfo();
info.setDescription("OpenAPI 接口列表");
info.setVersion("3.9.1");
info.setVersion("3.9.2");
info.setTitle("OpenAPI 接口列表");
info.setTermsOfService("https://jeecg.com");

View File

@@ -43,9 +43,16 @@ public class OpenApi implements Serializable {
private String requestUrl;
/**
* IP 名单
* IP 名单
*/
private String blackList;
private String whiteList;
//update-begin---author:scott ---date:20260417 for【PR/9083】OpenAPI新增白名单备注字段-----------
/**
* 白名单备注说明
*/
private String comment;
//update-end---author:scott ---date:20260417 for【PR/9083】OpenAPI新增白名单备注字段-----------
/**
* 请求头json
*/

View File

@@ -4,6 +4,7 @@ import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.IpUtils;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiAuth;
import org.jeecg.modules.openapi.entity.OpenApiLog;
@@ -20,6 +21,7 @@ import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* @date 2024/12/19 16:55
@@ -38,7 +40,7 @@ public class ApiAuthFilter implements Filter {
Date callTime = new Date();
HttpServletRequest request = (HttpServletRequest)servletRequest;
String ip = request.getRemoteAddr();
String ip = IpUtils.getIpAddr(request);
String appkey = request.getHeader("appkey");
String signature = request.getHeader("signature");
@@ -46,8 +48,8 @@ public class ApiAuthFilter implements Filter {
OpenApi openApi = findOpenApi(request);
// IP 名单核验
checkBlackList(openApi, ip);
// IP 名单核验
checkWhiteList(openApi, ip);
// 签名核验
checkSignValid(appkey, signature, timestamp);
@@ -80,22 +82,108 @@ public class ApiAuthFilter implements Filter {
this.openApiPermissionService = applicationContext.getBean(OpenApiPermissionService.class);
}
//update-begin---author:scott ---date:20260416 for【PR/9083】OpenAPI白名单增强支持CIDR网段和通配符匹配-----------
/**
* IP 名单核验
* IP 名单核验支持精确IP、CIDR网段如192.168.1.0/24、通配符如10.2.3.*
* @param openApi
* @param ip
*/
protected void checkBlackList(OpenApi openApi, String ip) {
if (!StringUtils.hasText(openApi.getBlackList())) {
protected void checkWhiteList(OpenApi openApi, String ip) {
if (!StringUtils.hasText(openApi.getWhiteList())) {
return;
}
List<String> blackList = Arrays.asList(openApi.getBlackList().split(","));
if (blackList.contains(ip)) {
throw new JeecgBootException("目标接口限制IP[" + ip + "]进行访问IP已记录请停止访问");
List<String> whiteList = Arrays.stream(openApi.getWhiteList().split("[,\\n]"))
.map(String::trim)
.filter(StringUtils::hasText)
.collect(Collectors.toList());
for (String item : whiteList) {
if (isIpMatch(ip, item)) {
return;
}
}
throw new JeecgBootException("IP[" + ip + "]不在白名单中,禁止访问");
}
/**
* IP匹配支持精确匹配、CIDR网段匹配、通配符匹配
* @param ip 客户端IP
* @param pattern 白名单条目IP/CIDR/通配符)
* @return 是否匹配
*/
private boolean isIpMatch(String ip, String pattern) {
if (!ip.contains(".") || !pattern.contains(".")) {
return ip.equals(pattern);
}
if (pattern.contains("/")) {
return isCidrMatch(ip, pattern);
}
if (pattern.contains("*")) {
return isWildcardMatch(ip, pattern);
}
return ip.equals(pattern);
}
/**
* CIDR网段匹配仅IPv4如 192.168.1.0/24
*/
private boolean isCidrMatch(String ip, String cidr) {
String[] parts = cidr.split("/");
if (parts.length != 2) {
return false;
}
try {
long ipLong = ipToLong(ip);
long cidrLong = ipToLong(parts[0]);
int prefixLength = Integer.parseInt(parts[1]);
if (prefixLength < 0 || prefixLength > 32) {
return false;
}
long mask = prefixLength == 0 ? 0 : (-1L << (32 - prefixLength));
return (ipLong & mask) == (cidrLong & mask);
} catch (Exception e) {
log.warn("CIDR匹配解析失败: cidr={}, ip={}", cidr, ip);
return false;
}
}
/**
* 通配符匹配,如 10.2.3.*
*/
private boolean isWildcardMatch(String ip, String pattern) {
String[] ipParts = ip.split("\\.");
String[] patternParts = pattern.split("\\.");
if (ipParts.length != 4 || patternParts.length != 4) {
return false;
}
for (int i = 0; i < 4; i++) {
if ("*".equals(patternParts[i])) {
continue;
}
if (!ipParts[i].equals(patternParts[i])) {
return false;
}
}
return true;
}
/**
* IPv4地址转long
*/
private long ipToLong(String ip) {
String[] parts = ip.split("\\.");
if (parts.length != 4) {
throw new IllegalArgumentException("非法IPv4地址: " + ip);
}
long result = 0;
for (int i = 0; i < 4; i++) {
result = (result << 8) | (Integer.parseInt(parts[i]) & 0xFF);
}
return result;
}
//update-end---author:scott ---date:20260416 for【PR/9083】OpenAPI白名单增强支持CIDR网段和通配符匹配-----------
/**
* 签名验证
* @param appkey

View File

@@ -32,6 +32,7 @@ public class OssFileController {
private IOssFileService ossFileService;
@ResponseBody
@RequiresPermissions("system:ossFile:list")
@GetMapping("/list")
public Result<IPage<OssFile>> queryPageList(OssFile file,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@@ -63,6 +64,7 @@ public class OssFileController {
}
@ResponseBody
@RequiresPermissions("system:ossFile:delete")
@DeleteMapping("/delete")
public Result delete(@RequestParam(name = "id") String id) {
Result result = new Result();

View File

@@ -1,344 +0,0 @@
package org.jeecg.modules.print.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.print.PrintService;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.print.entity.PrintTemplate;
import org.jeecg.modules.print.service.IPrintTemplateService;
import org.jeecg.modules.print.support.PrintServerEnvironmentService;
import org.jeecg.modules.print.support.PrintServerPdfJobService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.jeecg.modules.print.ai.INativePrintTemplateImageAnalyzeService;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import com.alibaba.fastjson.JSON;
/**
* 打印模板维护Hiprint
*/
@Slf4j
@Tag(name = "打印模板")
@RestController
@RequestMapping("/print/template")
public class PrintTemplateController extends JeecgController<PrintTemplate, IPrintTemplateService> {
@Autowired private PrintServerEnvironmentService printServerEnvironmentService;
@Autowired private PrintServerPdfJobService printServerPdfJobService;
@Autowired
private INativePrintTemplateImageAnalyzeService nativePrintTemplateImageAnalyzeService;
/**
* STOMP 实时通知:广播打印模板变更到 /topic/sync/print-templates。
* 直接用 SimpMessagingTemplate 内联推送,避免 jeecg-system-biz核心模块
* 反向依赖 jeecg-module-xslmes业务模块造成的循环依赖。
* 消息体格式与 MesXslStompNotifyService.publishPrintTemplateChanged 完全一致,
* 桌面端订阅方无需任何改动。
*/
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Operation(summary = "打印模板-分页列表")
@GetMapping(value = "/list")
@RequiresPermissions("print:template:list")
public Result<IPage<PrintTemplate>> list(
PrintTemplate query,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<PrintTemplate> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
qw.orderByDesc("create_time");
Page<PrintTemplate> page = new Page<>(pageNo, pageSize);
return Result.OK(service.page(page, qw));
}
@AutoLog(value = "打印模板-添加")
@Operation(summary = "打印模板-添加")
@PostMapping(value = "/add")
@RequiresPermissions("print:template:add")
public Result<String> add(@RequestBody PrintTemplate entity) {
if (StringUtils.isBlank(entity.getTemplateCode())) {
return Result.error("模板编码不能为空");
}
if (service.getByCode(entity.getTemplateCode()) != null) {
return Result.error("模板编码已存在");
}
if (StringUtils.isBlank(entity.getTemplateJson())) {
entity.setTemplateJson("{}");
}
service.save(entity);
publishPrintTemplateChanged("add", entity.getId());
return Result.OK("添加成功");
}
@AutoLog(value = "打印模板-编辑")
@Operation(summary = "打印模板-编辑")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("print:template:edit")
public Result<String> edit(@RequestBody PrintTemplate entity) {
PrintTemplate db = service.getById(entity.getId());
if (db == null) {
return Result.error("记录不存在");
}
if (StringUtils.isNotBlank(entity.getTemplateCode())
&& !entity.getTemplateCode().equals(db.getTemplateCode())) {
if (service.getByCode(entity.getTemplateCode()) != null) {
return Result.error("模板编码已存在");
}
}
service.updateById(entity);
publishPrintTemplateChanged("edit", entity.getId());
return Result.OK("修改成功");
}
@AutoLog(value = "打印模板-保存JSON")
@Operation(summary = "打印模板-保存模板JSON")
@PostMapping(value = "/saveJson")
@RequiresPermissions("print:template:edit")
public Result<String> saveJson(@RequestBody Map<String, String> body) {
String id = body.get("id");
String templateJson = body.get("templateJson");
if (StringUtils.isBlank(id)) {
return Result.error("id 不能为空");
}
if (templateJson == null) {
return Result.error("templateJson 不能为空");
}
PrintTemplate db = service.getById(id);
if (db == null) {
return Result.error("记录不存在");
}
db.setTemplateJson(templateJson);
service.updateById(db);
return Result.OK("保存成功");
}
@AutoLog(value = "打印模板-删除")
@Operation(summary = "打印模板-删除")
@DeleteMapping(value = "/delete")
@RequiresPermissions("print:template:delete")
public Result<String> delete(@RequestParam(name = "id") String id) {
service.removeById(id);
publishPrintTemplateChanged("delete", id);
return Result.OK("删除成功");
}
@AutoLog(value = "打印模板-批量删除")
@Operation(summary = "打印模板-批量删除")
@DeleteMapping(value = "/deleteBatch")
@RequiresPermissions("print:template:delete")
public Result<String> deleteBatch(@RequestParam(name = "ids") String ids) {
if (StringUtils.isBlank(ids)) {
return Result.error("参数 ids 不能为空");
}
List<String> idList = java.util.Arrays.asList(ids.split(","));
service.removeByIds(idList);
idList.forEach(id -> publishPrintTemplateChanged("delete", id.trim()));
return Result.OK("批量删除成功");
}
@Operation(summary = "打印模板-通过id查询")
@GetMapping(value = "/queryById")
@RequiresPermissions("print:template:list")
public Result<PrintTemplate> queryById(@RequestParam(name = "id") String id) {
return Result.OK(service.getById(id));
}
@AutoLog(value = "打印模板-图片分析生成原生JSON")
@Operation(summary = "打印模板-上传图片分析为原生模板JSON前端传 imageBase64可接 OpenAI 兼容视觉模型)")
@PostMapping(value = "/analyzeImageForNative")
@RequiresPermissions("print:template:edit")
public Result<Map<String, Object>> analyzeImageForNative(@RequestBody Map<String, String> body) {
try {
String imageBase64 = body == null ? null : body.get("imageBase64");
if (StringUtils.isBlank(imageBase64)) {
return Result.error("imageBase64 不能为空");
}
String filename = body.get("filename");
String mime = body.get("mime");
byte[] bytes = decodeImageBase64(imageBase64);
return Result.OK(nativePrintTemplateImageAnalyzeService.analyzeBytes(bytes, mime, filename));
} catch (Exception e) {
log.error("图片分析失败", e);
return Result.error("图片分析失败:" + e.getMessage());
}
}
private static byte[] decodeImageBase64(String imageBase64) {
String s = StringUtils.trimToEmpty(imageBase64);
int comma = s.indexOf(',');
if (s.startsWith("data:") && comma > 0) {
s = s.substring(comma + 1);
}
return Base64.getDecoder().decode(s.replaceAll("\\s", ""));
}
@Operation(summary = "打印模板-通过编码查询")
@GetMapping(value = "/queryByCode")
@RequiresPermissions("print:template:list")
public Result<PrintTemplate> queryByCode(@RequestParam(name = "code") String code) {
PrintTemplate t = service.getByCode(code);
if (t == null) {
return Result.error("未找到模板: " + code);
}
return Result.OK(t);
}
@Operation(summary = "打印模板-查询可用打印机")
@GetMapping(value = "/queryPrinters")
@RequiresPermissions("print:template:list")
public Result<Map<String, Object>> queryPrinters() {
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
}
@AutoLog(value = "打印模板-服务端直打")
@Operation(summary = "打印模板-服务端直打")
@PostMapping(value = "/directPrint")
@RequiresPermissions("print:template:list")
public Result<String> directPrint(@RequestBody Map<String, Object> body) {
String templateCode = String.valueOf(body.getOrDefault("templateCode", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
Object dataJsonObj = body.get("dataJson");
String dataJsonText = dataJsonObj == null ? "" : String.valueOf(dataJsonObj);
if (StringUtils.isBlank(templateCode)) {
return Result.error("templateCode 不能为空");
}
if (StringUtils.isBlank(dataJsonText)) {
return Result.error("dataJson 不能为空");
}
PrintTemplate tpl = service.getByCode(templateCode);
if (tpl == null) {
return Result.error("模板不存在: " + templateCode);
}
try {
PrintService target = printServerPdfJobService.resolvePrintService(printerName);
if (StringUtils.isNotBlank(printerName)
&& !"__system_default__".equals(printerName)
&& target != null
&& !printerName.equalsIgnoreCase(String.valueOf(target.getName()).trim())) {
return Result.error("未找到指定打印机: " + printerName);
}
if (target == null) {
return Result.error("未找到可用打印机,请检查服务器打印机配置");
}
// 说明:当前接口实现的是服务端直打(纯文本)。若需按 hiprint 模板渲染版式,建议接入独立渲染服务。
String content =
"QH-MES 快速打印\n模板编号: "
+ templateCode
+ "\n模板名称: "
+ String.valueOf(tpl.getTemplateName())
+ "\n\n数据JSON:\n"
+ dataJsonText
+ "\n";
final String[] lines = content.replace("\r\n", "\n").split("\n", -1);
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(target);
job.setJobName("QH-MES-" + templateCode);
job.setPrintable(
new Printable() {
@Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
Graphics2D g2 = (Graphics2D) graphics;
g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
g2.setFont(new Font("Microsoft YaHei", Font.PLAIN, 10));
int lineHeight = g2.getFontMetrics().getHeight() + 2;
int maxLinesPerPage = Math.max(1, (int) (pageFormat.getImageableHeight() / lineHeight));
int start = pageIndex * maxLinesPerPage;
if (start >= lines.length) {
return Printable.NO_SUCH_PAGE;
}
int end = Math.min(lines.length, start + maxLinesPerPage);
int y = g2.getFontMetrics().getAscent();
for (int i = start; i < end; i += 1) {
g2.drawString(lines[i], 0, y);
y += lineHeight;
}
return Printable.PAGE_EXISTS;
}
});
job.print();
return Result.OK("已提交到服务器打印机: " + target.getName());
} catch (Exception e) {
log.error("服务端直打失败", e);
return Result.error("服务端直打失败: " + e.getMessage());
}
}
@AutoLog(value = "打印模板-PDF后端打印")
@Operation(summary = "打印模板-PDF后端打印")
@PostMapping(value = "/directPrintPdf")
@RequiresPermissions("print:template:list")
public Result<String> directPrintPdf(@RequestBody Map<String, Object> body) {
String templateCode = String.valueOf(body.getOrDefault("templateCode", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
if (StringUtils.isBlank(templateCode)) {
return Result.error("templateCode 不能为空");
}
return printServerPdfJobService.submitPdfBase64(printerName, pdfBase64, fileName, templateCode);
}
// ═══════════════════════════ 桌面端免密接口 ═══════════════════════════
@Operation(summary = "打印模板-免密通过编码查询(桌面端)")
@GetMapping(value = "/anon/queryByCode")
public Result<PrintTemplate> anonQueryByCode(@RequestParam(name = "code") String code) {
PrintTemplate t = service.getByCode(code);
if (t == null) {
return Result.error("未找到模板: " + code);
}
return Result.OK(t);
}
@Operation(summary = "打印模板-免密分页列表(桌面端)")
@GetMapping(value = "/anon/list")
public Result<IPage<PrintTemplate>> anonList(PrintTemplate query,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<PrintTemplate> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
qw.orderByAsc("template_code");
Page<PrintTemplate> page = new Page<>(pageNo, pageSize);
return Result.OK(service.page(page, qw));
}
/**
* 广播打印模板变更事件到 /topic/sync/print-templates桌面端订阅同步刷新本地缓存。
* 消息体格式 = MesXslStompNotifyService.publishPrintTemplateChanged 的输出,
* 内联实现避免反向依赖业务模块。
*/
private void publishPrintTemplateChanged(String action, String templateId) {
try {
Map<String, Object> event = new HashMap<>();
event.put("cmd", "PRINT_TEMPLATE_CHANGED");
event.put("action", action);
event.put("templateId", templateId);
event.put("timestamp", System.currentTimeMillis());
messagingTemplate.convertAndSend("/topic/sync/print-templates", JSON.toJSONString(event));
} catch (Exception e) {
log.debug("广播 STOMP 事件失败 [PRINT_TEMPLATE_CHANGED]: {}", e.getMessage());
}
}
}

View File

@@ -174,9 +174,22 @@ public class QuartzJobServiceImpl extends ServiceImpl<QuartzJobMapper, QuartzJob
}
}
/**
* 安全加载Job类仅允许 org.jeecg. 包下的类,且必须实现 org.quartz.Job 接口
*/
private static Job getClass(String classname) throws Exception {
Class<?> class1 = Class.forName(classname);
return (Job) class1.newInstance();
// 包名白名单校验防止任意类实例化导致RCE
if (classname == null || !classname.startsWith("org.jeecg.")) {
throw new IllegalArgumentException("非法的任务类名:" + classname + ",仅允许 org.jeecg 包下的Job类");
}
//update-begin---author:scott ---date:20260416 for【PR#9538】Class.forName使用上下文类加载器增强部署兼容性-----------
Class<?> clazz = Class.forName(classname, true, Thread.currentThread().getContextClassLoader());
//update-end---author:scott ---date:20260416 for【PR#9538】Class.forName使用上下文类加载器增强部署兼容性-----------
// 校验是否实现了 org.quartz.Job 接口
if (!Job.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("非法的任务类:" + classname + ",必须实现 org.quartz.Job 接口");
}
return (Job) clazz.getDeclaredConstructor().newInstance();
}
}

View File

@@ -0,0 +1,13 @@
{
"id": "E0CC280",
"appTitle": null,
"appLogo": null,
"carouselImgJson": null,
"routeImgJson": null,
"appVersion": "1.0.0",
"versionNum": 100,
"downloadUrl": "https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/app/JeecgUniapp3_0617.apk",
"wgtUrl": "",
"webDownloadUrl": "https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/app/jeecgboot-setup-3.8.3.exe",
"updateNote": "1. 优化用户体验\n2. 修复已知bug\n"
}

View File

@@ -9,6 +9,7 @@ import com.jeecg.dingtalk.api.core.response.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.dto.PushMessageDTO;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.config.TenantContext;
@@ -110,6 +111,7 @@ public class SysAnnouncementController {
* @param req
* @return
*/
@RequiresPermissions("system:sysAnnouncement:list")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Result<IPage<SysAnnouncement>> queryPageList(SysAnnouncement sysAnnouncement,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@@ -136,6 +138,7 @@ public class SysAnnouncementController {
* @param sysAnnouncement
* @return
*/
@RequiresPermissions("system:sysAnnouncement:add")
@RequestMapping(value = "/add", method = RequestMethod.POST)
public Result<SysAnnouncement> add(@RequestBody SysAnnouncement sysAnnouncement) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -143,6 +146,10 @@ public class SysAnnouncementController {
// 代码逻辑说明: 标题处理xss攻击的问题
String title = XssUtils.scriptXss(sysAnnouncement.getTitile());
sysAnnouncement.setTitile(title);
//update-begin---author:liusq ---date:2025-04-13 for【issues/9521】富文本msgContent字段未做XSS过滤存在存储型XSS漏洞-----------
String msgContent = XssUtils.richTextXss(sysAnnouncement.getMsgContent());
sysAnnouncement.setMsgContent(msgContent);
//update-end---author:liusq ---date:2025-04-13 for【issues/9521】富文本msgContent字段未做XSS过滤存在存储型XSS漏洞-----------
// 【安全校验】校验附件文件名,防止路径遍历攻击
SsrfFileTypeFilter.checkPathTraversalBatch(sysAnnouncement.getFiles());
sysAnnouncement.setDelFlag(CommonConstant.DEL_FLAG_0.toString());
@@ -165,6 +172,7 @@ public class SysAnnouncementController {
* @param sysAnnouncement
* @return
*/
@RequiresPermissions("system:sysAnnouncement:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<SysAnnouncement> eidt(@RequestBody SysAnnouncement sysAnnouncement) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -176,6 +184,10 @@ public class SysAnnouncementController {
// 代码逻辑说明: 标题处理xss攻击的问题
String title = XssUtils.scriptXss(sysAnnouncement.getTitile());
sysAnnouncement.setTitile(title);
//update-begin---author:liusq ---date:2025-04-13 for【issues/9521】富文本msgContent字段未做XSS过滤存在存储型XSS漏洞-----------
String msgContent = XssUtils.richTextXss(sysAnnouncement.getMsgContent());
sysAnnouncement.setMsgContent(msgContent);
//update-end---author:liusq ---date:2025-04-13 for【issues/9521】富文本msgContent字段未做XSS过滤存在存储型XSS漏洞-----------
// 【安全校验】校验附件文件名,防止路径遍历攻击
SsrfFileTypeFilter.checkPathTraversalBatch(sysAnnouncement.getFiles());
sysAnnouncement.setNoticeType(NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue());
@@ -196,6 +208,7 @@ public class SysAnnouncementController {
* @param sysAnnouncement
* @return
*/
//@RequiresPermissions("system:sysAnnouncement:editIzTop")
@RequestMapping(value = "/editIzTop", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<SysAnnouncement> editIzTop(@RequestBody SysAnnouncement sysAnnouncement) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -216,6 +229,7 @@ public class SysAnnouncementController {
* @param id
* @return
*/
@RequiresPermissions("system:sysAnnouncement:delete")
@RequestMapping(value = "/delete", method = RequestMethod.DELETE)
public Result<SysAnnouncement> delete(@RequestParam(name="id",required=true) String id) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -238,6 +252,7 @@ public class SysAnnouncementController {
* @param ids
* @return
*/
@RequiresPermissions("system:sysAnnouncement:deleteBatch")
@RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE)
public Result<SysAnnouncement> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -278,6 +293,7 @@ public class SysAnnouncementController {
* @param id
* @return
*/
@RequiresPermissions("system:sysAnnouncement:doReleaseData")
@RequestMapping(value = "/doReleaseData", method = RequestMethod.GET)
public Result<SysAnnouncement> doReleaseData(@RequestParam(name="id",required=true) String id, HttpServletRequest request) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -358,6 +374,7 @@ public class SysAnnouncementController {
* @param id
* @return
*/
@RequiresPermissions("system:sysAnnouncement:doReovkeData")
@RequestMapping(value = "/doReovkeData", method = RequestMethod.GET)
public Result<SysAnnouncement> doReovkeData(@RequestParam(name="id",required=true) String id, HttpServletRequest request) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -467,6 +484,7 @@ public class SysAnnouncementController {
*
* @param request
*/
@RequiresPermissions("system:sysAnnouncement:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(SysAnnouncement sysAnnouncement,HttpServletRequest request) {
// Step.1 组装查询条件
@@ -491,6 +509,7 @@ public class SysAnnouncementController {
* @param response
* @return
*/
@RequiresPermissions("system:sysAnnouncement:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
@@ -532,6 +551,7 @@ public class SysAnnouncementController {
* @param anntId
* @return
*/
//@RequiresPermissions("system:sysAnnouncement:syncNotic")
@RequestMapping(value = "/syncNotic", method = RequestMethod.GET)
public Result<SysAnnouncement> syncNotic(@RequestParam(name="anntId",required=false) String anntId, HttpServletRequest request) {
Result<SysAnnouncement> result = new Result<SysAnnouncement>();
@@ -681,7 +701,7 @@ public class SysAnnouncementController {
Result<Page<SysAnnouncementSend>> result = new Result<>();
//----------------------------------------------------------------------------------------
// step.1 此接口过慢,可以采用缓存一小时方案
String keyString = String.format(CommonConstant.CACHE_KEY_USER_LAST_ANNOUNT_TIME_1HOUR + "_" + noticeType, userId);
String keyString = String.format(CommonConstant.CACHE_KEY_USER_LAST_ANNOUNT_TIME_1HOUR, userId) + "_" + noticeType;
if (redisTemplate.hasKey(keyString)) {
log.debug("[SysAnnouncementSend Redis] 通过Redis缓存查询用户最后一次收到系统通知时间userId={}", userId);
Page<SysAnnouncementSend> pageList = (Page<SysAnnouncementSend>) redisTemplate.opsForValue().get(keyString);

View File

@@ -285,14 +285,14 @@ public class SysAnnouncementSendController {
@RequestParam(name="busId",required=true) String busId,
@RequestParam(name="busType",required=false) String busType) {
//更新阅读状态
sysAnnouncementSendService.updateReadFlagByBusId(busId,busType);
//刷新未读数量
JSONObject obj = new JSONObject();
obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER);
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
webSocket.sendMessage(sysUser.getId(), obj.toJSONString());
boolean updateFlag = sysAnnouncementSendService.updateReadFlagByBusId(busId,busType);
if(updateFlag){
//刷新未读数量
JSONObject obj = new JSONObject();
obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER);
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
webSocket.sendMessage(sysUser.getId(), obj.toJSONString());
}
return Result.ok();
}
}

View File

@@ -3,6 +3,7 @@ package org.jeecg.modules.system.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.RedisUtil;
@@ -10,6 +11,9 @@ import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.system.entity.SysAppVersion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
import java.io.InputStream;
/**
* @Description: app系统配置
@@ -25,6 +29,10 @@ public class SysAppVersionController{
@Autowired
private RedisUtil redisUtil;
/**
* app3版本json文件路径
*/
private final String JSON_PATH = "classpath:org/jeecg/modules/system/config/json/app3-version.json";
/**
* APP缓存前缀
@@ -41,16 +49,29 @@ public class SysAppVersionController{
if (oConvertUtils.isNotEmpty(appConfig)) {
try {
SysAppVersion sysAppVersion = (SysAppVersion)appConfig;
if(oConvertUtils.isEmpty(sysAppVersion.getDownloadUrl())){
String jsonContent = readJson(JSON_PATH);
sysAppVersion = JSONObject.parseObject(jsonContent, SysAppVersion.class);
return Result.OK(sysAppVersion);
}
return Result.OK(sysAppVersion);
} catch (Exception e) {
log.error(e.toString(),e);
return Result.error("app版本信息获取失败" + e.getMessage());
}
}else{
// 缓存中没有从配置的json文件中获取
try {
String jsonContent = readJson(JSON_PATH);
SysAppVersion sysAppVersion = JSONObject.parseObject(jsonContent, SysAppVersion.class);
return Result.OK(sysAppVersion);
} catch (Exception e) {
log.error("从JSON文件读取app版本信息失败{}", e);
}
}
return Result.OK();
}
/**
* 保存APP3
*
@@ -65,4 +86,21 @@ public class SysAppVersionController{
redisUtil.set(APP3_VERSION + id,sysAppVersion);
return Result.OK();
}
/**
* 读取json格式文件
* @param jsonSrc
* @return
*/
private String readJson(String jsonSrc) {
String json = "";
try {
//换个写法解决springboot读取jar包中文件的问题
InputStream stream = getClass().getClassLoader().getResourceAsStream(jsonSrc.replace("classpath:", ""));
json = IOUtils.toString(stream,"UTF-8");
} catch (IOException e) {
log.error(e.getMessage(),e);
}
return json;
}
}

View File

@@ -121,11 +121,13 @@ public class SysDataSourceController extends JeecgController<SysDataSource, ISys
*/
@AutoLog(value = "多数据源管理-添加")
@Operation(summary = "多数据源管理-添加")
@RequiresPermissions("system:datasource:add")
@PostMapping(value = "/add")
public Result<?> add(@RequestBody SysDataSource sysDataSource) {
// 代码逻辑说明: jdbc连接地址漏洞问题
try {
JdbcSecurityUtil.validate(sysDataSource.getDbUrl());
JdbcSecurityUtil.validateDriver(sysDataSource.getDbDriver());
}catch (JeecgBootException e){
log.error(e.toString());
return Result.error("操作失败:" + e.getMessage());
@@ -141,11 +143,13 @@ public class SysDataSourceController extends JeecgController<SysDataSource, ISys
*/
@AutoLog(value = "多数据源管理-编辑")
@Operation(summary = "多数据源管理-编辑")
@RequiresPermissions("system:datasource:edit")
@RequestMapping(value = "/edit", method ={RequestMethod.PUT, RequestMethod.POST})
public Result<?> edit(@RequestBody SysDataSource sysDataSource) {
// 代码逻辑说明: jdbc连接地址漏洞问题
try {
JdbcSecurityUtil.validate(sysDataSource.getDbUrl());
JdbcSecurityUtil.validateDriver(sysDataSource.getDbDriver());
} catch (JeecgBootException e) {
log.error(e.toString());
return Result.error("操作失败:" + e.getMessage());
@@ -161,6 +165,7 @@ public class SysDataSourceController extends JeecgController<SysDataSource, ISys
*/
@AutoLog(value = "多数据源管理-通过id删除")
@Operation(summary = "多数据源管理-通过id删除")
@RequiresPermissions("system:datasource:delete")
@DeleteMapping(value = "/delete")
public Result<?> delete(@RequestParam(name = "id") String id) {
return sysDataSourceService.deleteDataSource(id);
@@ -174,6 +179,7 @@ public class SysDataSourceController extends JeecgController<SysDataSource, ISys
*/
@AutoLog(value = "多数据源管理-批量删除")
@Operation(summary = "多数据源管理-批量删除")
@RequiresPermissions("system:datasource:delete")
@DeleteMapping(value = "/deleteBatch")
public Result<?> deleteBatch(@RequestParam(name = "ids") String ids) {
List<String> idList = Arrays.asList(ids.split(","));
@@ -193,6 +199,7 @@ public class SysDataSourceController extends JeecgController<SysDataSource, ISys
*/
@AutoLog(value = "多数据源管理-通过id查询")
@Operation(summary = "多数据源管理-通过id查询")
@RequiresPermissions("system:datasource:list")
@GetMapping(value = "/queryById")
public Result<?> queryById(@RequestParam(name = "id") String id) throws InterruptedException {
SysDataSource sysDataSource = sysDataSourceService.getById(id);
@@ -211,6 +218,7 @@ public class SysDataSourceController extends JeecgController<SysDataSource, ISys
* @param request
* @param sysDataSource
*/
@RequiresPermissions("system:datasource:export")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, SysDataSource sysDataSource) {
//------------------------------------------------------------------------------------------------
@@ -229,6 +237,7 @@ public class SysDataSourceController extends JeecgController<SysDataSource, ISys
* @param response
* @return
*/
@RequiresPermissions("system:datasource:import")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, SysDataSource.class);

View File

@@ -137,10 +137,10 @@ public class SysDepartController {
* @return
*/
@RequestMapping(value = "/queryDepartTreeSync", method = RequestMethod.GET)
public Result<List<SysDepartTreeModel>> queryDepartTreeSync(@RequestParam(name = "pid", required = false) String parentId,@RequestParam(name = "ids", required = false) String ids, @RequestParam(name = "primaryKey", required = false) String primaryKey) {
public Result<List<SysDepartTreeModel>> queryDepartTreeSync(@RequestParam(name = "pid", required = false) String parentId,@RequestParam(name = "ids", required = false) String ids, @RequestParam(name = "primaryKey", required = false) String primaryKey, @RequestParam(name = "orgCategory", required = false) String orgCategory) {
Result<List<SysDepartTreeModel>> result = new Result<>();
try {
List<SysDepartTreeModel> list = sysDepartService.queryTreeListByPid(parentId,ids, primaryKey);
List<SysDepartTreeModel> list = sysDepartService.queryTreeListByPid(parentId,ids, primaryKey, orgCategory);
result.setResult(list);
result.setSuccess(true);
} catch (Exception e) {
@@ -737,6 +737,16 @@ public class SysDepartController {
List<SysPositionSelectTreeVo> list = sysDepartService.getRankRelation(departId);
return Result.ok(list);
}
/**
* 获取ALL职级关系
* @param departId
* @return
*/
@GetMapping("/getALLRankRelation")
public Result<List<SysPositionSelectTreeVo>> getALLRankRelation(@RequestParam(name = "departId",required = false) String departId){
List<SysPositionSelectTreeVo> list = sysDepartService.getALLRankRelation(departId);
return Result.ok(list);
}
/**
* 根据部门code获取当前和上级部门名称

View File

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
@@ -65,6 +66,7 @@ public class SysFillRuleController extends JeecgController<SysFillRule, ISysFill
* @param ruleCode
* @return
*/
@RequiresRoles({"admin"})
@GetMapping(value = "/testFillRule")
public Result testFillRule(@RequestParam("ruleCode") String ruleCode) {
Object result = FillRuleUtil.executeRule(ruleCode, new JSONObject());
@@ -79,6 +81,7 @@ public class SysFillRuleController extends JeecgController<SysFillRule, ISysFill
*/
@AutoLog(value = "填值规则-添加")
@Operation(summary = "填值规则-添加")
@RequiresRoles({"admin"})
@PostMapping(value = "/add")
public Result<?> add(@RequestBody SysFillRule sysFillRule) {
sysFillRuleService.save(sysFillRule);
@@ -93,6 +96,7 @@ public class SysFillRuleController extends JeecgController<SysFillRule, ISysFill
*/
@AutoLog(value = "填值规则-编辑")
@Operation(summary = "填值规则-编辑")
@RequiresRoles({"admin"})
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<?> edit(@RequestBody SysFillRule sysFillRule) {
sysFillRuleService.updateById(sysFillRule);
@@ -107,6 +111,7 @@ public class SysFillRuleController extends JeecgController<SysFillRule, ISysFill
*/
@AutoLog(value = "填值规则-通过id删除")
@Operation(summary = "填值规则-通过id删除")
@RequiresRoles({"admin"})
@DeleteMapping(value = "/delete")
public Result<?> delete(@RequestParam(name = "id", required = true) String id) {
sysFillRuleService.removeById(id);
@@ -121,6 +126,7 @@ public class SysFillRuleController extends JeecgController<SysFillRule, ISysFill
*/
@AutoLog(value = "填值规则-批量删除")
@Operation(summary = "填值规则-批量删除")
@RequiresRoles({"admin"})
@DeleteMapping(value = "/deleteBatch")
public Result<?> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.sysFillRuleService.removeByIds(Arrays.asList(ids.split(",")));

View File

@@ -34,6 +34,7 @@ public class SysGatewayRouteController extends JeecgController<SysGatewayRoute,
@Autowired
private ISysGatewayRouteService sysGatewayRouteService;
@RequiresPermissions("system:gateway:updateAll")
@PostMapping(value = "/updateAll")
public Result<?> updateAll(@RequestBody JSONObject json) {
sysGatewayRouteService.updateAll(json);

View File

@@ -91,9 +91,13 @@ public class SysPermissionController {
query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0);
query.orderByAsc(SysPermission::getSortNo);
//支持通过菜单名字,模糊查询
//支持通过菜单名字或url,模糊查询
if(oConvertUtils.isNotEmpty(sysPermission.getName())){
query.like(SysPermission::getName, sysPermission.getName());
query.and(wrapper -> wrapper
.like(SysPermission::getName, sysPermission.getName())
.or()
.like(SysPermission::getUrl, sysPermission.getName())
);
}
List<SysPermission> list = sysPermissionService.list(query);
List<SysPermissionTree> treeList = new ArrayList<>();

View File

@@ -0,0 +1,173 @@
package org.jeecg.modules.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.system.entity.SysUgroup;
import org.jeecg.modules.system.service.ISysUgroupService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
import java.util.Date;
/**
* @Description: 用户组表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
@Tag(name="用户组表")
@RestController
@RequestMapping("/sys/ugroup")
@Slf4j
public class SysUgroupController extends JeecgController<SysUgroup, ISysUgroupService> {
@Autowired
private ISysUgroupService sysUgroupService;
/**
* 分页列表查询
*
* @param sysUgroup
* @param pageNo
* @param pageSize
* @param req
* @return
*/
//@AutoLog(value = "用户组表-分页列表查询")
@Operation(summary="用户组表-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<SysUgroup>> queryPageList(SysUgroup sysUgroup,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<SysUgroup> queryWrapper = QueryGenerator.initQueryWrapper(sysUgroup, req.getParameterMap());
Page<SysUgroup> page = new Page<SysUgroup>(pageNo, pageSize);
IPage<SysUgroup> pageList = sysUgroupService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param sysUgroup
* @return
*/
@AutoLog(value = "用户组表-添加")
@Operation(summary="用户组表-添加")
@RequiresPermissions("system:sys_ugroup:add")
@PostMapping(value = "/add")
public Result<SysUgroup> add(@RequestBody SysUgroup sysUgroup) {
Result<SysUgroup> result = new Result<SysUgroup>();
try {
sysUgroup.setCreateTime(new Date());
sysUgroupService.save(sysUgroup);
result.success("添加成功!");
} catch (Exception e) {
log.error(e.getMessage(), e);
result.error500("操作失败");
}
return result;
}
/**
* 编辑
*
* @param sysUgroup
* @return
*/
@AutoLog(value = "用户组表-编辑")
@Operation(summary="用户组表-编辑")
@RequiresPermissions("system:sys_ugroup:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody SysUgroup sysUgroup) {
sysUgroupService.updateById(sysUgroup);
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "用户组表-通过id删除")
@Operation(summary="用户组表-通过id删除")
@RequiresPermissions("system:sys_ugroup:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
sysUgroupService.deleteById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "用户组表-批量删除")
@Operation(summary="用户组表-批量删除")
@RequiresPermissions("system:sys_ugroup:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.sysUgroupService.deleteByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "用户组表-通过id查询")
@Operation(summary="用户组表-通过id查询")
@GetMapping(value = "/queryById")
public Result<SysUgroup> queryById(@RequestParam(name="id",required=true) String id) {
SysUgroup sysUgroup = sysUgroupService.getById(id);
if(sysUgroup==null) {
return Result.error("未找到对应数据");
}
return Result.OK(sysUgroup);
}
/**
* 导出excel
*
* @param request
* @param sysUgroup
*/
@RequiresPermissions("system:sys_ugroup:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, SysUgroup sysUgroup) {
return super.exportXls(request, sysUgroup, SysUgroup.class, "用户组表");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequiresPermissions("system:sys_ugroup:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, SysUgroup.class);
}
}

View File

@@ -0,0 +1,164 @@
package org.jeecg.modules.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.system.entity.SysUgroupUser;
import org.jeecg.modules.system.service.ISysUgroupUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
/**
* @Description: 用户组关系表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
@Tag(name="用户组关系表")
@RestController
@RequestMapping("/system/sysUgroupUser")
@Slf4j
public class SysUgroupUserController extends JeecgController<SysUgroupUser, ISysUgroupUserService> {
@Autowired
private ISysUgroupUserService sysUgroupUserService;
/**
* 分页列表查询
*
* @param sysUgroupUser
* @param pageNo
* @param pageSize
* @param req
* @return
*/
//@AutoLog(value = "用户组关系表-分页列表查询")
@Operation(summary="用户组关系表-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<SysUgroupUser>> queryPageList(SysUgroupUser sysUgroupUser,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<SysUgroupUser> queryWrapper = QueryGenerator.initQueryWrapper(sysUgroupUser, req.getParameterMap());
Page<SysUgroupUser> page = new Page<SysUgroupUser>(pageNo, pageSize);
IPage<SysUgroupUser> pageList = sysUgroupUserService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param sysUgroupUser
* @return
*/
@AutoLog(value = "用户组关系表-添加")
@Operation(summary="用户组关系表-添加")
@RequiresPermissions("system:sys_ugroup_user:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody SysUgroupUser sysUgroupUser) {
sysUgroupUserService.save(sysUgroupUser);
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param sysUgroupUser
* @return
*/
@AutoLog(value = "用户组关系表-编辑")
@Operation(summary="用户组关系表-编辑")
@RequiresPermissions("system:sys_ugroup_user:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody SysUgroupUser sysUgroupUser) {
sysUgroupUserService.updateById(sysUgroupUser);
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "用户组关系表-通过id删除")
@Operation(summary="用户组关系表-通过id删除")
@RequiresPermissions("system:sys_ugroup_user:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
sysUgroupUserService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "用户组关系表-批量删除")
@Operation(summary="用户组关系表-批量删除")
@RequiresPermissions("system:sys_ugroup_user:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.sysUgroupUserService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "用户组关系表-通过id查询")
@Operation(summary="用户组关系表-通过id查询")
@GetMapping(value = "/queryById")
public Result<SysUgroupUser> queryById(@RequestParam(name="id",required=true) String id) {
SysUgroupUser sysUgroupUser = sysUgroupUserService.getById(id);
if(sysUgroupUser==null) {
return Result.error("未找到对应数据");
}
return Result.OK(sysUgroupUser);
}
/**
* 导出excel
*
* @param request
* @param sysUgroupUser
*/
@RequiresPermissions("system:sys_ugroup_user:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, SysUgroupUser sysUgroupUser) {
return super.exportXls(request, sysUgroupUser, SysUgroupUser.class, "用户组关系表");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequiresPermissions("system:sys_ugroup_user:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, SysUgroupUser.class);
}
}

View File

@@ -0,0 +1,62 @@
package org.jeecg.modules.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
/**
* @Description: 用户组表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
@Data
@TableName("sys_ugroup")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="用户组表")
public class SysUgroup implements Serializable {
private static final long serialVersionUID = 1L;
/**主键id*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键id")
private java.lang.String id;
/**角色名称*/
@Excel(name = "用户组名称", width = 15)
@Schema(description = "用户组名称")
private java.lang.String groupName;
/**描述*/
@Excel(name = "描述", width = 15)
@Schema(description = "描述")
private java.lang.String description;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private java.util.Date createTime;
/**更新人*/
@Schema(description = "更新人")
private java.lang.String updateBy;
/**更新时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新时间")
private java.util.Date updateTime;
/**租户ID*/
@Excel(name = "租户ID", width = 15)
@Schema(description = "租户ID")
private java.lang.Integer tenantId;
}

View File

@@ -0,0 +1,52 @@
package org.jeecg.modules.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecgframework.poi.excel.annotation.Excel;
import java.io.Serializable;
/**
* @Description: 用户组关系表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
@Data
@TableName("sys_ugroup_user")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="用户组关系表")
public class SysUgroupUser implements Serializable {
private static final long serialVersionUID = 1L;
/**主键id*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键id")
private java.lang.String id;
/**用户id*/
@Excel(name = "用户id", width = 15)
@Schema(description = "用户id")
private java.lang.String userId;
/**用户组id*/
@Excel(name = "用户组id", width = 15)
@Schema(description = "用户组id")
private java.lang.String groupId;
/**租户ID*/
@Excel(name = "租户ID", width = 15)
@Schema(description = "租户ID")
private java.lang.Integer tenantId;
public SysUgroupUser() {
}
public SysUgroupUser(String userId, String groupId) {
this.userId = userId;
this.groupId = groupId;
}
}

View File

@@ -62,4 +62,13 @@ public interface SysAnnouncementSendMapper extends BaseMapper<SysAnnouncementSen
* @return
*/
List<String> getReadAnnSendByUserId(@Param("ids") List<String> ids, @Param("userId") String userId);
/**
* 根据业务id、业务类型和用户id获取未读消息
* @param busId
* @param busType
* @param userId
* @return
*/
List<String> getUnReadAnnByBusAndUserId(@Param("busId")String busId, @Param("busType")String busType, @Param("userId")String userId);
}

View File

@@ -12,9 +12,11 @@ import org.jeecg.modules.system.vo.SysDepartExportVo;
import org.jeecg.modules.system.vo.SysDepartPositionVo;
import org.jeecg.modules.system.vo.SysUserDepVo;
import org.jeecg.modules.system.vo.lowapp.ExportDepartVo;
import org.springframework.data.repository.query.Param;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* <p>
@@ -42,13 +44,21 @@ public interface SysDepartMapper extends BaseMapper<SysDepart> {
public List<SysDepart> queryDepartsByUsername(@Param("username") String username);
/**
* 根据用户名查询部门
* 根据 userId 查询部门
*
* @param userId
* @return
*/
public List<String> queryDepartsByUserId(@Param("userId") String userId);
/**
* 根据 userIds 查询部门ID
*
* @param userIds 用户ID列表
* @return
*/
List<Map<String, String>> queryDepartIdsByUserIds(@Param("userIds") Collection<String> userIds);
/**
* 通过部门编码获取部门id
* @param orgCode 部门编码
@@ -311,4 +321,11 @@ public interface SysDepartMapper extends BaseMapper<SysDepart> {
* @return
*/
List<SysUser> getDepartmentHead(@Param("page") Page<SysUser> page, @Param("departId") String departId);
/**
*获取所有部门
* @param departId
* @return
*/
List<SysDepartPositionVo> getAllDepartPost(@Param("departId")String departId);
}

View File

@@ -33,7 +33,17 @@ public interface SysPermissionMapper extends BaseMapper<SysPermission> {
* @return List<SysPermission>
*/
public List<SysPermission> queryByUser(@Param("userId") String userId);
//update-begin---author:scott ---date:2026-04-16 for【pull/9445】开启多租户模式时获取用户权限时加入tenant_id判断-----------
/**
* 根据用户id和租户id查询用户权限
* @param userId 用户ID
* @param tenantId 租户ID
* @return List<SysPermission>
*/
public List<SysPermission> queryByUserWithTenantId(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
//update-end---author:scott ---date:2026-04-16 for【pull/9445】开启多租户模式时获取用户权限时加入tenant_id判断-----------
/**
* 修改菜单状态字段: 是否子节点
* @param id 菜单id

View File

@@ -4,7 +4,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Select;
import org.jeecg.modules.system.entity.SysPosition;
import org.springframework.data.repository.query.Param;
import org.jeecg.modules.system.vo.SysPositionVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -37,4 +38,13 @@ public interface SysPositionMapper extends BaseMapper<SysPosition> {
*/
@Select("SELECT id FROM sys_position WHERE name = #{name} AND tenant_id = #{tenantId} ORDER BY create_time DESC")
List<String> getPositionIdByName(@Param("name") String name, @Param("tenantId") Integer tenantId, @Param("page") Page<SysPosition> page);
/**
* 批量通过用户id列表查询职位含userId字段用于批量同步场景
*
* @param userIds 用户id列表
* @return 职位VO列表每条记录含userId字段供调用方分组
*/
List<SysPositionVO> getPositionListByUserIds(@Param("userIds") List<String> userIds);
}

View File

@@ -2,7 +2,7 @@ package org.jeecg.modules.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.system.entity.SysThirdAppConfig;
import org.springframework.data.repository.query.Param;
import org.apache.ibatis.annotations.Param;
import java.util.List;

View File

@@ -0,0 +1,14 @@
package org.jeecg.modules.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.system.entity.SysUgroup;
/**
* @Description: 用户组表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
public interface SysUgroupMapper extends BaseMapper<SysUgroup> {
}

View File

@@ -0,0 +1,14 @@
package org.jeecg.modules.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.system.entity.SysUgroupUser;
/**
* @Description: 用户组关系表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
public interface SysUgroupUserMapper extends BaseMapper<SysUgroupUser> {
}

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.modules.system.entity.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.system.model.SysUserSysDepPostModel;
@@ -271,4 +272,23 @@ public interface SysUserMapper extends BaseMapper<SysUser> {
* @return
*/
List<SysUserSysDepPostModel> queryDepartUserByOrgCode(@Param("page") IPage page, @Param("orgCode") String orgCode, @Param("userParams") SysUser userParams);
/**
* 根据用户名查询用户的主部门信息
*
* @param username
* @return
*/
SysDepart getMainDepartByUsername(@Param("username") String username);
/**
* 根据用户组id获取用户分页列表
* @param page
* @param groupId
* @param username
* @param realname
* @return
*/
IPage<SysUser> getUserByUgroupId(Page page, @Param("groupId") String groupId, @Param("username") String username, @Param("realname") String realname);
}

View File

@@ -134,4 +134,25 @@
#{id}
</foreach>
</select>
<!-- 根据用户id、业务id和业务类型获取未读消息 -->
<select id="getUnReadAnnByBusAndUserId" resultType="java.lang.String">
SELECT
sas.annt_id
FROM
sys_announcement_send sas
LEFT JOIN sys_announcement sa ON sas.annt_id = sa.id
WHERE sa.send_status = '1'
AND sa.del_flag = '0'
AND sas.read_flag = 0
<if test="userId!=null and userId != ''">
AND sas.user_id = #{userId}
</if>
<if test="busId !=null and busId != ''">
AND sa.bus_id = #{busId}
</if>
<if test="busType !=null and busType != ''">
AND sa.bus_type = #{busType}
</if>
</select>
</mapper>

View File

@@ -20,8 +20,8 @@
)
)
</select>
<!-- 根据username查询所拥有的部门 -->
<!-- 根据 userId 查询所拥有的部门 -->
<select id="queryDepartsByUserId" parameterType="String" resultType="java.lang.String">
SELECT id
FROM sys_depart
@@ -32,6 +32,18 @@
)
</select>
<!-- 根据 userIds 查询所拥有的部门 -->
<select id="queryDepartIdsByUserIds" parameterType="String" resultType="java.util.Map">
SELECT sd.id depart_id,
sud.user_id user_id
FROM sys_depart sd
LEFT JOIN sys_user_depart sud ON sd.id = sud.dep_id
WHERE
<foreach item="idItem" index="index" collection="userIds" open=" sud.user_id IN (" separator="," close=")">
#{idItem}
</foreach>
</select>
<!-- 根据username和分类查询所拥有的部门/岗位/公司 -->
<select id="queryDeptByUserAndCategory" parameterType="String" resultType="org.jeecg.modules.system.entity.SysDepart">
SELECT *
@@ -310,8 +322,14 @@
<select id="getDepartmentHead" resultType="org.jeecg.modules.system.entity.SysUser">
select id, realname, avatar, sex, telephone, phone, main_dep_post_id, iz_hide_contact, sort, create_time from sys_user
where status = 1 and del_flag = 0
<bind name="bindDepartId" value="departId+'%'"/>
<bind name="bindDepartId" value="'%'+departId+'%'"/>
and depart_ids like #{bindDepartId}
order by sort,create_time desc
</select>
<!--获取所有顶级公司部门信息-->
<select id="getAllDepartPost" resultType="org.jeecg.modules.system.vo.SysDepartPositionVo">
select depart_name as positionName,id,iz_leaf,parent_id,org_category,org_code, dep_post_parent_id from sys_depart
where (parent_id IS NULL OR parent_id = '')
</select>
</mapper>

View File

@@ -224,4 +224,108 @@
and p.del_flag = 0
</select>
<!-- update-begin author:scott date:2026-04-16 for【pull/9445】开启多租户模式时获取用户权限时加入tenant_id判断 -->
<!-- 获取登录用户拥有的权限多租户模式加入tenant_id过滤 -->
<select id="queryByUserWithTenantId" parameterType="Object" resultMap="SysPermission">
SELECT * FROM (
SELECT p.id,
p.parent_id,
p.name,
p.url,
p.component,
p.is_route,
p.component_name,
p.redirect,
p.menu_type,
p.perms,
p.perms_type,
p.sort_no,
p.always_show,
p.icon,
p.is_leaf,
p.keep_alive,
p.hidden,
p.hide_tab,
p.rule_flag,
p.status,
p.internal_or_external
FROM sys_permission p
WHERE p.del_flag = 0
AND ( p.id in (
SELECT DISTINCT a.permission_id
FROM sys_role_permission a
JOIN sys_role b ON a.role_id = b.id
JOIN sys_user_role c ON c.role_id = b.id AND c.user_id = #{userId,jdbcType=VARCHAR}
)
or (p.url like '%:code' and p.url like '/online%' and p.hidden = 1)
or (p.url like '%:id' and p.url like '/online%' and p.hidden = 1)
or p.url = '/online'
)
<!--加入部门权限-->
UNION
SELECT p.id,
p.parent_id,
p.name,
p.url,
p.component,
p.is_route,
p.component_name,
p.redirect,
p.menu_type,
p.perms,
p.perms_type,
p.sort_no,
p.always_show,
p.icon,
p.is_leaf,
p.keep_alive,
p.hidden,
p.hide_tab,
p.rule_flag,
p.status,
p.internal_or_external
FROM sys_permission p
WHERE p.id in(
SELECT DISTINCT a.permission_id
FROM sys_depart_role_permission a
INNER JOIN sys_depart_role b ON a.role_id = b.id
INNER JOIN sys_depart_role_user c ON c.drole_id = b.id AND c.user_id = #{userId,jdbcType=VARCHAR}
)
and p.del_flag = 0
<!-- 租户套餐权限加入tenant_id过滤 -->
UNION
SELECT p.id,
p.parent_id,
p.name,
p.url,
p.component,
p.is_route,
p.component_name,
p.redirect,
p.menu_type,
p.perms,
p.perms_type,
p.sort_no,
p.always_show,
p.icon,
p.is_leaf,
p.keep_alive,
p.hidden,
p.hide_tab,
p.rule_flag,
p.status,
p.internal_or_external
FROM sys_permission p
WHERE p.id in (
SELECT distinct a.permission_id
FROM sys_tenant_pack_perms a
INNER JOIN sys_tenant_pack b ON a.pack_id = b.id AND b.STATUS = '1'
INNER JOIN sys_tenant st ON st.id = b.tenant_id and st.del_flag = 0 and st.id = #{tenantId,jdbcType=BIGINT}
INNER JOIN sys_tenant_pack_user c ON c.pack_id = b.id AND c.STATUS = '1' AND c.user_id = #{userId,jdbcType=VARCHAR}
)
and p.del_flag = 0
) h order by h.sort_no ASC
</select>
<!-- update-end author:scott date:2026-04-16 for【pull/9445】开启多租户模式时获取用户权限时加入tenant_id判断 -->
</mapper>

View File

@@ -19,4 +19,15 @@
#{positionId}
</foreach>
</select>
<!--批量通过用户id列表查询职位结果含userId字段供批量同步场景分组使用-->
<select id="getPositionListByUserIds" resultType="org.jeecg.modules.system.vo.SysPositionVO">
SELECT sp.name, sp.id, sup.user_id AS userId
FROM sys_position sp
INNER JOIN sys_user_position sup ON sp.id = sup.position_id
WHERE sup.user_id IN
<foreach collection="userIds" item="uid" open="(" separator="," close=")">
#{uid}
</foreach>
</select>
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.system.mapper.SysUgroupMapper">
</mapper>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.system.mapper.SysUgroupUserMapper">
</mapper>

View File

@@ -19,6 +19,8 @@
<bind name="bindRealname" value="'%'+realname+'%'"/>
and a.realname like #{bindRealname}
</if>
ORDER BY
a.sort,a.create_time desc
</select>
<!-- 根据部门查询部门用户 分页 -->

View File

@@ -14,7 +14,7 @@
<!-- 根据用户ids批量查询用户账号 -->
<select id="getUsernameByIds" resultType="String">
select username from sys_user where del_flag = 0 and a.status = 1 and id in
select username from sys_user where del_flag = 0 and status = 1 and id in
<foreach collection="userIds" index="index" item="id" open="(" separator="," close=")">
#{id}
</foreach>
@@ -69,12 +69,23 @@
and realname LIKE concat(concat('%',#{realname}),'%')
</if>
</select>
<!-- 根据用户组Id查询 -->
<select id="getUserByUgroupId" resultType="org.jeecg.modules.system.entity.SysUser">
select * from sys_user where del_flag = 0 and id in (select user_id from sys_ugroup_user where group_id=#{groupId})
<if test="username!=null and username!=''">
and username LIKE concat(concat('%',#{username}),'%')
</if>
<if test="realname!=null and realname!=''">
and realname LIKE concat(concat('%',#{realname}),'%')
</if>
</select>
<!-- 修改用户部门code -->
<update id="updateUserDepart">
UPDATE sys_user SET
<if test="orgCode!=null and loginTenantId!=null">
org_code = #{orgCode, jdbcType=VARCHAR}
org_code = #{orgCode, jdbcType=VARCHAR}
,login_tenant_id = #{loginTenantId, jdbcType=VARCHAR}
</if>
<if test="orgCode==null and loginTenantId!=null">
@@ -403,11 +414,11 @@
<if test="userParams != null">
<if test="userParams.realname != null and userParams.realname != ''">
<bind name="bindRealname" value="'%'+ userParams.realname +'%'"/>
AND su.realname LIKE bindRealname
AND su.realname LIKE #{bindRealname}
</if>
<if test="userParams.workNo != null and userParams.workNo != ''">
<bind name="bindWorkNo" value="'%'+ userParams.workNo +'%'"/>
AND su.work_no LIKE bindWorkNo
AND su.work_no LIKE #{bindWorkNo}
</if>
</if>
</sql>
@@ -462,4 +473,16 @@
</if>
order by su.sort,su.create_time desc
</select>
<!-- 根据用户名查询用户的主部门信息 -->
<select id="getMainDepartByUsername" resultType="org.jeecg.modules.system.entity.SysDepart" parameterType="java.lang.String">
SELECT sdp.* FROM sys_depart sdp
WHERE EXISTS (
SELECT 1 FROM sys_depart sd
INNER JOIN sys_user su ON su.main_dep_post_id = sd.id
WHERE su.username = #{username}
AND sd.parent_id = sdp.id
);
</select>
</mapper>

View File

@@ -51,5 +51,5 @@ public interface ISysAnnouncementSendService extends IService<SysAnnouncementSen
* @param busId
* @param busType
*/
void updateReadFlagByBusId(String busId, String busType);
boolean updateReadFlagByBusId(String busId, String busType);
}

View File

@@ -13,7 +13,9 @@ import org.jeecg.modules.system.vo.SysDepartExportVo;
import org.jeecg.modules.system.vo.SysPositionSelectTreeVo;
import org.jeecg.modules.system.vo.lowapp.ExportDepartVo;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* <p>
@@ -116,6 +118,14 @@ public interface ISysDepartService extends IService<SysDepart>{
*/
List<String> queryDepartsByUserId(String userId);
/**
* 根据 用户ID 查询部门ID列表
*
* @param userIds
* @return key = 用户ID, value = 部门ID列表
*/
Map<String, List<String>> queryDepartIdsByUserIds(Collection<String> userIds);
/**
* 根据部门id批量删除并删除其可能存在的子级部门
* @param ids 多个部门id
@@ -147,9 +157,10 @@ public interface ISysDepartService extends IService<SysDepart>{
* @param parentId 父id
* @param ids 多个部门id
* @param primaryKey 主键字段id或者orgCode
* @param orgCategory 逗号分隔的 orgCategory 值,如 "1,2";为空时退化为默认行为(排除岗位)
* @return
*/
List<SysDepartTreeModel> queryTreeListByPid(String parentId,String ids, String primaryKey);
List<SysDepartTreeModel> queryTreeListByPid(String parentId,String ids, String primaryKey, String orgCategory);
/**
* 获取某个部门的所有父级部门的ID
@@ -305,4 +316,11 @@ public interface ISysDepartService extends IService<SysDepart>{
* @return
*/
IPage<SysUser> getDepartmentHead(String departId, Page<SysUser> page);
/**
* 获取所有职级关系
* @param departId
* @return
*/
List<SysPositionSelectTreeVo> getALLRankRelation(String departId);
}

View File

@@ -2,6 +2,7 @@ package org.jeecg.modules.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.system.entity.SysPosition;
import org.jeecg.modules.system.vo.SysPositionVO;
import java.util.List;
@@ -33,4 +34,13 @@ public interface ISysPositionService extends IService<SysPosition> {
* @return
*/
String getPositionName(List<String> postList);
/**
* 批量通过用户id列表查询职位VO含userId字段用于批量同步场景消除N+1查询
*
* @param userIds 用户id列表
* @return 职位VO列表每条记录含userId字段供调用方分组
*/
List<SysPositionVO> getPositionListByUserIds(List<String> userIds);
}

View File

@@ -79,4 +79,14 @@ public interface ISysThirdAccountService extends IService<SysThirdAccount> {
* @return
*/
SysThirdAccount getOneByUuidAndThirdType(String unionid, String thirdType,Integer tenantId,String thirdUserId);
/**
* 批量通过本地用户id列表查询第三方账号用于全量同步批量预加载
*
* @param sysUserIds 本地用户id列表
* @param thirdType 第三方类型
* @return 第三方账号列表
*/
List<SysThirdAccount> listBySysUserIds(List<String> sysUserIds, String thirdType);
}

View File

@@ -0,0 +1,19 @@
package org.jeecg.modules.system.service;
import org.jeecg.modules.system.entity.SysUgroup;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* @Description: 用户组表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
public interface ISysUgroupService extends IService<SysUgroup> {
void deleteById(String id);
void deleteByIds(List<String> list);
}

View File

@@ -0,0 +1,14 @@
package org.jeecg.modules.system.service;
import org.jeecg.modules.system.entity.SysUgroupUser;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @Description: 用户组关系表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
public interface ISysUgroupUserService extends IService<SysUgroupUser> {
}

View File

@@ -538,4 +538,14 @@ public interface ISysUserService extends IService<SysUser> {
* @param userId
*/
void updateClientId(String clientId,String userId);
/**
* 根据用户组查询用户列表
* @param page
* @param groupId
* @param username
* @param realname
* @return
*/
IPage<SysUser> getUserByUgroupId(Page<SysUser> page, String groupId, String username, String realname);
}

View File

@@ -86,18 +86,15 @@ public class SysAnnouncementSendServiceImpl extends ServiceImpl<SysAnnouncementS
* @param busType
*/
@Override
public void updateReadFlagByBusId(String busId, String busType) {
SysAnnouncement announcement = sysAnnouncementMapper.selectOne(new QueryWrapper<SysAnnouncement>().eq("bus_type",busType).eq("bus_id",busId));
if(oConvertUtils.isNotEmpty(announcement)){
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
String userId = sysUser.getId();
LambdaUpdateWrapper<SysAnnouncementSend> updateWrapper = new UpdateWrapper().lambda();
updateWrapper.set(SysAnnouncementSend::getReadFlag, CommonConstant.HAS_READ_FLAG);
updateWrapper.set(SysAnnouncementSend::getReadTime, new Date());
updateWrapper.eq(SysAnnouncementSend::getAnntId,announcement.getId());
updateWrapper.eq(SysAnnouncementSend::getUserId,userId);
SysAnnouncementSend announcementSend = new SysAnnouncementSend();
sysAnnouncementSendMapper.update(announcementSend, updateWrapper);
public boolean updateReadFlagByBusId(String busId, String busType) {
boolean updateFlag = false;
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
String userId = sysUser.getId();
List<String> unReadAnnouncementsIds = sysAnnouncementSendMapper.getUnReadAnnByBusAndUserId(busId,busType,userId);
if(CollectionUtil.isNotEmpty(unReadAnnouncementsIds)){
sysAnnouncementSendMapper.updateReaded(userId, unReadAnnouncementsIds);
updateFlag = true;
}
return updateFlag;
}
}

View File

@@ -370,6 +370,11 @@ public class SysBaseApiImpl implements ISysBaseAPI {
return sysDepartService.queryDepartsByUserId(userId);
}
@Override
public Map<String, List<String>> getDepartIdsByUserIds(Collection<String> userIds) {
return sysDepartService.queryDepartIdsByUserIds(userIds);
}
@Override
public Set<String> getDepartParentIdsByUsername(String username) {
List<SysDepart> list = sysDepartService.queryDepartsByUsername(username);
@@ -404,6 +409,29 @@ public class SysBaseApiImpl implements ISysBaseAPI {
return result;
}
@Override
@Cacheable(cacheNames = CacheConstant.SYS_USERS_CACHE, key = "#username + '::main_depart_info'", unless = "#result == null")
public SysDepartModel queryMainDepartByUsername(String username) {
if (oConvertUtils.isEmpty(username)) {
return null;
}
// 根据用户名查询主部门信息
SysDepart mainDepart = userMapper.getMainDepartByUsername(username);
if (mainDepart == null) {
return null;
}
// 复制部门信息到模型对象
SysDepartModel model = new SysDepartModel();
BeanUtils.copyProperties(mainDepart, model);
// 设置部门路径名称
String departPathName = sysDepartService.getDepartPathNameByOrgCode(model.getOrgCode(), null);
model.setDepartPathName(departPathName);
return model;
}
@Override
public DictModel getParentDepartId(String departId) {
SysDepart depart = departMapper.getParentDepartId(departId);
@@ -2179,7 +2207,7 @@ public class SysBaseApiImpl implements ISysBaseAPI {
log.error("{} UniPush消息推送失败 返回response:{}", pushType, response.getBody());
}
} catch (RestClientException e) {
log.warn("UniAPP 消息推送异常:"+ e.getMessage(), e);
log.warn("UniAPP 消息推送异常:"+ e.getMessage());
}
}
/**

View File

@@ -353,7 +353,14 @@ public class SysCommentServiceImpl extends ServiceImpl<SysCommentMapper, SysComm
try {
String ctxPath = uploadpath;
String fileName = null;
File file = new File(ctxPath + File.separator + bizPath + File.separator);
//update-begin---author:liusq ---date:2026-03-30 for【issues/9427】修复uploadLocal bizPath路径遍历漏洞(CWE-22)-----------
// 路径遍历校验规范化后确保目标目录在uploadpath内
File uploadDir = new File(ctxPath).getCanonicalFile();
File file = new File(ctxPath + File.separator + bizPath + File.separator).getCanonicalFile();
if (!file.toPath().startsWith(uploadDir.toPath())) {
throw new JeecgBootException("非法业务路径,禁止访问上传目录之外的路径: " + bizPath);
}
//update-end---author:liusq ---date:2026-03-30 for【issues/9427】修复uploadLocal bizPath路径遍历漏洞(CWE-22)-----------
if (!file.exists()) {
file.mkdirs();// 创建文件根目录
}

View File

@@ -34,7 +34,7 @@ import org.jeecg.modules.system.vo.lowapp.ExportDepartVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import java.util.*;
import java.util.function.Consumer;
@@ -563,6 +563,21 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
return list;
}
@Override
public Map<String, List<String>> queryDepartIdsByUserIds(Collection<String> userIds) {
List<Map<String, String>> mapList = baseMapper.queryDepartIdsByUserIds(userIds);
if (CollectionUtils.isEmpty(mapList)) {
return Map.of();
}
Map<String, List<String>> res = new HashMap<>();
for (Map<String, String> map : mapList) {
String userId = map.get("user_id");
String departId = map.get("depart_id");
res.computeIfAbsent(userId, k -> new ArrayList<>()).add(departId);
}
return res;
}
/**
* 根据用户所负责部门ids获取父级部门编码
* @param departIds
@@ -665,7 +680,7 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
* @return
*/
@Override
public List<SysDepartTreeModel> queryTreeListByPid(String parentId,String ids, String primaryKey) {
public List<SysDepartTreeModel> queryTreeListByPid(String parentId,String ids, String primaryKey, String orgCategory) {
Consumer<LambdaQueryWrapper<SysDepart>> square = i -> {
if (oConvertUtils.isNotEmpty(ids)) {
if (CommonConstant.DEPART_KEY_ORG_CODE.equals(primaryKey)) {
@@ -689,8 +704,13 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
}
//------------------------------------------------------------------------------------------------
lqw.eq(true,SysDepart::getDelFlag,CommonConstant.DEL_FLAG_0.toString());
// 代码逻辑说明: 【QQYUN-13427】部门选择组件修改:需要过滤掉岗位 只保留 公司 子公司 部门---
lqw.ne(SysDepart::getOrgCategory,DepartCategoryEnum.DEPART_CATEGORY_POST.getValue());
// 按 orgCategory 过滤:传入则精确匹配,否则默认排除岗位
if (oConvertUtils.isNotEmpty(orgCategory)) {
lqw.in(SysDepart::getOrgCategory, (Object[]) orgCategory.split(SymbolConstant.COMMA));
} else {
// 代码逻辑说明: 【QQYUN-13427】部门选择组件修改:需要过滤掉岗位 只保留 公司 子公司 部门---
lqw.ne(SysDepart::getOrgCategory, DepartCategoryEnum.DEPART_CATEGORY_POST.getValue());
}
lqw.func(square);
// 代码逻辑说明: [VUEN-1143]排序不对vue3和2应该都有问题应该按照升序排------------
lqw.orderByAsc(SysDepart::getDepartOrder);
@@ -1754,12 +1774,34 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
//step2 查看是否有子级部门,存在递归查询职位
if (!CommonConstant.IS_LEAF.equals(sysDepartPosition.getIzLeaf())) {
//获取子级职位根据部门编码
this.getChildrenDepartPositionByOrgCode(selectTreeVos, departNameMap, sysDepartPosition);
this.getChildrenDepartPositionByOrgCode(selectTreeVos, departNameMap, sysDepartPosition,departId);
return buildTree(selectTreeVos);
}
return new ArrayList<>();
}
/**
* 获取所有部门职务
* @param departId
* @return
*/
@Override
public List<SysPositionSelectTreeVo> getALLRankRelation(String departId) {
//记录当前部门 key为部门id,value为部门名称
Map<String, String> departNameMap = new HashMap<>(5);
//step1 根据id查询部门信息
List<SysDepartPositionVo> departPositionList = baseMapper.getAllDepartPost(departId);
List<SysPositionSelectTreeVo> selectTreeVos = new ArrayList<>();
departPositionList.forEach(position -> {
//step2 查看是否有子级部门,存在递归查询职位
if (!CommonConstant.IS_LEAF.equals(position.getIzLeaf())) {
//获取子级职位根据部门编码
this.getChildrenDepartPositionByOrgCode(selectTreeVos, departNameMap, position,departId);
}
});
return buildTree(selectTreeVos);
}
/**
* 获取子级职位根据部门编码
*
@@ -1767,7 +1809,7 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
* @param departNameMap
* @param sysDepartPosition
*/
private void getChildrenDepartPositionByOrgCode(List<SysPositionSelectTreeVo> selectTreeVos, Map<String, String> departNameMap, SysDepartPositionVo sysDepartPosition) {
private void getChildrenDepartPositionByOrgCode(List<SysPositionSelectTreeVo> selectTreeVos, Map<String, String> departNameMap, SysDepartPositionVo sysDepartPosition,String departId) {
String orgCode = sysDepartPosition.getOrgCode();
//step1 根据父级id获取子级部门信息
List<SysDepartPositionVo> positionList = baseMapper.getDepartPostByOrgCode(orgCode + "%");
@@ -1778,9 +1820,11 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
departNameMap = new HashMap<>(5);
}
SysDepart depart = baseMapper.getDepartById(position.getParentId());
if(null != depart){
position.setDepartName(depart.getDepartName());
}
if(null != depart && oConvertUtils.isNotEmpty(departId)) {
position.setDepartName(depart.getDepartName());
}else{
position.setDepartName(this.getDepartPathNameByOrgCode(depart.getOrgCode(),null));
}
if(oConvertUtils.isNotEmpty(position.getDepPostParentId())){
LambdaQueryWrapper<SysDepart> query = new LambdaQueryWrapper<>();
query.eq(SysDepart::getId,position.getDepPostParentId());
@@ -1793,6 +1837,9 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
departNameMap.put(position.getParentId(), position.getPositionName());
//查看是否为部门岗位,不是则不需要处理
SysPositionSelectTreeVo treeVo = new SysPositionSelectTreeVo(position);
if(oConvertUtils.isEmpty(departId)){
treeVo.setOrgCode(position.getOrgCode());
}
selectTreeVos.add(treeVo);
}
}
@@ -2252,4 +2299,5 @@ public class SysDepartServiceImpl extends ServiceImpl<SysDepartMapper, SysDepart
}
return page.setRecords(departmentHead);
}
}

View File

@@ -559,11 +559,15 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
String orderInfo = keyword.substring(keyword.indexOf(orderKey) + orderKey.length() + 1, keyword.length() - 1);
keyword = keyword.substring(0, keyword.indexOf(orderKey));
String[] orderInfoArray = orderInfo.split(SymbolConstant.COMMA);
orderField = orderInfoArray[0];
orderType = orderInfoArray[1];
// 【issue/9570】排序字段和排序方向使用白名单校验防止 boolean-blind SQL 注入CASE WHEN/LIKE/database() 等绕过黑名单)
orderField = SqlInjectionUtil.getSqlInjectField(orderInfoArray[0]);
orderType = SqlInjectionUtil.getSqlInjectOrderType(orderInfoArray[1]);
}
if (oConvertUtils.isNotEmpty(keyword)) {
// 【安全】对keyword进行SQL注入检测和单引号转义防止通过keyword参数进行SQL注入
keyword = keyword.replace("'", "''");
// 判断是否是多选
if (keyword.contains(SymbolConstant.COMMA)) {
// 代码逻辑说明: JTC-529【表单设计器】 编辑页面报错in参数采用双引号导致 ----
@@ -661,6 +665,13 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
if (query != null) {
for (Map.Entry<String, String> searchItem : query.entrySet()) {
String fieldName = searchItem.getKey();
// update-begin---author:sjlei---date:20260413 for【#9524】修复 SQL _tableFilterSql 注入漏洞
// _tableFilterSql 是服务端内部专用 key对应 Mapper 中的 ${value} 裸拼接,
// 禁止从外部 condition 参数传入,防止 SQL 注入(#9520
if ("_tableFilterSql".equals(fieldName)) {
continue;
}
// update-end-----author:sjlei---date:20260413 for【#9520】修复 SQL _tableFilterSql 注入漏洞
queryParams.put(SqlInjectionUtil.getSqlInjectField(fieldName), searchItem.getValue());
}
}

View File

@@ -3,6 +3,7 @@ package org.jeecg.modules.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
@@ -235,7 +236,19 @@ public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, S
@Override
public List<SysPermission> queryByUser(String userId) {
List<SysPermission> permissionList = this.sysPermissionMapper.queryByUser(userId);
//update-begin---author:scott ---date:2026-04-16 for【pull/9445】开启多租户模式时获取用户权限时加入tenant_id判断-----------
List<SysPermission> permissionList;
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), -1);
if (tenantId != -1) {
permissionList = this.sysPermissionMapper.queryByUserWithTenantId(userId, tenantId);
} else {
permissionList = this.sysPermissionMapper.queryByUser(userId);
}
} else {
permissionList = this.sysPermissionMapper.queryByUser(userId);
}
//update-end---author:scott ---date:2026-04-16 for【pull/9445】开启多租户模式时获取用户权限时加入tenant_id判断-----------
//================= begin 开启租户的时候 如果没有test角色默认加入test角色================
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
if (permissionList == null) {

View File

@@ -6,8 +6,10 @@ import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.modules.system.entity.SysPosition;
import org.jeecg.modules.system.mapper.SysPositionMapper;
import org.jeecg.modules.system.service.ISysPositionService;
import org.jeecg.modules.system.vo.SysPositionVO;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -40,4 +42,14 @@ public class SysPositionServiceImpl extends ServiceImpl<SysPositionMapper, SysPo
}
return "";
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
@Override
public List<SysPositionVO> getPositionListByUserIds(List<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyList();
}
return this.baseMapper.getPositionListByUserIds(userIds);
}
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
}

View File

@@ -29,7 +29,7 @@ public class SysRoleIndexServiceImpl extends ServiceImpl<SysRoleIndexMapper, Sys
private RedisUtil redisUtil;
@Override
@Cacheable(cacheNames = DefIndexConst.CACHE_KEY, key = "'" + DefIndexConst.DEF_INDEX_ALL + "'")
@Cacheable(cacheNames = DefIndexConst.CACHE_KEY + "#3600", key = "'" + DefIndexConst.DEF_INDEX_ALL + "'")
public SysRoleIndex queryDefaultIndex() {
LambdaQueryWrapper<SysRoleIndex> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysRoleIndex::getRoleCode, DefIndexConst.DEF_INDEX_ALL);

View File

@@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -151,6 +152,19 @@ public class SysThirdAccountServiceImpl extends ServiceImpl<SysThirdAccountMappe
return super.getOne(queryWrapper);
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
@Override
public List<SysThirdAccount> listBySysUserIds(List<String> sysUserIds, String thirdType) {
if (sysUserIds == null || sysUserIds.isEmpty()) {
return Collections.emptyList();
}
LambdaQueryWrapper<SysThirdAccount> qw = new LambdaQueryWrapper<>();
qw.in(SysThirdAccount::getSysUserId, sysUserIds);
qw.eq(SysThirdAccount::getThirdType, thirdType);
return list(qw);
}
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
@Override
public List<SysThirdAccount> listThirdUserIdByUsername(String[] sysUsernameArr, String thirdType, Integer tenantId) {
return sysThirdAccountMapper.selectThirdIdsByUsername(sysUsernameArr, thirdType,tenantId);

View File

@@ -0,0 +1,41 @@
package org.jeecg.modules.system.service.impl;
import org.jeecg.modules.system.entity.SysUgroup;
import org.jeecg.modules.system.entity.SysUgroupUser;
import org.jeecg.modules.system.mapper.SysUgroupMapper;
import org.jeecg.modules.system.service.ISysUgroupService;
import org.jeecg.modules.system.service.ISysUgroupUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.List;
/**
* @Description: 用户组表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
@Service("sysUgroupServiceImpl")
public class SysUgroupServiceImpl extends ServiceImpl<SysUgroupMapper, SysUgroup> implements ISysUgroupService {
@Autowired
private ISysUgroupUserService sysUgroupUserService;
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteById(String id) {
this.baseMapper.deleteById(id);
sysUgroupUserService.remove(new QueryWrapper<SysUgroupUser>().eq("group_id", id));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByIds(List<String> list) {
this.baseMapper.deleteBatchIds(list);
sysUgroupUserService.remove(new QueryWrapper<SysUgroupUser>().in("group_id", list));
}
}

View File

@@ -0,0 +1,19 @@
package org.jeecg.modules.system.service.impl;
import org.jeecg.modules.system.entity.SysUgroupUser;
import org.jeecg.modules.system.mapper.SysUgroupUserMapper;
import org.jeecg.modules.system.service.ISysUgroupUserService;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
/**
* @Description: 用户组关系表
* @Author: jeecg-boot
* @Date: 2026-02-27
* @Version: V1.0
*/
@Service("sysUgroupUserServiceImpl")
public class SysUgroupUserServiceImpl extends ServiceImpl<SysUgroupUserMapper, SysUgroupUser> implements ISysUgroupUserService {
}

View File

@@ -135,7 +135,7 @@ public class SysUserDepartServiceImpl extends ServiceImpl<SysUserDepartMapper, S
realname = realname.trim();
}
List<SysUser> userList = this.baseMapper.queryDepartUserList(depCode, realname);
Map<String, SysUser> map = new HashMap(5);
Map<String, SysUser> map = new LinkedHashMap(5);
for (SysUser sysUser : userList) {
// 返回的用户数据去掉密码信息
sysUser.setSalt("");

View File

@@ -3036,6 +3036,31 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
this.baseMapper.updateById(sysUser);
}
/**
* 根据用户组查询用户列表
* @param page
* @param groupId
* @param username
* @param realname
* @return
*/
@Override
public IPage<SysUser> getUserByUgroupId(Page<SysUser> page, String groupId, String username, String realname) {
IPage<SysUser> userGroupList = userMapper.getUserByUgroupId(page, groupId, username,realname);
List<SysUser> records = userGroupList.getRecords();
if (null != records && records.size() > 0) {
List<String> userIds = records.stream().map(SysUser::getId).collect(Collectors.toList());
Map<String, String> useDepNames = this.getDepNamesByUserIds(userIds);
for (SysUser sysUser : userGroupList.getRecords()) {
//设置部门
sysUser.setOrgCodeTxt(useDepNames.get(sysUser.getId()));
//设置用户职位id
this.userPositionId(sysUser);
}
}
return userGroupList;
}
/**
* 是否有交集
*/

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.jeecg.dingtalk.api.base.JdtBaseAPI;
import com.jeecg.dingtalk.api.core.response.Response;
@@ -40,6 +41,7 @@ import org.jeecg.modules.system.mapper.*;
import org.jeecg.modules.system.model.SysDepartTreeModel;
import org.jeecg.modules.system.model.ThirdLoginModel;
import org.jeecg.modules.system.service.*;
import org.jeecg.modules.system.vo.SysPositionVO;
import org.jeecg.modules.system.vo.thirdapp.JdtDepartmentTreeVo;
import org.jeecg.modules.system.vo.thirdapp.SyncInfoVo;
import org.springframework.beans.BeanUtils;
@@ -346,6 +348,41 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
// 获取本地所有用户
sysUsers = userMapper.selectList(Wrappers.emptyWrapper());
}
if (CollectionUtils.isEmpty(sysUsers)) {
return syncInfo;
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
List<String> userIds = sysUsers.stream().map(SysUser::getId).collect(Collectors.toList());
// ① 批量预加载 sys_third_account → Map<sysUserId, SysThirdAccount>
Map<String, SysThirdAccount> thirdAccountMap = sysThirdAccountService
.listBySysUserIds(userIds, THIRD_TYPE)
.stream()
.collect(Collectors.toMap(SysThirdAccount::getSysUserId, a -> a, (a, b) -> a));
// ② 批量预加载用户-部门关系 → Map<userId, List<departId>>
LambdaQueryWrapper<SysUserDepart> udQw = new LambdaQueryWrapper<>();
udQw.in(SysUserDepart::getUserId, userIds);
Map<String, List<String>> userDepartIdsMap = sysUserDepartService.list(udQw)
.stream()
.collect(Collectors.groupingBy(
SysUserDepart::getUserId,
Collectors.mapping(SysUserDepart::getDepId, Collectors.toList())
));
// ③ 批量预加载所有涉及的部门 → Map<departId, SysDepart>
Set<String> allDepartIds = userDepartIdsMap.values().stream()
.flatMap(Collection::stream).collect(Collectors.toSet());
Map<String, SysDepart> departMap = Collections.emptyMap();
if (!allDepartIds.isEmpty()) {
departMap = sysDepartService.listByIds(allDepartIds)
.stream()
.collect(Collectors.toMap(SysDepart::getId, d -> d, (a, b) -> a));
}
// ④ 批量预加载职位 → Map<userId, List<SysPositionVO>>
Map<String, List<SysPositionVO>> positionMap = sysPositionService
.getPositionListByUserIds(userIds)
.stream()
.collect(Collectors.groupingBy(SysPositionVO::getUserId));
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
// 查询钉钉所有的部门,用于同步用户和部门的关系
List<Department> allDepartment = JdtDepartmentAPI.listAll(accessToken);
@@ -361,7 +398,9 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
* 1. 查询 sys_third_account第三方账号表是否有数据如果有代表已同步
* 2. 本地表里没有就先用手机号判断不通过再用username(用户账号)判断。
*/
SysThirdAccount sysThirdAccount = sysThirdAccountService.getOneBySysUserId(sysUser.getId(), THIRD_TYPE);
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
SysThirdAccount sysThirdAccount = thirdAccountMap.get(sysUser.getId());
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
if (sysThirdAccount != null && oConvertUtils.isNotEmpty(sysThirdAccount.getThirdUserId())) {
// sys_third_account 表匹配成功通过第三方userId查询出第三方userInfo
dtUserInfo = JdtUserAPI.getUserById(sysThirdAccount.getThirdUserId(), accessToken);
@@ -384,12 +423,16 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
if (dtUserInfo != null && dtUserInfo.isSuccess() && dtUserInfo.getResult() != null) {
User dtUser = dtUserInfo.getResult();
dtUserId = dtUser.getUserid();
User updateQwUser = this.sysUserToDtUser(sysUser, dtUser, allDepartment);
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
User updateQwUser = this.sysUserToDtUser(sysUser, dtUser, allDepartment, userDepartIdsMap, departMap, positionMap);
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
Response<JSONObject> updateRes = JdtUserAPI.update(updateQwUser, accessToken);
// 收集成功/失败信息
apiSuccess = this.syncUserCollectErrInfo(updateRes, sysUser, syncInfo);
} else {
User newQwUser = this.sysUserToDtUser(sysUser, allDepartment);
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
User newQwUser = this.sysUserToDtUser(sysUser, allDepartment, userDepartIdsMap, departMap, positionMap);
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
Response<String> createRes = JdtUserAPI.create(newQwUser, accessToken);
dtUserId = createRes.getResult();
// 收集成功/失败信息
@@ -611,6 +654,56 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
return user;
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 【同步用户】将SysUser转为【钉钉】的User对象创建新用户使用批量预加载Map消除N+1查询
*/
private User sysUserToDtUser(SysUser sysUser, List<Department> allDepartment,
Map<String, List<String>> userDepartIdsMap, Map<String, SysDepart> departMap,
Map<String, List<SysPositionVO>> positionMap) {
User user = new User();
user.setUserid(sysUser.getUsername());
return this.sysUserToDtUser(sysUser, user, allDepartment, userDepartIdsMap, departMap, positionMap);
}
/**
* 【同步用户】将SysUser转为【钉钉】的User对象更新旧用户使用批量预加载Map消除N+1查询
*/
private User sysUserToDtUser(SysUser sysUser, User user, List<Department> allDepartment,
Map<String, List<String>> userDepartIdsMap, Map<String, SysDepart> departMap,
Map<String, List<SysPositionVO>> positionMap) {
user.setName(sysUser.getRealname());
user.setMobile(sysUser.getPhone());
user.setTelephone(sysUser.getTelephone());
user.setJob_number(sysUser.getWorkNo());
// 职务翻译使用预加载Map替代单次查询
List<SysPositionVO> positionList = positionMap.getOrDefault(sysUser.getId(), Collections.emptyList());
if (!positionList.isEmpty()) {
String positionName = positionList.stream().map(SysPositionVO::getName).collect(Collectors.joining(SymbolConstant.COMMA));
user.setTitle(positionName);
}
user.setEmail(sysUser.getEmail());
// 查询并同步用户部门关系使用预加载Map替代单次查询
List<SysDepart> departList = this.getUserDepart(sysUser, userDepartIdsMap, departMap);
if (departList != null) {
List<Integer> departmentIdList = new ArrayList<>();
for (SysDepart sysDepart : departList) {
Department department = this.getDepartmentByDepartId(sysDepart.getId(), allDepartment);
if (department != null) {
departmentIdList.add(department.getDept_id());
}
}
user.setDept_id_list(departmentIdList.toArray(new Integer[]{}));
user.setDept_order_list(null);
}
if (oConvertUtils.isEmpty(user.getDept_id_list())) {
user.setDept_id_list(1);
user.setDept_order_list(null);
}
return user;
}
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 【同步用户】将【钉钉】的User对象转为SysUser创建新用户
@@ -694,6 +787,24 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
return departList.size() == 0 ? null : departList;
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 查询用户和部门的关系使用批量预加载Map消除N+1查询
*/
private List<SysDepart> getUserDepart(SysUser sysUser, Map<String, List<String>> userDepartIdsMap,
Map<String, SysDepart> departMap) {
List<String> departIds = userDepartIdsMap.get(sysUser.getId());
if (departIds == null || departIds.isEmpty()) {
return null;
}
List<SysDepart> departList = departIds.stream()
.map(departMap::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
return departList.isEmpty() ? null : departList;
}
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 根据sysDepartId查询钉钉的部门
*/

View File

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.jeecg.qywx.api.base.JwAccessTokenAPI;
import com.jeecg.qywx.api.core.common.AccessToken;
@@ -43,6 +44,7 @@ import org.jeecg.modules.system.entity.*;
import org.jeecg.modules.system.mapper.*;
import org.jeecg.modules.system.model.SysDepartTreeModel;
import org.jeecg.modules.system.service.*;
import org.jeecg.modules.system.vo.SysPositionVO;
import org.jeecg.modules.system.vo.thirdapp.JwDepartmentTreeVo;
import org.jeecg.modules.system.vo.thirdapp.JwSysUserDepartVo;
import org.jeecg.modules.system.vo.thirdapp.JwUserDepartVo;
@@ -353,6 +355,40 @@ public class ThirdAppWechatEnterpriseServiceImpl implements IThirdAppService {
// 获取本地所有用户
sysUsers = userMapper.selectList(Wrappers.emptyWrapper());
}
if (CollectionUtils.isEmpty(sysUsers)) {
return syncInfo;
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
List<String> userIds = sysUsers.stream().map(SysUser::getId).collect(Collectors.toList());
// ① 批量预加载 sys_third_account → Map<sysUserId, SysThirdAccount>
Map<String, SysThirdAccount> thirdAccountMap = sysThirdAccountService
.listBySysUserIds(userIds, THIRD_TYPE)
.stream()
.collect(Collectors.toMap(SysThirdAccount::getSysUserId, a -> a, (a, b) -> a));
// ② 批量预加载用户-部门关系 → Map<userId, List<departId>>
LambdaQueryWrapper<SysUserDepart> udQw = new LambdaQueryWrapper<>();
udQw.in(SysUserDepart::getUserId, userIds);
Map<String, List<String>> userDepartIdsMap = sysUserDepartService.list(udQw)
.stream()
.collect(Collectors.groupingBy(
SysUserDepart::getUserId,
Collectors.mapping(SysUserDepart::getDepId, Collectors.toList())
));
// ③ 批量预加载所有涉及的部门 → Map<departId, SysDepart>
Set<String> allDepartIds = userDepartIdsMap.values().stream()
.flatMap(Collection::stream).collect(Collectors.toSet());
Map<String, SysDepart> departMap = Collections.emptyMap();
if (!allDepartIds.isEmpty()) {
departMap = sysDepartService.listByIds(allDepartIds)
.stream()
.collect(Collectors.toMap(SysDepart::getId, d -> d, (a, b) -> a));
}
// ④ 批量预加载职位 → Map<userId, List<SysPositionVO>>
Map<String, List<SysPositionVO>> positionMap = sysPositionService
.getPositionListByUserIds(userIds)
.stream()
.collect(Collectors.groupingBy(SysPositionVO::getUserId));
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
// 循环判断新用户和需要更新的用户
for1:
@@ -367,7 +403,9 @@ public class ThirdAppWechatEnterpriseServiceImpl implements IThirdAppService {
* 2. 本地表里没有就先用手机号判断不通过再用username判断。
*/
User qwUser;
SysThirdAccount sysThirdAccount = sysThirdAccountService.getOneBySysUserId(sysUser.getId(), THIRD_TYPE);
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
SysThirdAccount sysThirdAccount = thirdAccountMap.get(sysUser.getId());
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
for (User qwUserTemp : qwUsers) {
if (sysThirdAccount == null || oConvertUtils.isEmpty(sysThirdAccount.getThirdUserId()) || !sysThirdAccount.getThirdUserId().equals(qwUserTemp.getUserid())) {
// sys_third_account 表匹配失败,尝试用手机号匹配
@@ -383,7 +421,9 @@ public class ThirdAppWechatEnterpriseServiceImpl implements IThirdAppService {
// }
}
// 循环到此说明用户匹配成功,进行更新操作
qwUser = this.sysUserToQwUser(sysUser, qwUserTemp);
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
qwUser = this.sysUserToQwUser(sysUser, qwUserTemp, userDepartIdsMap, departMap, positionMap);
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
int errCode = JwUserAPI.updateUser(qwUser, accessToken);
// 收集错误信息
this.syncUserCollectErrInfo(errCode, sysUser, syncInfo);
@@ -392,7 +432,9 @@ public class ThirdAppWechatEnterpriseServiceImpl implements IThirdAppService {
continue for1;
}
// 循环到此说明是新用户,直接调接口创建
qwUser = this.sysUserToQwUser(sysUser);
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
qwUser = this.sysUserToQwUser(sysUser, userDepartIdsMap, departMap, positionMap);
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
int errCode = JwUserAPI.createUser(qwUser, accessToken);
// 收集错误信息
boolean apiSuccess = this.syncUserCollectErrInfo(errCode, sysUser, syncInfo);
@@ -554,6 +596,79 @@ public class ThirdAppWechatEnterpriseServiceImpl implements IThirdAppService {
return this.sysUserToQwUser(sysUser, user);
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 【同步用户】将SysUser转为企业微信的User对象创建新用户使用批量预加载Map
*/
private User sysUserToQwUser(SysUser sysUser, Map<String, List<String>> userDepartIdsMap,
Map<String, SysDepart> departMap, Map<String, List<SysPositionVO>> positionMap) {
User user = new User();
user.setUserid(sysUser.getUsername());
return this.sysUserToQwUser(sysUser, user, userDepartIdsMap, departMap, positionMap);
}
/**
* 【同步用户】将SysUser转为企业微信的User对象更新旧用户使用批量预加载Map
*/
private User sysUserToQwUser(SysUser sysUser, User user, Map<String, List<String>> userDepartIdsMap,
Map<String, SysDepart> departMap, Map<String, List<SysPositionVO>> positionMap) {
user.setName(sysUser.getRealname());
user.setMobile(sysUser.getPhone());
// 查询并同步用户部门关系使用预加载Map替代单次查询
List<SysDepart> departList = this.getUserDepart(sysUser, userDepartIdsMap, departMap);
if (departList != null) {
List<Integer> departmentIdList = new ArrayList<>();
List<Integer> isLeaderInDept = new ArrayList<>();
List<String> manageDepartIdList = new ArrayList<>();
if (oConvertUtils.isNotEmpty(sysUser.getDepartIds())) {
manageDepartIdList = Arrays.asList(sysUser.getDepartIds().split(","));
}
for (SysDepart sysDepart : departList) {
if (oConvertUtils.isNotEmpty(sysDepart.getQywxIdentifier())) {
try {
departmentIdList.add(Integer.parseInt(sysDepart.getQywxIdentifier()));
} catch (NumberFormatException ignored) {
continue;
}
if (CommonConstant.USER_IDENTITY_2.equals(sysUser.getUserIdentity())) {
isLeaderInDept.add(manageDepartIdList.contains(sysDepart.getId()) ? 1 : 0);
} else {
isLeaderInDept.add(0);
}
}
}
user.setDepartment(departmentIdList.toArray(new Integer[]{}));
user.setIs_leader_in_dept(isLeaderInDept.toArray(new Integer[]{}));
}
if (user.getDepartment() == null || user.getDepartment().length == 0) {
user.setDepartment(new Integer[]{1});
user.setIs_leader_in_dept(new Integer[]{0});
}
// 职务翻译使用预加载Map替代单次查询
List<SysPositionVO> positionList = positionMap.getOrDefault(sysUser.getId(), Collections.emptyList());
if (!positionList.isEmpty()) {
String positionName = positionList.stream().map(SysPositionVO::getName).collect(Collectors.joining(SymbolConstant.COMMA));
user.setPosition(positionName);
}
if (sysUser.getSex() != null) {
user.setGender(sysUser.getSex().toString());
}
user.setEmail(sysUser.getEmail());
if (sysUser.getStatus() != null) {
if (CommonConstant.USER_UNFREEZE.equals(sysUser.getStatus()) || CommonConstant.USER_FREEZE.equals(sysUser.getStatus())) {
user.setEnable(sysUser.getStatus() == 1 ? 1 : 0);
} else {
user.setEnable(1);
}
}
user.setTelephone(sysUser.getTelephone());
if (CommonConstant.DEL_FLAG_1.equals(sysUser.getDelFlag())) {
user.setEnable(0);
}
return user;
}
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 【同步用户】将SysUser转为企业微信的User对象更新旧用户
*/
@@ -648,6 +763,24 @@ public class ThirdAppWechatEnterpriseServiceImpl implements IThirdAppService {
return departList.size() == 0 ? null : departList;
}
//update-begin---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 查询用户和部门的关系使用批量预加载Map消除N+1查询
*/
private List<SysDepart> getUserDepart(SysUser sysUser, Map<String, List<String>> userDepartIdsMap,
Map<String, SysDepart> departMap) {
List<String> departIds = userDepartIdsMap.get(sysUser.getId());
if (departIds == null || departIds.isEmpty()) {
return null;
}
List<SysDepart> departList = departIds.stream()
.map(departMap::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
return departList.isEmpty() ? null : departList;
}
//update-end---author:sjlei ---date:2026-04-17 for【#9496】全量同步N+1查询性能优化-----------
/**
* 【同步用户】将企业微信的User对象转为SysUser创建新用户
*/

View File

@@ -4,6 +4,7 @@ import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.jeecg.common.util.MyCommonsMultipartFile;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
@@ -26,6 +27,7 @@ public class HttpFileToMultipartFileUtil {
* @throws Exception
*/
public static MultipartFile httpFileToMultipartFile(String fileUrl, String filename) throws Exception {
SsrfFileTypeFilter.checkSsrfHttpUrl(fileUrl);
byte[] bytes = downloadImageData(fileUrl);
return convertByteToMultipartFile(bytes, filename);
}

View File

@@ -32,6 +32,43 @@ public class XssUtils {
Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
};
//update-begin---author:liusq ---date:2025-04-13 for【issues/9521】富文本msgContent字段存储型XSS过滤-----------
/**
* 针对富文本HTML内容的XSS过滤移除危险脚本和事件处理器保留合法HTML标签和样式。
* 与 scriptXss() 的区别本方法不对HTML实体进行全局转义适用于富文本内容如公告正文
*/
private static final Pattern[] RICH_TEXT_PATTERNS = new Pattern[]{
// <script> 标签及其内容
Pattern.compile("<script[^>]*>[\\s\\S]*?</script>", Pattern.CASE_INSENSITIVE),
// 自闭合 <script/>
Pattern.compile("<script[^>]*/>", Pattern.CASE_INSENSITIVE),
// 所有内联事件处理器属性,如 onerror= onclick= onmouseover= 等
Pattern.compile("\\s+on\\w+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)", Pattern.CASE_INSENSITIVE),
// javascript: 协议
Pattern.compile("javascript\\s*:", Pattern.CASE_INSENSITIVE),
// vbscript: 协议
Pattern.compile("vbscript\\s*:", Pattern.CASE_INSENSITIVE),
// CSS expression()IE 特有)
Pattern.compile("expression\\s*\\(", Pattern.CASE_INSENSITIVE),
// <iframe> 标签(防止内嵌恶意页面)
Pattern.compile("<iframe[^>]*>[\\s\\S]*?</iframe>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<iframe[^>]*/>", Pattern.CASE_INSENSITIVE),
// <object> / <embed> 标签
Pattern.compile("<object[^>]*>[\\s\\S]*?</object>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<embed[^>]*>", Pattern.CASE_INSENSITIVE),
};
public static String richTextXss(String value) {
if (value == null) {
return null;
}
for (Pattern pattern : RICH_TEXT_PATTERNS) {
value = pattern.matcher(value).replaceAll("");
}
return value;
}
//update-end---author:liusq ---date:2025-04-13 for【issues/9521】富文本msgContent字段存储型XSS过滤-----------
public static String scriptXss(String value) {
if (value != null) {
value = value.replaceAll(" ", "");

View File

@@ -0,0 +1,20 @@
package org.jeecg.modules.system.vo;
import lombok.Data;
import org.jeecg.modules.system.entity.SysDictItem;
import java.util.List;
/**
* @Description: 批量字典VO
* @author: zzl
*/
@Data
public class SysDictBatchVo {
/**
* 字典列表
*/
private List<SysDictPage> dictList;
}

View File

@@ -0,0 +1,23 @@
package org.jeecg.modules.system.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jeecg.modules.system.entity.SysPosition;
/**
* 职务VO扩展了userId字段用于批量查询职位时携带用户ID供全量同步批量预加载场景
*
* @author sjlei
* @version V1.0
* @date 2026-04-17
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SysPositionVO extends SysPosition {
/**
* 批量查询时携带的用户ID非数据库字段仅用于查询结果分组
*/
private String userId;
}

View File

@@ -0,0 +1,31 @@
package org.jeecg.modules.system.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* @Description: 用户组vo
* @author: jeecg-boot
*/
@Data
public class SysUserGroupVO implements Serializable{
private static final long serialVersionUID = 1L;
/**用户组id*/
private String groupId;
/**对应的用户id集合*/
private List<String> userIdList;
public SysUserGroupVO() {
super();
}
public SysUserGroupVO(String groupId, List<String> userIdList) {
super();
this.groupId = groupId;
this.userIdList = userIdList;
}
}