更新项目配置,新增设备同步模块,优化WebSocket和Swagger配置,增强SCADA系统的免登录接口,支持数据字典项和登录日志的免登录查询与记录。调整Java编译设置,确保更好的开发体验。

This commit is contained in:
geht
2026-04-28 10:23:58 +08:00
parent bbe46dcf2d
commit 142a0bdaba
1013 changed files with 41858 additions and 28 deletions

View File

@@ -61,6 +61,10 @@
<artifactId>jeecg-boot-module-airag</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>

View File

@@ -10,9 +10,14 @@ import com.alibaba.fastjson.JSONObject;
import org.jeecg.common.base.BaseMap;
import org.jeecg.common.constant.WebsocketConst;
import org.jeecg.common.modules.redis.client.JeecgRedisClient;
import org.jeecg.modules.system.entity.SysLog;
import org.jeecg.modules.system.service.ISysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import java.util.Date;
/**
* @Author scott
@@ -27,6 +32,13 @@ public class WebSocket {
/**线程安全Map*/
private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();
/**
* 当前在线 WebSocket 连接数。
*/
public static int getOnlineCount() {
return sessionPool.size();
}
/**
* Redis触发监听名字
*/
@@ -34,10 +46,15 @@ public class WebSocket {
//避免初次调用出现空指针的情况
private static JeecgRedisClient jeecgRedisClient;
private static ISysLogService sysLogService;
@Autowired
private void setJeecgRedisClient(JeecgRedisClient jeecgRedisClient){
WebSocket.jeecgRedisClient = jeecgRedisClient;
}
@Autowired
private void setSysLogService(ISysLogService sysLogService){
WebSocket.sysLogService = sysLogService;
}
//==========【websocket接受、推送消息等方法 —— 具体服务节点推送ws消息】========================================================================================
@@ -45,7 +62,7 @@ public class WebSocket {
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
try {
sessionPool.put(userId, session);
log.debug("【系统 WebSocket】有新的连接,总数为:" + sessionPool.size());
log.info("【系统 WebSocket】连接建立 userId={}, sessionId={}, online={}", userId, session.getId(), sessionPool.size());
} catch (Exception e) {
}
}
@@ -54,7 +71,7 @@ public class WebSocket {
public void onClose(@PathParam("userId") String userId) {
try {
sessionPool.remove(userId);
log.debug("【系统 WebSocket】连接断开,总数为:" + sessionPool.size());
log.info("【系统 WebSocket】连接断开 userId={}, online={}", userId, sessionPool.size());
} catch (Exception e) {
e.printStackTrace();
}
@@ -67,6 +84,8 @@ public class WebSocket {
* @param message
*/
public void pushMessage(String userId, String message) {
int successCount = 0;
int failCount = 0;
for (Map.Entry<String, Session> item : sessionPool.entrySet()) {
//userId key值= {用户id + "_"+ 登录token的md5串}
//TODO vue2未改key新规则暂时不影响逻辑
@@ -75,29 +94,57 @@ public class WebSocket {
try {
// 代码逻辑说明: websocket报错 https://gitee.com/jeecg/jeecg-boot/issues/I4C0MU
synchronized (session){
log.debug("【系统 WebSocket】推送单人消息:" + message);
session.getBasicRemote().sendText(message);
}
successCount++;
} catch (Exception e) {
failCount++;
log.error(e.getMessage(),e);
}
}
}
// 日志分级策略:业务消息 info心跳消息 debug异常场景 warn/error
if (failCount > 0) {
log.warn("【系统 WebSocket】定向推送存在失败 userKeyContains={}, success={}, fail={}, payload={}",
userId, successCount, failCount, message);
} else if (isHeartbeatOrBlank(message == null ? "" : message.trim())) {
log.debug("【系统 WebSocket】定向心跳推送 userKeyContains={}, success={}, fail={}",
userId, successCount, failCount);
} else {
log.info("【系统 WebSocket】定向业务推送 userKeyContains={}, success={}, fail={}, payload={}",
userId, successCount, failCount, message);
}
}
/**
* ws遍历群发消息
*/
public void pushMessage(String message) {
int successCount = 0;
int failCount = 0;
try {
for (Map.Entry<String, Session> item : sessionPool.entrySet()) {
String sessionKey = item.getKey();
Session session = item.getValue();
try {
item.getValue().getAsyncRemote().sendText(message);
if (session == null || !session.isOpen()) {
failCount++;
log.warn("【系统 WebSocket】连接不可用跳过发送key={}", sessionKey);
continue;
}
// 使用同步发送提升送达确定性,避免 AsyncRemote 在断连边界下静默丢消息
synchronized (session) {
session.getBasicRemote().sendText(message);
}
successCount++;
} catch (Exception e) {
failCount++;
log.warn("【系统 WebSocket】发送失败key={},原因={}", sessionKey, e.getMessage());
log.error(e.getMessage(), e);
}
}
log.debug("【系统 WebSocket】群发消息:" + message);
log.info("【系统 WebSocket】群发完成 success={}, fail={}, online={}, payload={}",
successCount, failCount, sessionPool.size(), message);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
@@ -109,13 +156,25 @@ public class WebSocket {
*/
@OnMessage
public void onMessage(String message, @PathParam(value = "userId") String userId) {
if(!"ping".equals(message) && !WebsocketConst.CMD_CHECK.equals(message)){
log.debug("【系统 WebSocket】收到客户端消息:" + message);
}else{
log.debug("【系统 WebSocket】收到客户端消息:" + message);
// 代码逻辑说明: 【issues/1161】前端websocket因心跳导致监听不起作用---
this.sendMessage(userId, "ping");
String normalized = message == null ? "" : message.trim();
if (isHeartbeatOrBlank(normalized)) {
log.debug("【系统 WebSocket】收到客户端心跳/空消息:{}", normalized);
if ("ping".equalsIgnoreCase(normalized) || WebsocketConst.CMD_CHECK.equalsIgnoreCase(normalized)) {
// 代码逻辑说明: 【issues/1161】前端websocket因心跳导致监听不起作用---
this.sendMessage(userId, "ping");
}
return;
}
if (!isJsonMessage(normalized)) {
log.debug("【系统 WebSocket】忽略非JSON消息:{}", normalized);
return;
}
if (handleScadaLog(normalized, userId)) {
return;
}
log.debug("【系统 WebSocket】收到客户端消息:" + normalized);
// //------------------------------------------------------------------------------
// JSONObject obj = new JSONObject();
@@ -179,6 +238,82 @@ public class WebSocket {
sendMessage(userId, message);
}
}
private boolean handleScadaLog(String message, String userId) {
try {
JSONObject obj = JSONObject.parseObject(message);
if (obj == null) {
return false;
}
String cmd = obj.getString("cmd");
if (!"SCADA_LOGIN_LOG".equals(cmd) && !"SCADA_LOG".equals(cmd)) {
return false;
}
if (sysLogService == null) {
return true;
}
String category = obj.getString("category");
String account = obj.getString("account");
Boolean success = obj.getBoolean("success");
String msg = obj.getString("message");
String clientIp = obj.getString("clientIp");
String clientType = normalizeClientType(obj.getString("clientType"));
String exception = obj.getString("exception");
Integer logType = obj.getInteger("logType");
Integer operateType = obj.getInteger("operateType");
String method = obj.getString("method");
String requestUrl = obj.getString("requestUrl");
SysLog logEntity = new SysLog();
logEntity.setCreateTime(new Date());
logEntity.setLogType(logType == null ? ("EXCEPTION".equalsIgnoreCase(category) ? 2 : 1) : logType);
logEntity.setOperateType(operateType == null ? ("LOGIN".equalsIgnoreCase(category) ? 1 : 5) : operateType);
logEntity.setRequestType("WS");
logEntity.setRequestUrl(oConvertUtils.isEmpty(requestUrl) ? ("/websocket/" + userId) : requestUrl);
logEntity.setMethod(oConvertUtils.isEmpty(method) ? "SCADA_LOG" : method);
logEntity.setUsername(account);
logEntity.setUserid(account);
logEntity.setIp(oConvertUtils.isEmpty(clientIp) ? "desktop" : clientIp);
logEntity.setClientType(clientType);
if ("LOGIN".equalsIgnoreCase(category) || "SCADA_LOGIN_LOG".equals(cmd)) {
logEntity.setLogContent(String.format("桌面端登录%s%s", Boolean.TRUE.equals(success) ? "成功" : "失败", oConvertUtils.isEmpty(msg) ? "" : msg));
} else if ("EXCEPTION".equalsIgnoreCase(category)) {
String exceptionMsg = oConvertUtils.isEmpty(exception) ? "" : (" 异常: " + exception);
logEntity.setLogContent(String.format("桌面端异常日志:%s%s", oConvertUtils.isEmpty(msg) ? "" : msg, exceptionMsg));
} else {
logEntity.setLogContent(String.format("桌面端操作日志:%s", oConvertUtils.isEmpty(msg) ? "" : msg));
}
logEntity.setRequestParam(message);
sysLogService.save(logEntity);
return true;
} catch (Exception e) {
log.warn("处理 SCADA 登录日志消息失败: {}", e.getMessage());
return false;
}
}
private boolean isHeartbeatOrBlank(String message) {
return oConvertUtils.isEmpty(message)
|| "ping".equalsIgnoreCase(message)
|| "pong".equalsIgnoreCase(message)
|| WebsocketConst.CMD_CHECK.equalsIgnoreCase(message);
}
private boolean isJsonMessage(String message) {
return !oConvertUtils.isEmpty(message) && message.startsWith("{");
}
private String normalizeClientType(String rawClientType) {
if (oConvertUtils.isEmpty(rawClientType)) {
return "gkj";
}
String value = rawClientType.trim().toLowerCase();
if ("desktop".equals(value) || "pc".equals(value) || "windows".equals(value)) {
return "gkj";
}
return value;
}
//=======【采用redis发布订阅模式——推送消息】==========================================================================================
}

View File

@@ -40,6 +40,7 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
@@ -153,6 +154,108 @@ public class SysDictController {
return Result.ok(res);
}
/**
* SCADA 系统专用:免登录查询数据字典项。
* 支持按 dictCode / dictName / itemText / itemValue 条件过滤,支持增量同步。
*/
@GetMapping("/scada/queryDictItem")
@Operation(summary = "SCADA-免登录查询数据字典项")
public Result<List<Map<String, Object>>> scadaQueryDictItem(
@RequestParam(name = "dictCode", required = false) String dictCode,
@RequestParam(name = "dictName", required = false) String dictName,
@RequestParam(name = "itemText", required = false) String itemText,
@RequestParam(name = "itemValue", required = false) String itemValue,
@RequestParam(name = "status", required = false) Integer status,
@RequestParam(name = "updatedAfter", required = false) String updatedAfter,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "500") Integer pageSize) {
if (pageNo == null || pageNo < 1) {
pageNo = 1;
}
if (pageSize == null || pageSize < 1) {
pageSize = 500;
}
pageSize = Math.min(pageSize, 1000);
LambdaQueryWrapper<SysDictItem> itemWrapper = new LambdaQueryWrapper<>();
itemWrapper.like(oConvertUtils.isNotEmpty(itemText), SysDictItem::getItemText, itemText);
itemWrapper.like(oConvertUtils.isNotEmpty(itemValue), SysDictItem::getItemValue, itemValue);
itemWrapper.eq(status != null, SysDictItem::getStatus, status);
if (oConvertUtils.isNotEmpty(updatedAfter)) {
try {
Date parsedSyncTime;
try {
parsedSyncTime = DateUtils.parseDate(updatedAfter, "yyyy-MM-dd HH:mm:ss");
} catch (Exception firstEx) {
parsedSyncTime = DateUtils.parseDate(updatedAfter, "yyyy-MM-dd'T'HH:mm:ss");
}
final Date syncTime = parsedSyncTime;
itemWrapper.and(w -> w.ge(SysDictItem::getUpdateTime, syncTime).or().ge(SysDictItem::getCreateTime, syncTime));
} catch (Exception e) {
return Result.error("updatedAfter格式错误支持yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd'T'HH:mm:ss");
}
}
// 若传入字典头过滤条件,先按字典头筛出 dictId 再查询字典项
if (oConvertUtils.isNotEmpty(dictCode) || oConvertUtils.isNotEmpty(dictName)) {
LambdaQueryWrapper<SysDict> dictFilter = new LambdaQueryWrapper<>();
dictFilter.eq(SysDict::getDelFlag, CommonConstant.DEL_FLAG_0);
dictFilter.like(oConvertUtils.isNotEmpty(dictCode), SysDict::getDictCode, dictCode);
dictFilter.like(oConvertUtils.isNotEmpty(dictName), SysDict::getDictName, dictName);
List<SysDict> filteredDicts = sysDictService.list(dictFilter);
if (oConvertUtils.isEmpty(filteredDicts)) {
return Result.ok(new ArrayList<>());
}
List<String> filteredDictIds = filteredDicts.stream().map(SysDict::getId).collect(java.util.stream.Collectors.toList());
itemWrapper.in(SysDictItem::getDictId, filteredDictIds);
}
itemWrapper.orderByAsc(SysDictItem::getSortOrder);
itemWrapper.orderByDesc(SysDictItem::getCreateTime);
IPage<SysDictItem> itemPage = sysDictItemService.page(new Page<>(pageNo, pageSize), itemWrapper);
List<SysDictItem> itemList = itemPage.getRecords();
if (oConvertUtils.isEmpty(itemList)) {
return Result.ok(new ArrayList<>());
}
List<String> dictIds = itemList.stream().map(SysDictItem::getDictId).distinct().collect(java.util.stream.Collectors.toList());
LambdaQueryWrapper<SysDict> dictQuery = new LambdaQueryWrapper<>();
dictQuery.in(SysDict::getId, dictIds);
dictQuery.eq(SysDict::getDelFlag, CommonConstant.DEL_FLAG_0);
List<SysDict> dictList = sysDictService.list(dictQuery);
Map<String, SysDict> dictMap = dictList.stream().collect(java.util.stream.Collectors.toMap(SysDict::getId, d -> d));
List<Map<String, Object>> resultList = new ArrayList<>();
for (SysDictItem item : itemList) {
SysDict dict = dictMap.get(item.getDictId());
if (dict == null) {
continue;
}
Map<String, Object> row = new LinkedHashMap<>(24);
row.put("id", item.getId());
row.put("dictId", item.getDictId());
row.put("dictName", dict.getDictName());
row.put("dictCode", dict.getDictCode());
row.put("dictType", dict.getType());
row.put("dictDescription", dict.getDescription());
row.put("itemText", item.getItemText());
row.put("itemValue", item.getItemValue());
row.put("itemDescription", item.getDescription());
row.put("sortOrder", item.getSortOrder());
row.put("status", item.getStatus());
row.put("itemColor", item.getItemColor());
row.put("createBy", item.getCreateBy());
row.put("createTime", item.getCreateTime());
row.put("updateBy", item.getUpdateBy());
row.put("updateTime", item.getUpdateTime());
resultList.add(row);
}
return Result.ok(resultList);
}
/**
* 获取字典数据
* @param dictCode

View File

@@ -8,11 +8,11 @@ import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.modules.system.entity.SysLog;
@@ -23,6 +23,8 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecgframework.poi.handler.inter.IExcelExportServerEnhanced;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@@ -32,6 +34,7 @@ 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 lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.ModelAndView;
@@ -51,6 +54,77 @@ public class SysLogController extends JeecgController<SysLog, ISysLogService> {
@Autowired
private ISysLogService sysLogService;
@PostMapping("/scada/addLoginLog")
@Operation(summary = "SCADA-免登录写入登录日志")
public Result<String> scadaAddLoginLog(@RequestBody java.util.Map<String, Object> payload, HttpServletRequest request) {
payload.put("category", "LOGIN");
return scadaAddLog(payload, request);
}
@PostMapping("/scada/addLog")
@Operation(summary = "SCADA-免登录写入通用日志")
public Result<String> scadaAddLog(@RequestBody java.util.Map<String, Object> payload, HttpServletRequest request) {
try {
String account = oConvertUtils.getString(payload.get("account"));
String message = oConvertUtils.getString(payload.get("message"));
String category = oConvertUtils.getString(payload.get("category"));
boolean success = Boolean.parseBoolean(oConvertUtils.getString(payload.get("success")));
String clientType = normalizeClientType(oConvertUtils.getString(payload.get("clientType")));
String exception = oConvertUtils.getString(payload.get("exception"));
int logType = oConvertUtils.getInt(payload.get("logType"), "EXCEPTION".equalsIgnoreCase(category) ? 2 : 1);
int operateType = oConvertUtils.getInt(payload.get("operateType"), "LOGIN".equalsIgnoreCase(category) ? 1 : 5);
String method = oConvertUtils.getString(payload.get("method"));
if (oConvertUtils.isEmpty(method)) {
method = "LOGIN".equalsIgnoreCase(category) ? "LOGIN" : "SCADA_LOG";
}
String requestUrl = oConvertUtils.getString(payload.get("requestUrl"));
if (oConvertUtils.isEmpty(requestUrl)) {
requestUrl = "/sys/log/scada/addLog";
}
SysLog logEntity = new SysLog();
logEntity.setCreateTime(new java.util.Date());
logEntity.setLogType(logType);
logEntity.setOperateType(operateType);
logEntity.setRequestType("WS/HTTP");
logEntity.setRequestUrl(requestUrl);
logEntity.setMethod(method);
logEntity.setUsername(account);
logEntity.setUserid(account);
logEntity.setIp(IpUtils.getIpAddr(request));
logEntity.setClientType(clientType);
logEntity.setLogContent(buildLogContent(category, success, message, exception));
logEntity.setRequestParam(com.alibaba.fastjson.JSON.toJSONString(payload));
sysLogService.save(logEntity);
return Result.OK("ok");
} catch (Exception e) {
log.error("SCADA写入日志失败", e);
return Result.error("写入失败:" + e.getMessage());
}
}
private String buildLogContent(String category, boolean success, String message, String exception) {
if ("LOGIN".equalsIgnoreCase(category)) {
return String.format("桌面端登录%s%s", success ? "成功" : "失败", oConvertUtils.isEmpty(message) ? "" : message);
}
if ("EXCEPTION".equalsIgnoreCase(category)) {
String exceptionMsg = oConvertUtils.isEmpty(exception) ? "" : (" 异常: " + exception);
return String.format("桌面端异常日志:%s%s", oConvertUtils.isEmpty(message) ? "" : message, exceptionMsg);
}
return String.format("桌面端操作日志:%s", oConvertUtils.isEmpty(message) ? "" : message);
}
private String normalizeClientType(String rawClientType) {
if (oConvertUtils.isEmpty(rawClientType)) {
return "gkj";
}
String value = rawClientType.trim().toLowerCase();
if ("desktop".equals(value) || "pc".equals(value) || "windows".equals(value)) {
return "gkj";
}
return value;
}
/**
* for [issues/8699]AutoPoi在使用@ExcelEntity当设置show=true并且该项为null时报错
*/

View File

@@ -9,8 +9,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
@@ -22,6 +25,7 @@ import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.PasswordConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.constant.WebsocketConst;
import org.jeecg.common.modules.redis.client.JeecgRedisClient;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.util.JwtUtil;
@@ -35,11 +39,13 @@ import org.jeecg.modules.system.excelstyle.ExcelExportSysUserStyle;
import org.jeecg.modules.system.model.DepartIdModel;
import org.jeecg.modules.system.model.SysUserSysDepPostModel;
import org.jeecg.modules.system.model.SysUserSysDepartModel;
import org.jeecg.modules.message.websocket.WebSocket;
import org.jeecg.modules.system.service.*;
import org.jeecg.modules.system.util.ImportSysUserCache;
import org.jeecg.modules.system.vo.SysDepartUsersVO;
import org.jeecg.modules.system.vo.SysUserExportVo;
import org.jeecg.modules.system.vo.SysUserRoleVO;
import org.jeecg.modules.system.vo.SysUserTenantVo;
import org.jeecg.modules.system.vo.lowapp.DepartAndUserInfo;
import org.jeecg.modules.system.vo.lowapp.UpdateDepartInfo;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
@@ -48,6 +54,7 @@ import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
@@ -70,6 +77,7 @@ import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/sys/user")
@Tag(name = "用户管理")
public class SysUserController {
@Autowired
@@ -109,6 +117,66 @@ public class SysUserController {
private JeecgRedisClient jeecgRedisClient;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private WebSocket webSocket;
/** 设备同步模块 STOMP与桌面端统一通道 /topic/sync/jeecg-users无 device-sync 模块时为 null */
@Autowired(required = false)
private SimpMessagingTemplate simpMessagingTemplate;
private void notifyScadaUserChanged(String action, String userId) {
try {
JSONObject payload = new JSONObject();
payload.put(WebsocketConst.MSG_CMD, "SCADA_USER_CHANGED");
payload.put("action", action);
payload.put("userId", userId);
payload.put("timestamp", System.currentTimeMillis());
log.info("SCADA用户变更通知开始 action={}, userId={}, onlineWs={}", action, userId, WebSocket.getOnlineCount());
// 固定工控机通道定向推送(桌面端连接 /websocket/scada-sync
webSocket.pushMessage("scada-sync", payload.toJSONString());
// 单机直推避免仅依赖Redis链路导致消息偶发丢失
webSocket.pushMessage(payload.toJSONString());
// 集群转发通过Redis发布订阅推送到其它节点在线客户端
webSocket.sendMessage(payload.toJSONString());
pushUserChangeToStompTopic(payload.toJSONString());
log.info("SCADA用户变更通知完成 action={}, userId={}, payload={}", action, userId, payload.toJSONString());
} catch (Exception e) {
log.warn("推送SCADA用户变更消息失败 action={}, userId={}", action, userId, e);
}
}
private void pushUserChangeToStompTopic(String jsonPayload) {
if (simpMessagingTemplate == null) {
return;
}
try {
simpMessagingTemplate.convertAndSend("/topic/sync/jeecg-users", jsonPayload);
} catch (Exception e) {
log.debug("STOMP 广播用户变更跳过device-sync 未启用或 Broker 未就绪): {}", e.getMessage());
}
}
private void notifyScadaUsersChanged(String action, String ids) {
try {
JSONObject payload = new JSONObject();
payload.put(WebsocketConst.MSG_CMD, "SCADA_USER_CHANGED");
payload.put("action", action);
payload.put("userIds", ids);
payload.put("timestamp", System.currentTimeMillis());
log.info("SCADA批量用户变更通知开始 action={}, ids={}, onlineWs={}", action, ids, WebSocket.getOnlineCount());
// 固定工控机通道定向推送(桌面端连接 /websocket/scada-sync
webSocket.pushMessage("scada-sync", payload.toJSONString());
// 单机直推避免仅依赖Redis链路导致消息偶发丢失
webSocket.pushMessage(payload.toJSONString());
// 集群转发通过Redis发布订阅推送到其它节点在线客户端
webSocket.sendMessage(payload.toJSONString());
pushUserChangeToStompTopic(payload.toJSONString());
log.info("SCADA批量用户变更通知完成 action={}, ids={}, payload={}", action, ids, payload.toJSONString());
} catch (Exception e) {
log.warn("推送SCADA批量用户变更消息失败 action={}, ids={}", action, ids, e);
}
}
/**
* 获取租户下用户数据(支持租户隔离)
@@ -119,6 +187,7 @@ public class SysUserController {
* @return
*/
@PermissionData(pageComponent = "system/UserList")
@Operation(summary = "用户管理-分页查询")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Result<IPage<SysUser>> queryPageList(SysUser user,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) {
@@ -156,6 +225,7 @@ public class SysUserController {
}
@RequiresPermissions("system:user:add")
@Operation(summary = "用户管理-新增用户")
@RequestMapping(value = "/add", method = RequestMethod.POST)
public Result<SysUser> add(@RequestBody JSONObject jsonObject) {
Result<SysUser> result = new Result<SysUser>();
@@ -178,6 +248,7 @@ public class SysUserController {
String relTenantIds = jsonObject.getString("relTenantIds");
sysUserService.saveUser(user, selectedRoles, selectedDeparts, relTenantIds, false);
baseCommonService.addLog("添加用户username " +user.getUsername() ,CommonConstant.LOG_TYPE_2, 2);
notifyScadaUserChanged("add", user.getId());
result.success("添加成功!");
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -187,6 +258,7 @@ public class SysUserController {
}
@RequiresPermissions("system:user:edit")
@Operation(summary = "用户管理-编辑用户")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<SysUser> edit(@RequestBody JSONObject jsonObject) {
Result<SysUser> result = new Result<SysUser>();
@@ -215,6 +287,7 @@ public class SysUserController {
//update-begin---author:wangshuai---date:2025-11-12---for:【JHHB-776】用户编辑应该从数据库查出老数据页面传递什么字段把这些字段覆盖数据库查询结果再更新---
oConvertUtils.copyNonNullFields(user, sysUser);
sysUserService.editUser(sysUser, roles, departs, relTenantIds, updateFromPage);
notifyScadaUserChanged("edit", sysUser.getId());
//update-end---author:wangshuai---date:2025-11-12---for:【JHHB-776】用户编辑应该从数据库查出老数据页面传递什么字段把这些字段覆盖数据库查询结果再更新---
result.success("修改成功!");
}
@@ -253,6 +326,7 @@ public class SysUserController {
String relTenantIds = jsonObject.getString("relTenantIds");
sysUserService.saveUser(user, selectedRoles, selectedDeparts, relTenantIds, true);
baseCommonService.addLog("添加用户username " + user.getUsername(), CommonConstant.LOG_TYPE_2, 2);
notifyScadaUserChanged("addTenantUser", user.getId());
result.success("添加成功!");
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -265,11 +339,13 @@ public class SysUserController {
* 删除用户
*/
@RequiresPermissions("system:user:delete")
@Operation(summary = "用户管理-删除用户")
@RequestMapping(value = "/delete", method = RequestMethod.DELETE)
public Result<?> delete(@RequestParam(name="id",required=true) String id) {
baseCommonService.addLog("删除用户id " +id ,CommonConstant.LOG_TYPE_2, 3);
List<String> userNameList = sysUserService.userIdToUsername(Arrays.asList(id));
this.sysUserService.deleteUser(id);
notifyScadaUserChanged("delete", id);
if (!userNameList.isEmpty()) {
String joinedString = String.join(",", userNameList);
@@ -281,11 +357,13 @@ public class SysUserController {
* 批量删除用户
*/
@RequiresPermissions("system:user:deleteBatch")
@Operation(summary = "用户管理-批量删除用户")
@RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE)
public Result<?> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
baseCommonService.addLog("批量删除用户, ids " +ids ,CommonConstant.LOG_TYPE_2, 3);
List<String> userNameList = sysUserService.userIdToUsername(Arrays.asList(ids.split(",")));
this.sysUserService.deleteBatchUsers(ids);
notifyScadaUsersChanged("deleteBatch", ids);
// 用户变更,触发同步工作流
if (!userNameList.isEmpty()) {
@@ -312,6 +390,7 @@ public class SysUserController {
if(oConvertUtils.isNotEmpty(id)) {
// 代码逻辑说明: [QQYUN-5577]用户列表-冻结用户,再解冻之后,用户还是无法登陆,有缓存问题 #5066------------
sysUserService.updateStatus(id,status);
notifyScadaUserChanged("status", id);
}
}
} catch (Exception e) {
@@ -329,6 +408,7 @@ public class SysUserController {
*/
@RequiresRoles({"admin"})
@RequiresPermissions("system:user:resetPassword")
@Operation(summary = "用户管理-重置密码")
@RequestMapping(value = "/resetPassword", method = RequestMethod.PUT)
public Result<SysUser> resetPassword(@RequestParam(name = "usernames") String usernames) {
Result<SysUser> result = new Result<SysUser>();
@@ -344,6 +424,7 @@ public class SysUserController {
}
@RequiresPermissions("system:user:queryById")
@Operation(summary = "用户管理-根据ID查询")
@RequestMapping(value = "/queryById", method = RequestMethod.GET)
public Result<SysUser> queryById(@RequestParam(name = "id", required = true) String id) {
Result<SysUser> result = new Result<SysUser>();
@@ -384,6 +465,7 @@ public class SysUserController {
* @return
*/
@RequestMapping(value = "/checkOnlyUser", method = RequestMethod.GET)
@Operation(summary = "用户管理-校验账号唯一")
public Result<Boolean> checkOnlyUser(SysUser sysUser) {
Result<Boolean> result = new Result<>();
//如果此参数为false则程序发生异常
@@ -411,6 +493,7 @@ public class SysUserController {
* 修改密码
*/
@RequiresPermissions("system:user:changepwd")
@Operation(summary = "用户管理-修改密码")
@RequestMapping(value = "/changePassword", method = RequestMethod.PUT)
public Result<?> changePassword(@RequestBody SysUser sysUser, HttpServletRequest request) {
//-------------------------------------------------------------------------------------
@@ -432,6 +515,107 @@ public class SysUserController {
return sysUserService.changePassword(sysUser);
}
/**
* SCADA 系统专用:免登录查询用户信息。
* 支持通过 username / workNo / phone 任一条件查询,返回基础信息(不返回敏感字段)。
*/
@GetMapping("/scada/queryUser")
@Operation(summary = "SCADA-免登录查询用户信息")
public Result<List<Map<String, Object>>> scadaQueryUser(@RequestParam(name = "username", required = false) String username,
@RequestParam(name = "workNo", required = false) String workNo,
@RequestParam(name = "phone", required = false) String phone,
@RequestParam(name = "updatedAfter", required = false) String updatedAfter,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "200") Integer pageSize,
@RequestParam(name = "includeDetail", defaultValue = "false") Boolean includeDetail) {
if (pageNo == null || pageNo < 1) {
pageNo = 1;
}
if (pageSize == null || pageSize < 1) {
pageSize = 200;
}
// 控制单次最大拉取量,避免接口被大查询拖慢
pageSize = Math.min(pageSize, 1000);
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getDelFlag, CommonConstant.DEL_FLAG_0);
queryWrapper.eq(SysUser::getStatus, Integer.valueOf(CommonConstant.STATUS_1));
queryWrapper.eq(oConvertUtils.isNotEmpty(username), SysUser::getUsername, username);
queryWrapper.eq(oConvertUtils.isNotEmpty(workNo), SysUser::getWorkNo, workNo);
queryWrapper.eq(oConvertUtils.isNotEmpty(phone), SysUser::getPhone, phone);
if (oConvertUtils.isNotEmpty(updatedAfter)) {
try {
Date syncTime = DateUtils.parseDate(updatedAfter, new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss"});
queryWrapper.and(w -> w.ge(SysUser::getUpdateTime, syncTime).or().ge(SysUser::getCreateTime, syncTime));
} catch (Exception e) {
return Result.error("updatedAfter格式错误支持yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd'T'HH:mm:ss");
}
}
queryWrapper.orderByAsc(SysUser::getSort);
queryWrapper.orderByDesc(SysUser::getCreateTime);
IPage<SysUser> userPage = sysUserService.page(new Page<>(pageNo, pageSize), queryWrapper);
List<SysUser> userList = userPage.getRecords();
List<Map<String, Object>> resultList = new ArrayList<>();
for (SysUser user : userList) {
List<Map<String, Object>> departList = new ArrayList<>();
List<Map<String, Object>> companyList = new ArrayList<>();
List<Map<String, Object>> tenantList = new ArrayList<>();
if (Boolean.TRUE.equals(includeDetail)) {
List<SysDepart> userDeparts = sysDepartService.queryUserDeparts(user.getId());
for (SysDepart depart : userDeparts) {
Map<String, Object> departInfo = new LinkedHashMap<>(8);
departInfo.put("id", depart.getId());
departInfo.put("departName", depart.getDepartName());
departInfo.put("orgCode", depart.getOrgCode());
departInfo.put("orgCategory", depart.getOrgCategory());
departList.add(departInfo);
// 机构类别1公司、4子公司
if ("1".equals(depart.getOrgCategory()) || "4".equals(depart.getOrgCategory())) {
companyList.add(departInfo);
}
}
List<SysUserTenantVo> userTenants = userTenantService.getTenantListByUserId(user.getId(), Collections.singletonList(CommonConstant.STATUS_1));
for (SysUserTenantVo tenantVo : userTenants) {
Map<String, Object> tenantInfo = new LinkedHashMap<>(8);
tenantInfo.put("tenantId", tenantVo.getRelTenantIds());
tenantInfo.put("tenantName", tenantVo.getName());
tenantInfo.put("tenantUserStatus", tenantVo.getUserTenantStatus());
tenantInfo.put("tenantUserId", tenantVo.getTenantUserId());
tenantInfo.put("tenantAdmin", tenantVo.getTenantAdmin());
tenantList.add(tenantInfo);
}
}
Map<String, Object> userInfo = new LinkedHashMap<>(16);
userInfo.put("id", user.getId());
userInfo.put("username", user.getUsername());
userInfo.put("realname", user.getRealname());
// SysUser无独立nickname字段SCADA同步端约定用realname兼容昵称展示
userInfo.put("nickname", user.getRealname());
userInfo.put("password", user.getPassword());
userInfo.put("salt", user.getSalt());
userInfo.put("workNo", user.getWorkNo());
userInfo.put("phone", user.getPhone());
userInfo.put("email", user.getEmail());
userInfo.put("orgCode", user.getOrgCode());
userInfo.put("post", user.getPost());
userInfo.put("status", user.getStatus());
userInfo.put("createTime", user.getCreateTime());
userInfo.put("updateTime", user.getUpdateTime());
userInfo.put("lastPwdUpdateTime", user.getLastPwdUpdateTime());
userInfo.put("departIds", user.getDepartIds());
userInfo.put("departmentList", departList);
userInfo.put("companyList", companyList);
userInfo.put("loginTenantId", user.getLoginTenantId());
userInfo.put("tenantList", tenantList);
resultList.add(userInfo);
}
return Result.ok(resultList);
}
/**
* 查询指定用户和部门关联的数据
*

View File

@@ -36,6 +36,12 @@
<artifactId>jeecg-module-xslmes</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- 设备断联续传同步模块 -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-module-device-sync</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- flyway 数据库自动升级 -->
<dependency>

View File

@@ -22,7 +22,10 @@ import java.util.Map;
* 报错提醒: 未集成mongo报错可以打开启动类上面的注释 exclude={MongoAutoConfiguration.class}
*/
@Slf4j
@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@SpringBootApplication(
exclude = MongoAutoConfiguration.class,
scanBasePackages = {"org.jeecg"}
)
@ImportAutoConfiguration(JustAuthAutoConfiguration.class) // spring boot 3.x justauth 兼容性处理
public class JeecgSystemApplication extends SpringBootServletInitializer {
@@ -32,6 +35,7 @@ public class JeecgSystemApplication extends SpringBootServletInitializer {
}
public static void main(String[] args) throws UnknownHostException {
SpringApplication app = new SpringApplication(JeecgSystemApplication.class);
Map<String, Object> defaultProperties = new HashMap<>();
defaultProperties.put("management.health.elasticsearch.enabled", false);

View File

@@ -233,7 +233,7 @@ jeecg:
#webapp文件路径
webapp: /opt/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
# 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
smsSendType: aliyun
#阿里云oss存储和大鱼短信秘钥配置

View File

@@ -199,7 +199,7 @@ jeecg:
#webapp文件路径
webapp: /opt/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
#阿里云oss存储和大鱼短信秘钥配置
oss:
accessKey: ??

View File

@@ -222,7 +222,7 @@ jeecg:
#webapp文件路径
webapp: /opt/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
#阿里云oss存储和大鱼短信秘钥配置
oss:
accessKey: ??

View File

@@ -219,7 +219,7 @@ jeecg:
#webapp文件路径
webapp: /opt/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
#阿里云oss存储和大鱼短信秘钥配置
oss:
accessKey: ??

View File

@@ -213,7 +213,7 @@ jeecg:
#webapp文件路径
webapp: /opt/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
#阿里云oss存储和大鱼短信秘钥配置
oss:
accessKey: ??

View File

@@ -226,7 +226,7 @@ jeecg:
#webapp文件路径
webapp: /opt/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
#阿里云oss存储和大鱼短信秘钥配置
oss:
accessKey: ??

View File

@@ -226,7 +226,7 @@ jeecg:
#webapp文件路径
webapp: /opt/jeecg-boot/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/api/getUserInfo
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/api/getUserInfo,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
# 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
smsSendType: aliyun
#阿里云oss存储和大鱼短信秘钥配置

View File

@@ -208,7 +208,7 @@ jeecg:
#webapp文件路径
webapp: /opt/webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
#阿里云oss存储和大鱼短信秘钥配置
oss:
accessKey: ??

View File

@@ -227,7 +227,7 @@ jeecg:
#webapp文件路径
webapp: D://opt//webapp
shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
# 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
smsSendType: aliyun
#阿里云oss存储和大鱼短信秘钥配置