commit 8de2fdc8a4c5c31ab14153966a02afd9663ed33a Author: hongawen <83944980@qq.com> Date: Mon Apr 13 11:50:14 2026 +0800 项目初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6cb740 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Compiled class file +*.class +*.iml +*.idea +target/ +logs/ +docs/ + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*velocity.log* + +# Eclipse # +.classpath +.project +.settings/ + +.DS_Store + +_dockerCerts/ + +.factorypath + +node_modules/ +package-lock.json +yarn.lock + +rebel.xml + +!DmJdbcDriver18.jar +!kingbase8-8.6.0.jar +/.fastRequest/collections/Root/Default Group/directory.json +/.fastRequest/collections/Root/directory.json +/.fastRequest/config/fastRequestCurrentProjectConfig.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7fda346 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Repository Guidelines + +## Agent 工作方式 +进入本仓库后,先阅读本文件,再开始分析、修改或输出结论。不要跳过现有文档直接下判断,至少先结合根目录 `README.md`、`docs/`、目标模块代码和相关配置确认上下文。 + +日常交互遵循以下习惯: + +- 先整理执行方案,说明目标、涉及模块、预计修改点和验证方式,待用户评审确认后再执行。 +- 不要想当然;如果需求存在歧义、前提不清或有多种实现路径,先说清假设与取舍,再继续。 +- 先确认任务位于 `entrance`、`system`、`user`、`detection` 还是 `tools/activate-tool`,再沿配置、Controller、Service、Mapper、XML 和调用链向下分析。 +- 涉及认证、字典、日志、注册资源、WebSocket 或 Netty 通信链路时,先核对已有实现和 `docs/` 中的说明,避免只看局部代码就下结论。 +- 回复风格保持简洁、直接,优先给出可执行结果;如果存在限制、风险或未验证部分,需要明确说明。 + +## 执行与修改原则 +- 简单优先:只做当前需求所需的最小改动,不额外引入新功能、抽象层、配置项或“顺手优化”。 +- 外科手术式修改:只改与任务直接相关的文件和代码行,不重构无关模块,不调整无关格式或注释。 +- 保持现有风格:遵循仓库已有包结构、分层方式、命名和写法,不按个人偏好重写。 +- 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。 +- 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性和受影响范围检查进行验证。 +- 除非用户明确要求,否则不执行任何 `mvn` 编译、打包、测试或其他构建命令。 + +## 项目结构与模块划分 +`CN_Tool` 是一个 Maven 多模块后端项目,根目录的 [`pom.xml`](C:/code/gitea/cn_tool/CN_Tool/pom.xml) 聚合了 `entrance`、`system`、`user`、`detection` 和 `tools`。 + +- `entrance`:Spring Boot 启动模块,入口类为 `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`。 +- `system`:系统字典、日志、配置、注册资源等公共能力。 +- `user`:认证、用户、角色、功能资源及相关过滤逻辑。 +- `detection`:Netty / WebSocket 通信与连接生命周期管理。 +- `tools/activate-tool`:激活码与许可能力。 +- `docs/`:项目基线、配置和运行说明文档。 + +Java 源码位于 `src/main/java`,配置文件位于 `src/main/resources`,MyBatis XML 映射文件按包结构存放在 `**/mapper/mapping/*.xml`。 + +## 代码风格与命名规范 +保持现有 Java 风格:4 空格缩进、UTF-8 文件编码、基础包名使用 `com.njcn.gather`。命名沿用分层后缀,如 `*Controller`、`*Service`、`*ServiceImpl`、`*Mapper`、`*Param`、`*PO`、`*VO`。优先复用现有 Lombok 注解,如 `@Data`、`@RequiredArgsConstructor`、`@Slf4j`。Mapper XML 文件名应与接口名保持一致。业务代码中,关键流程、分支判断、状态流转或容易误解的节点需要补充简洁的中文注释,但不要添加无信息量的注释。 + +## 提交与合并请求规范 +当前 `main` 分支尚无可参考的提交历史,仓库内也没有既有提交规范。建议使用“模块前缀 + 动词短句”的提交格式,例如 `user: 优化登录会话校验`、`system: 增加字典参数校验`。提交 PR 时应说明影响模块、配置或数据结构变更、人工验证步骤;若接口行为有变化,附上请求与响应示例。 + +## 安全与配置提示 +将 `application.yml` 视为环境配置文件处理,不要在提交中新增明文密钥、数据库口令或许可证材料。本地运行时需保证 `D:\logs` 可写;如部署环境不同,应通过配置覆盖日志目录。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..afbe981 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +用于减少 LLM 常见编码失误的行为准则。可按需与项目特定说明合并使用。 + +**权衡:** 这些准则更偏向谨慎而非速度。对于非常简单的任务,请自行判断。 + +## 1. 编码前先思考 + +**不要想当然。不要掩饰困惑。把权衡点说出来。** + +开始实现前: +- 明确说明你的假设;如果不确定,就提问。 +- 如果存在多种理解方式,请列出来,不要默默自行选择。 +- 如果有更简单的方案,请直接指出;必要时应当提出不同意见。 +- 如果有内容不清楚,就先停下来;说清楚困惑点,然后提问。 + +## 2. 简单优先 + +**只写解决问题所需的最少代码。不要做猜测式扩展。** + +- 不要添加用户未要求的功能。 +- 不要为一次性代码引入抽象。 +- 不要加入未被要求的“灵活性”或“可配置性”。 +- 不要为不可能发生的场景补充错误处理。 +- 如果你写了 200 行,而实际上 50 行就能解决,就重写。 + +问问自己:“一个资深工程师会认为这太复杂了吗?”如果答案是会,那就继续简化。 + +## 3. 外科手术式修改 + +**只改必须改的地方。只清理自己造成的问题。** + +修改现有代码时: +- 不要顺手“优化”相邻代码、注释或格式。 +- 不要重构没有问题的部分。 +- 保持与现有风格一致,即使你个人会写成别的样子。 +- 如果发现无关的死代码,可以指出,但不要直接删除。 + +当你的改动产生“遗留物”时: +- 删除因你的改动而变成未使用的 `import`、变量或函数。 +- 不要删除原本就存在的死代码,除非用户明确要求。 + +检验标准:每一行变更都应该能直接对应到用户的请求。 + +## 4. 以目标驱动执行 + +**先定义成功标准,再循环推进直到验证完成。** + +把任务转换成可验证的目标: +- “增加校验” → “先为非法输入编写测试,再让测试通过” +- “修复 bug” → “先写出能复现问题的测试,再让测试通过” +- “重构 X” → “确保改动前后测试都通过” + +对于多步骤任务,先给出简短计划: +``` +1. [步骤] → 验证方式:[检查项] +2. [步骤] → 验证方式:[检查项] +3. [步骤] → 验证方式:[检查项] +``` + +明确的成功标准可以支持你独立推进;模糊的标准(例如“把它弄好”)则会导致反复确认。 + +--- + +**如果这些准则有效,你会看到:** diff 中不必要的改动更少,因过度设计导致的返工更少,而且澄清问题会发生在实现之前,而不是出错之后。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a01a9bf --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# CN_Tool + +CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓库内保留的核心能力包括: + +- 用户认证、用户/角色/菜单资源管理 +- 系统字典、日志、系统配置、注册资源管理 +- WebSocket / Netty 通信基础设施 +- 激活码与许可证能力 + +## 当前真实模块 + +根聚合模块下当前包含以下子模块: + +- `entrance` +- `system` +- `user` +- `detection` +- `tools` + +其中 `tools` 当前仅保留: + +- `activate-tool` + +## 启动入口 + +当前主启动入口位于: + +- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java` + +`entrance` 模块聚合了 `system`、`user`、`detection`、`activate-tool`,是当前运行时主入口。 + +## 技术基线 + +- Java:源码目标版本为 `1.8` +- Spring Boot:`2.3.12.RELEASE` +- 构建方式:Maven 多模块工程 +- ORM:MyBatis-Plus +- 数据库:MySQL + +## 运行与构建前提 + +当前项目存在以下前提条件: + +- 需要可用的 JDK 8 环境 +- 需要 Maven 环境 +- 当前仓库未发现 `mvnw` +- 依赖私有 `com.njcn` 组件 +- 根 `pom.xml` 中存在内网 Nexus 发布仓库配置 +- 运行前通常需要可访问的 MySQL 数据库和基础表数据 + +说明: + +当前这份仓库并不保证在任意外部环境下可直接编译运行。 +如果要做真实构建和启动,需要先满足内部依赖和环境条件。 + +## 文档入口 + +P0 已补齐基线文档,建议按以下顺序阅读: + +1. [docs/01-项目总览.md](./docs/01-项目总览.md) +2. [docs/02-配置清单.md](./docs/02-配置清单.md) +3. [docs/03-构建与运行前提.md](./docs/03-构建与运行前提.md) +4. [docs/04-过时文档说明.md](./docs/04-过时文档说明.md) + +## 模块说明 + +- `user` + - 负责认证、用户、角色、菜单资源相关能力 +- `system` + - 负责字典、日志、系统配置、注册资源相关能力 +- `detection` + - 当前以通信基础设施为主,包含 WebSocket / Netty 相关组件 +- `tools/activate-tool` + - 负责激活码生成、激活码验证、许可证读取等能力 + +## 文档使用规则 + +当前仓库中部分历史说明仍然存在。 +如文档之间出现冲突,建议按以下优先级理解: + +1. `docs/` 下的基线文档 +2. 根 `README.md` +3. 各模块下的 `Readme.md` +4. 最终以源码和配置为准 diff --git a/detection/Readme.md b/detection/Readme.md new file mode 100644 index 0000000..1334595 --- /dev/null +++ b/detection/Readme.md @@ -0,0 +1,9 @@ +#### 简介 + 设备模块主要包含以下功能: +* 检测计划管理 +* 被检设备管理 +* 被检设备下监测点管理 +* 误差体系管理 +* 检测脚本管理 +* 检测源管理 +* 检测报告管理 diff --git a/detection/pom.xml b/detection/pom.xml new file mode 100644 index 0000000..d91fb47 --- /dev/null +++ b/detection/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + com.njcn.gather + CN_Tool + 1.0.0 + + detection + + + + com.njcn + njcn-common + 0.0.1 + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + + com.alibaba + fastjson + 1.2.83 + + + io.netty + netty-all + 4.1.68.Final + + + + diff --git a/detection/src/main/java/com/njcn/gather/detection/pojo/vo/SocketDataMsg.java b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/SocketDataMsg.java new file mode 100644 index 0000000..5dab158 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/SocketDataMsg.java @@ -0,0 +1,43 @@ +package com.njcn.gather.detection.pojo.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +/** + * @author wr + * @description + * @date 2024/12/13 9:09 + */ +@Data +public class SocketDataMsg { + + /** + * 标识不同业务 + */ + private String type = "aaa"; + + /** + * 请求id,确保接收到响应时,知晓是针对的哪次请求的应答 + */ + @JSONField(ordinal = 1) + private String requestId; + + /** + * 源初始化 INIT_GATHER$01 INIT_GATHER采集初始化,01 统计采集、02 暂态采集、03 实时采集 + */ + @JSONField(ordinal = 2) + private String operateCode; + + /** + * 数据体,传输前需要将对象、Array等转为String + */ + @JSONField(ordinal = 4) + private String data; + + /** + * code码 + */ + @JSONField(ordinal = 3) + private Integer code; + +} diff --git a/detection/src/main/java/com/njcn/gather/detection/pojo/vo/SocketMsg.java b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/SocketMsg.java new file mode 100644 index 0000000..b813e04 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/SocketMsg.java @@ -0,0 +1,31 @@ +package com.njcn.gather.detection.pojo.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +/** + * @author wr + * @description socket 通用发送报文请求 + * @date 2024/12/11 15:57 + */ +@Data +public class SocketMsg { + + /** + * 请求id,确保接收到响应时,知晓是针对的哪次请求的应答 + */ + @JSONField(ordinal = 1) + private String requestId; + + /** + * 源初始化 INIT_GATHER$01 INIT_GATHER采集初始化,01 统计采集、02 暂态采集、03 实时采集 + */ + @JSONField(ordinal = 2) + private String operateCode; + + /** + * 数据体,传输前需要将对象、Array等转为String + */ + @JSONField(ordinal = 3) + private T data; +} diff --git a/detection/src/main/java/com/njcn/gather/detection/pojo/vo/WebSocketVO.java b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/WebSocketVO.java new file mode 100644 index 0000000..7b84011 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/WebSocketVO.java @@ -0,0 +1,30 @@ +package com.njcn.gather.detection.pojo.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Generic WebSocket payload wrapper. + * + * @author chendaofei + * @author hongawen + * @date 2026/04/08 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class WebSocketVO { + + private String type = "transport"; + + private String requestId; + + private String operateCode; + + private Integer code; + + private String desc; + + private T data; +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/MsgUtil.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/MsgUtil.java new file mode 100644 index 0000000..ba1936b --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/MsgUtil.java @@ -0,0 +1,32 @@ +package com.njcn.gather.detection.util.socket; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.njcn.gather.detection.pojo.vo.SocketDataMsg; + +/** + * Generic socket message helper retained by the communication foundation. + * Stage 4-B removes detection-specific text assembly helpers and keeps only + * payload parsing and JSON framing methods used by the base transport layer. + * + * @author wr + * @author hongawen + * @date 2026/04/08 + */ +public final class MsgUtil { + + private MsgUtil() { + } + + public static SocketDataMsg socketDataMsg(String textMsg) { + return JSON.parseObject(textMsg, SocketDataMsg.class); + } + + public static String toJsonWithNewLine(Object obj) { + return JSON.toJSONString(obj, SerializerFeature.PrettyFormat) + "\n"; + } + + public static String toJsonWithNewLinePlain(Object obj) { + return JSON.toJSONString(obj) + "\n"; + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/SocketManager.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/SocketManager.java new file mode 100644 index 0000000..dfacf9d --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/SocketManager.java @@ -0,0 +1,170 @@ +package com.njcn.gather.detection.util.socket; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.njcn.gather.detection.util.socket.cilent.NettyClient; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionType; +import com.njcn.gather.detection.util.socket.config.SocketConnectionConfig; +import io.netty.channel.Channel; +import io.netty.channel.nio.NioEventLoopGroup; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Generic socket session manager. + * Stage 4-A removes detection-only caches from this class and keeps only the + * retained transport responsibilities: session registry, auto-connect and + * outbound dispatch. + * + * @author wr + * @author hongawen + * @date 2026/04/07 + */ +@Slf4j +@Component +public class SocketManager { + + @Resource + private SocketConnectionConfig socketConnectionConfig; + + @Resource + private NettyClient nettyClient; + + /** + * Key: sessionKey(userId + connection tag), value: active channel. + */ + private static final Map SOCKET_SESSIONS = new ConcurrentHashMap<>(); + + /** + * Key: sessionKey(userId + connection tag), value: event loop group. + */ + private static final Map SOCKET_GROUPS = new ConcurrentHashMap<>(); + + public static void addUser(String sessionKey, Channel channel) { + SOCKET_SESSIONS.put(sessionKey, channel); + } + + public static void addGroup(String sessionKey, NioEventLoopGroup group) { + SOCKET_GROUPS.put(sessionKey, group); + } + + public static void removeUser(String sessionKey) { + Channel channel = SOCKET_SESSIONS.remove(sessionKey); + NioEventLoopGroup eventLoopGroup = SOCKET_GROUPS.remove(sessionKey); + + if (ObjectUtil.isNotNull(channel)) { + try { + channel.close().sync(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Close socket channel interrupted: sessionKey={}", sessionKey, e); + } + } + + if (ObjectUtil.isNotNull(eventLoopGroup)) { + eventLoopGroup.shutdownGracefully(); + log.info("Socket connection closed: sessionKey={}", sessionKey); + } + } + + public static Channel getChannelByUserId(String sessionKey) { + return SOCKET_SESSIONS.get(sessionKey); + } + + public static NioEventLoopGroup getGroupByUserId(String sessionKey) { + return SOCKET_GROUPS.get(sessionKey); + } + + public static boolean isChannelActive(String sessionKey) { + Channel channel = getChannelByUserId(sessionKey); + return ObjectUtil.isNotNull(channel) && channel.isActive(); + } + + public static void sendMsg(String sessionKey, String msg) { + Channel channel = SOCKET_SESSIONS.get(sessionKey); + if (ObjectUtil.isNull(channel)) { + log.warn("Send socket message failed because channel does not exist: sessionKey={}, message={}", + sessionKey, msg); + return; + } + channel.writeAndFlush(msg + '\n'); + log.info("{}__{} -> {} : {}", sessionKey, channel.id(), channel.remoteAddress(), msg); + } + + /** + * Key refactor point: auto-connect now depends only on connection context + * and transport callbacks attached to that context. + */ + public void smartSend(ConnectionContext context, String msg) { + if (ObjectUtil.isNull(context) || ObjectUtil.isNull(context.getConnectionType())) { + log.warn("smartSend skipped because connection context is null"); + return; + } + String sessionKey = context.getSessionKey(); + String requestId = extractRequestId(msg); + if (StrUtil.isBlank(sessionKey)) { + log.warn("smartSend skipped because sessionKey is blank, requestId={}", requestId); + return; + } + + if (needsAutoConnect(context.getConnectionType(), requestId) && !isChannelActive(sessionKey)) { + String ip = resolveIp(context.getConnectionType()); + Integer port = resolvePort(context.getConnectionType()); + log.info("Socket auto connect triggered: type={}, sessionKey={}, requestId={}", + context.getConnectionType(), sessionKey, requestId); + CompletableFuture.runAsync(() -> nettyClient.connect(ip, port, context, msg)); + return; + } + + sendMsg(sessionKey, msg); + } + + private static String extractRequestId(String msg) { + try { + if (StrUtil.isBlank(msg)) { + return "unknown"; + } + JSONObject jsonObject = JSON.parseObject(msg); + String requestId = jsonObject.getString("requestId"); + if (StrUtil.isNotBlank(requestId)) { + return requestId; + } + requestId = jsonObject.getString("request_id"); + if (StrUtil.isNotBlank(requestId)) { + return requestId; + } + } catch (Exception e) { + log.debug("Extract requestId from socket message failed: {}", msg, e); + } + return "unknown"; + } + + private boolean needsAutoConnect(ConnectionType connectionType, String requestId) { + if (connectionType == ConnectionType.SOURCE) { + return SocketConnectionConfig.needsSourceConnection(requestId); + } + return SocketConnectionConfig.needsDeviceConnection(requestId); + } + + private String resolveIp(ConnectionType connectionType) { + if (connectionType == ConnectionType.SOURCE) { + return socketConnectionConfig.getSource().getIp(); + } + return socketConnectionConfig.getDevice().getIp(); + } + + private Integer resolvePort(ConnectionType connectionType) { + if (connectionType == ConnectionType.SOURCE) { + return socketConnectionConfig.getSource().getPort(); + } + return socketConnectionConfig.getDevice().getPort(); + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/AbstractNettyClientHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/AbstractNettyClientHandler.java new file mode 100644 index 0000000..b0ee3dd --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/AbstractNettyClientHandler.java @@ -0,0 +1,103 @@ +package com.njcn.gather.detection.util.socket.cilent; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.detection.util.socket.SocketManager; +import com.njcn.gather.detection.util.socket.communication.handler.ConnectionLifecycleHandler; +import com.njcn.gather.detection.util.socket.communication.handler.SocketMessageHandler; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import lombok.extern.slf4j.Slf4j; + +/** + * Shared client handler skeleton for the retained Netty communication layer. + * Stage 4-A centralizes common session registration, message delegation and + * idle cleanup so concrete handlers stay transport-oriented. + * + * @author hongawen + * @date 2026/04/07 + */ +@Slf4j +public abstract class AbstractNettyClientHandler extends SimpleChannelInboundHandler { + + private final String handlerName; + + protected final ConnectionContext connectionContext; + + private final SocketMessageHandler socketMessageHandler; + + private final ConnectionLifecycleHandler lifecycleHandler; + + protected AbstractNettyClientHandler(String handlerName, ConnectionContext connectionContext, + SocketMessageHandler socketMessageHandler, + ConnectionLifecycleHandler lifecycleHandler) { + this.handlerName = handlerName; + this.connectionContext = connectionContext; + this.socketMessageHandler = socketMessageHandler; + this.lifecycleHandler = lifecycleHandler == null ? ConnectionLifecycleHandler.NO_OP : lifecycleHandler; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + String sessionKey = resolveSessionKey(); + log.info("{} channel active: channelId={}, sessionKey={}", handlerName, ctx.channel().id(), sessionKey); + if (StrUtil.isNotBlank(sessionKey)) { + SocketManager.addUser(sessionKey, ctx.channel()); + } else { + log.warn("{} channel active without sessionKey, skip registration", handlerName); + } + lifecycleHandler.onConnected(connectionContext); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + String sessionKey = resolveSessionKey(); + log.warn("{} channel inactive: channelId={}, sessionKey={}", handlerName, ctx.channel().id(), sessionKey); + if (StrUtil.isNotBlank(sessionKey)) { + SocketManager.removeUser(sessionKey); + } + lifecycleHandler.onDisconnected(connectionContext); + super.channelInactive(ctx); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { + if (socketMessageHandler == null) { + log.warn("{} receive message but handler is null: sessionKey={}, message={}", + handlerName, resolveSessionKey(), msg); + return; + } + try { + socketMessageHandler.handle(connectionContext, msg); + } catch (Exception e) { + lifecycleHandler.onMessageHandlingError(connectionContext, msg, e); + throw e; + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent && ((IdleStateEvent) evt).state() == IdleState.READER_IDLE) { + log.warn("{} trigger reader idle timeout: sessionKey={}", handlerName, resolveSessionKey()); + lifecycleHandler.onIdleTimeout(connectionContext); + ctx.close(); + return; + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("{} catch transport exception: sessionKey={}, message={}", + handlerName, resolveSessionKey(), cause.getMessage(), cause); + lifecycleHandler.onException(connectionContext, cause); + ctx.close(); + } + + protected String resolveSessionKey() { + return connectionContext == null ? null : connectionContext.getSessionKey(); + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/HeartbeatHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/HeartbeatHandler.java new file mode 100644 index 0000000..bad7c13 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/HeartbeatHandler.java @@ -0,0 +1,138 @@ +package com.njcn.gather.detection.util.socket.cilent; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.detection.util.socket.SocketManager; +import com.njcn.gather.detection.util.socket.communication.handler.ConnectionLifecycleHandler; +import com.njcn.gather.detection.util.socket.communication.handler.HeartbeatMessageStrategy; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Generic Netty heartbeat handler. + * Stage 4-A moves heartbeat framing behind a strategy interface so the + * retained transport layer can keep heartbeat capability without embedding + * detection-specific packet structures. + * + * @author cdf + * @author hongawen + * @date 2026/04/07 + */ +@Slf4j +public class HeartbeatHandler extends SimpleChannelInboundHandler { + + private static final int MAX_HEARTBEAT_MISSES = 3; + + private final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(1); + + private final ConnectionContext connectionContext; + + private final HeartbeatMessageStrategy heartbeatMessageStrategy; + + private final ConnectionLifecycleHandler lifecycleHandler; + + private ScheduledFuture heartbeatFuture; + + private int consecutiveHeartbeatMisses; + + public HeartbeatHandler(ConnectionContext connectionContext, HeartbeatMessageStrategy heartbeatMessageStrategy, + ConnectionLifecycleHandler lifecycleHandler) { + this.connectionContext = connectionContext; + this.heartbeatMessageStrategy = heartbeatMessageStrategy; + this.lifecycleHandler = lifecycleHandler == null ? ConnectionLifecycleHandler.NO_OP : lifecycleHandler; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + scheduleHeartbeat(ctx); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + shutdownExecutorGracefully(); + super.channelInactive(ctx); + } + + private void scheduleHeartbeat(ChannelHandlerContext ctx) { + if (heartbeatMessageStrategy == null) { + log.debug("Skip heartbeat scheduling because strategy is null: sessionKey={}", resolveSessionKey()); + return; + } + heartbeatFuture = heartbeatExecutor.scheduleAtFixedRate(() -> { + if (!ctx.channel().isActive()) { + return; + } + try { + String heartbeatMessage = heartbeatMessageStrategy.buildHeartbeatMessage(connectionContext); + if (StrUtil.isBlank(heartbeatMessage)) { + return; + } + // The client pipeline still uses line based frames, so the + // generic heartbeat writer normalizes the trailing separator. + if (!heartbeatMessage.endsWith("\n")) { + heartbeatMessage = heartbeatMessage + "\n"; + } + ctx.channel().writeAndFlush(heartbeatMessage); + consecutiveHeartbeatMisses++; + log.debug("Send heartbeat packet: sessionKey={}, time={}, misses={}", + resolveSessionKey(), LocalDateTime.now(), consecutiveHeartbeatMisses); + if (consecutiveHeartbeatMisses >= MAX_HEARTBEAT_MISSES) { + handleHeartbeatTimeout(ctx); + } + } catch (Exception e) { + log.error("Send heartbeat packet failed: sessionKey={}", resolveSessionKey(), e); + } + }, 3, 10, TimeUnit.SECONDS); + } + + private void handleHeartbeatTimeout(ChannelHandlerContext ctx) { + log.warn("Heartbeat timeout reached: sessionKey={}, misses={}", + resolveSessionKey(), consecutiveHeartbeatMisses); + lifecycleHandler.onIdleTimeout(connectionContext); + String sessionKey = resolveSessionKey(); + if (StrUtil.isNotBlank(sessionKey)) { + SocketManager.removeUser(sessionKey); + } + consecutiveHeartbeatMisses = 0; + if (ctx.channel().isActive()) { + ctx.close(); + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { + if (heartbeatMessageStrategy != null && heartbeatMessageStrategy.isHeartbeatResponse(connectionContext, msg)) { + consecutiveHeartbeatMisses = 0; + log.debug("Receive heartbeat response: sessionKey={}, time={}", resolveSessionKey(), LocalDateTime.now()); + return; + } + ctx.fireChannelRead(msg); + } + + private String resolveSessionKey() { + return connectionContext == null ? null : connectionContext.getSessionKey(); + } + + private void shutdownExecutorGracefully() { + try { + if (heartbeatFuture != null && !heartbeatFuture.isCancelled()) { + heartbeatFuture.cancel(false); + } + heartbeatExecutor.shutdown(); + if (!heartbeatExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + heartbeatExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + heartbeatExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyClient.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyClient.java new file mode 100644 index 0000000..f47323d --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyClient.java @@ -0,0 +1,154 @@ +package com.njcn.gather.detection.util.socket.cilent; + +import cn.hutool.core.util.ObjectUtil; +import com.njcn.gather.detection.util.socket.SocketManager; +import com.njcn.gather.detection.util.socket.communication.handler.ConnectionLifecycleHandler; +import com.njcn.gather.detection.util.socket.communication.handler.HeartbeatMessageStrategy; +import com.njcn.gather.detection.util.socket.communication.handler.SocketMessageHandler; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionType; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * Generic Netty client entry. + * Stage 4-A removes direct dependencies on detection handlers and services. + * Message parsing, heartbeat framing and lifecycle side effects now come from + * callbacks attached to {@link ConnectionContext}. + * + * @author wr + * @author hongawen + * @date 2026/04/07 + */ +@Slf4j +@Component +public class NettyClient { + + public void connect(String ip, Integer port, ConnectionContext context, String msg) { + if (ObjectUtil.isNull(context) || ObjectUtil.isNull(context.getConnectionType())) { + log.warn("Skip socket connect because connection context is null"); + return; + } + SocketMessageHandler messageHandler = context.getMessageHandler(); + ConnectionLifecycleHandler lifecycleHandler = resolveLifecycleHandler(context); + HeartbeatMessageStrategy heartbeatMessageStrategy = context.getHeartbeatStrategy(); + SimpleChannelInboundHandler handler = createHandler(context, messageHandler, lifecycleHandler); + executeSocketConnection(ip, port, context, msg, handler, lifecycleHandler, heartbeatMessageStrategy); + } + + private SimpleChannelInboundHandler createHandler(ConnectionContext context, + SocketMessageHandler messageHandler, + ConnectionLifecycleHandler lifecycleHandler) { + if (context.getConnectionType() == ConnectionType.SOURCE) { + return new NettySourceClientHandler(context, messageHandler, lifecycleHandler); + } + if (context.getConnectionType() == ConnectionType.DEVICE) { + return new NettyDevClientHandler(context, messageHandler, lifecycleHandler); + } + return new NettyContrastClientHandler(context, messageHandler, lifecycleHandler); + } + + private ConnectionLifecycleHandler resolveLifecycleHandler(ConnectionContext context) { + ConnectionLifecycleHandler lifecycleHandler = context.getLifecycleHandler(); + return lifecycleHandler == null ? ConnectionLifecycleHandler.NO_OP : lifecycleHandler; + } + + private void executeSocketConnection(String ip, Integer port, ConnectionContext context, String msg, + SimpleChannelInboundHandler handler, + ConnectionLifecycleHandler lifecycleHandler, + HeartbeatMessageStrategy heartbeatMessageStrategy) { + NioEventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap bootstrap = new Bootstrap() + .group(group) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .channel(NioSocketChannel.class) + .handler(createChannelInitializer(context, handler, lifecycleHandler, heartbeatMessageStrategy)); + ChannelFuture channelFuture = bootstrap.connect(ip, port).sync(); + handleConnectionResult(channelFuture, context, group, msg, lifecycleHandler); + } catch (Exception e) { + log.warn("Connect socket server error: type={}, sessionKey={}", + context.getConnectionType(), context.getSessionKey(), e); + group.shutdownGracefully(); + lifecycleHandler.onConnectFailed(context, e); + } + } + + private ChannelInitializer createChannelInitializer(ConnectionContext context, + SimpleChannelInboundHandler handler, + ConnectionLifecycleHandler lifecycleHandler, + HeartbeatMessageStrategy heartbeatMessageStrategy) { + return new ChannelInitializer() { + @Override + protected void initChannel(NioSocketChannel ch) { + setupPipeline(ch.pipeline(), context, handler, lifecycleHandler, heartbeatMessageStrategy); + } + }; + } + + /** + * Key refactor point: pipeline extension now comes from generic strategy + * and lifecycle callbacks instead of fixed detection business classes. + */ + private void setupPipeline(ChannelPipeline pipeline, ConnectionContext context, + SimpleChannelInboundHandler handler, + ConnectionLifecycleHandler lifecycleHandler, + HeartbeatMessageStrategy heartbeatMessageStrategy) { + pipeline.addLast(new LineBasedFrameDecoder(10240 * 2)) + .addLast(new StringDecoder(CharsetUtil.UTF_8)) + .addLast(new StringEncoder(CharsetUtil.UTF_8)) + .addLast(new HeartbeatHandler(context, heartbeatMessageStrategy, lifecycleHandler)); + if (context.getConnectionType().isEnableIdleMonitor()) { + pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS)); + } + pipeline.addLast(handler); + } + + private void handleConnectionResult(ChannelFuture channelFuture, ConnectionContext context, + NioEventLoopGroup group, String msg, + ConnectionLifecycleHandler lifecycleHandler) { + channelFuture.addListener((ChannelFutureListener) future -> { + if (!future.isSuccess()) { + log.warn("Connect socket server failed: type={}, sessionKey={}", + context.getConnectionType(), context.getSessionKey(), future.cause()); + group.shutdownGracefully(); + lifecycleHandler.onConnectFailed(context, future.cause()); + return; + } + log.info("Connect socket server success: type={}, sessionKey={}", + context.getConnectionType(), context.getSessionKey()); + manageSocketConnection(context, group); + SocketManager.addUser(context.getSessionKey(), future.channel()); + SocketManager.sendMsg(context.getSessionKey(), msg); + }); + } + + private void manageSocketConnection(ConnectionContext context, NioEventLoopGroup group) { + String sessionKey = context.getSessionKey(); + NioEventLoopGroup existingGroup = SocketManager.getGroupByUserId(sessionKey); + if (ObjectUtil.isNotNull(existingGroup)) { + try { + existingGroup.shutdownGracefully().sync(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + SocketManager.addGroup(sessionKey, group); + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyContrastClientHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyContrastClientHandler.java new file mode 100644 index 0000000..066b7e8 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyContrastClientHandler.java @@ -0,0 +1,20 @@ +package com.njcn.gather.detection.util.socket.cilent; + +import com.njcn.gather.detection.util.socket.communication.handler.ConnectionLifecycleHandler; +import com.njcn.gather.detection.util.socket.communication.handler.SocketMessageHandler; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; + +/** + * Contrast device client transport handler. + * + * @author caozehui + * @author hongawen + * @date 2026/04/07 + */ +public class NettyContrastClientHandler extends AbstractNettyClientHandler { + + public NettyContrastClientHandler(ConnectionContext connectionContext, SocketMessageHandler socketMessageHandler, + ConnectionLifecycleHandler lifecycleHandler) { + super("contrast-device-client", connectionContext, socketMessageHandler, lifecycleHandler); + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyDevClientHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyDevClientHandler.java new file mode 100644 index 0000000..45f1503 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettyDevClientHandler.java @@ -0,0 +1,20 @@ +package com.njcn.gather.detection.util.socket.cilent; + +import com.njcn.gather.detection.util.socket.communication.handler.ConnectionLifecycleHandler; +import com.njcn.gather.detection.util.socket.communication.handler.SocketMessageHandler; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; + +/** + * Device client transport handler. + * + * @author wr + * @author hongawen + * @date 2026/04/07 + */ +public class NettyDevClientHandler extends AbstractNettyClientHandler { + + public NettyDevClientHandler(ConnectionContext connectionContext, SocketMessageHandler socketMessageHandler, + ConnectionLifecycleHandler lifecycleHandler) { + super("device-client", connectionContext, socketMessageHandler, lifecycleHandler); + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettySourceClientHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettySourceClientHandler.java new file mode 100644 index 0000000..8648af7 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/cilent/NettySourceClientHandler.java @@ -0,0 +1,20 @@ +package com.njcn.gather.detection.util.socket.cilent; + +import com.njcn.gather.detection.util.socket.communication.handler.ConnectionLifecycleHandler; +import com.njcn.gather.detection.util.socket.communication.handler.SocketMessageHandler; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; + +/** + * Source client transport handler. + * + * @author wr + * @author hongawen + * @date 2026/04/07 + */ +public class NettySourceClientHandler extends AbstractNettyClientHandler { + + public NettySourceClientHandler(ConnectionContext connectionContext, SocketMessageHandler socketMessageHandler, + ConnectionLifecycleHandler lifecycleHandler) { + super("source-client", connectionContext, socketMessageHandler, lifecycleHandler); + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/constants/SocketTransportConstants.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/constants/SocketTransportConstants.java new file mode 100644 index 0000000..1dfde89 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/constants/SocketTransportConstants.java @@ -0,0 +1,21 @@ +package com.njcn.gather.detection.util.socket.communication.constants; + +/** + * Socket transport constants kept by the communication foundation. + * Stage 4-A extracts these values from detection-only helpers so the + * retained transport layer does not depend on business utility classes. + * + * @author hongawen + * @date 2026/04/07 + */ +public final class SocketTransportConstants { + + public static final String SOURCE_SESSION_TAG = "_Source"; + + public static final String DEVICE_SESSION_TAG = "_Dev"; + + public static final String CONTRAST_SESSION_TAG = "_Contrast_Dev"; + + private SocketTransportConstants() { + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/ConnectionLifecycleHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/ConnectionLifecycleHandler.java new file mode 100644 index 0000000..cffba42 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/ConnectionLifecycleHandler.java @@ -0,0 +1,35 @@ +package com.njcn.gather.detection.util.socket.communication.handler; + +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; + +/** + * Connection lifecycle callback for the retained communication foundation. + * Business modules can attach optional callbacks here without leaking their + * own service types into Netty and WebSocket infrastructure. + * + * @author hongawen + * @date 2026/04/07 + */ +public interface ConnectionLifecycleHandler { + + ConnectionLifecycleHandler NO_OP = new ConnectionLifecycleHandler() { + }; + + default void onConnected(ConnectionContext context) { + } + + default void onDisconnected(ConnectionContext context) { + } + + default void onConnectFailed(ConnectionContext context, Throwable cause) { + } + + default void onIdleTimeout(ConnectionContext context) { + } + + default void onMessageHandlingError(ConnectionContext context, String message, Throwable cause) { + } + + default void onException(ConnectionContext context, Throwable cause) { + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/HeartbeatMessageStrategy.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/HeartbeatMessageStrategy.java new file mode 100644 index 0000000..7bced6b --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/HeartbeatMessageStrategy.java @@ -0,0 +1,25 @@ +package com.njcn.gather.detection.util.socket.communication.handler; + +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; + +/** + * Heartbeat protocol strategy for generic client connections. + * The transport layer only knows when to send and detect heartbeat frames; + * the concrete heartbeat payload is provided by the business side. + * + * @author hongawen + * @date 2026/04/07 + */ +public interface HeartbeatMessageStrategy { + + /** + * Build the outbound heartbeat packet. Return {@code null} or blank to + * disable heartbeat sending for the current connection. + */ + String buildHeartbeatMessage(ConnectionContext context); + + /** + * Check whether the inbound message is a heartbeat response frame. + */ + boolean isHeartbeatResponse(ConnectionContext context, String message); +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/SocketMessageHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/SocketMessageHandler.java new file mode 100644 index 0000000..90a593a --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/handler/SocketMessageHandler.java @@ -0,0 +1,23 @@ +package com.njcn.gather.detection.util.socket.communication.handler; + +import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext; + +/** + * Socket 消息处理接口。 + * 第一阶段先把消息回调从具体业务 Service 中抽离成统一入口,后续可以继续沉淀为独立通讯模块。 + * + * @author hongawen + * @date 2026/04/07 + */ +@FunctionalInterface +public interface SocketMessageHandler { + + /** + * 处理收到的 Socket 消息。 + * + * @param context 连接上下文 + * @param message 文本消息 + * @throws Exception 处理异常 + */ + void handle(ConnectionContext context, String message) throws Exception; +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/model/ConnectionContext.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/model/ConnectionContext.java new file mode 100644 index 0000000..09d711c --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/model/ConnectionContext.java @@ -0,0 +1,111 @@ +package com.njcn.gather.detection.util.socket.communication.model; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.detection.util.socket.communication.handler.ConnectionLifecycleHandler; +import com.njcn.gather.detection.util.socket.communication.handler.HeartbeatMessageStrategy; +import com.njcn.gather.detection.util.socket.communication.handler.SocketMessageHandler; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Generic communication connection context. + * Stage 4-A keeps transport identity and optional callbacks in one place so + * Netty client/server code does not need to know detection business types. + * + * @author hongawen + * @date 2026/04/07 + */ +public class ConnectionContext { + + public static final String ATTR_PRE_DETECTION_PARAM = "preDetectionParam"; + + public static final String ATTR_CONTRAST_PARAM = "contrastDetectionParam"; + + public static final String ATTR_SOCKET_MESSAGE_HANDLER = "socketMessageHandler"; + + public static final String ATTR_CONNECTION_LIFECYCLE_HANDLER = "connectionLifecycleHandler"; + + public static final String ATTR_HEARTBEAT_MESSAGE_STRATEGY = "heartbeatMessageStrategy"; + + private final String userId; + + private final ConnectionType connectionType; + + private final Map attributes = new ConcurrentHashMap<>(); + + public ConnectionContext(String userId, ConnectionType connectionType) { + this.userId = userId; + this.connectionType = connectionType; + } + + public static ConnectionContext of(String userId, ConnectionType connectionType) { + return new ConnectionContext(userId, connectionType); + } + + public String getUserId() { + return userId; + } + + public ConnectionType getConnectionType() { + return connectionType; + } + + /** + * Key refactor point: the transport foundation now resolves the session + * key from one place instead of reassembling it across multiple classes. + */ + public String getSessionKey() { + if (StrUtil.isBlank(userId) || connectionType == null) { + return userId; + } + return userId + connectionType.getSessionTag(); + } + + public ConnectionContext addAttribute(String key, Object value) { + if (StrUtil.isNotBlank(key) && value != null) { + attributes.put(key, value); + } + return this; + } + + public ConnectionContext addMessageHandler(SocketMessageHandler handler) { + return addAttribute(ATTR_SOCKET_MESSAGE_HANDLER, handler); + } + + public SocketMessageHandler getMessageHandler() { + return getAttribute(ATTR_SOCKET_MESSAGE_HANDLER, SocketMessageHandler.class); + } + + public ConnectionContext addLifecycleHandler(ConnectionLifecycleHandler lifecycleHandler) { + return addAttribute(ATTR_CONNECTION_LIFECYCLE_HANDLER, lifecycleHandler); + } + + public ConnectionLifecycleHandler getLifecycleHandler() { + return getAttribute(ATTR_CONNECTION_LIFECYCLE_HANDLER, ConnectionLifecycleHandler.class); + } + + public ConnectionContext addHeartbeatStrategy(HeartbeatMessageStrategy heartbeatMessageStrategy) { + return addAttribute(ATTR_HEARTBEAT_MESSAGE_STRATEGY, heartbeatMessageStrategy); + } + + public HeartbeatMessageStrategy getHeartbeatStrategy() { + return getAttribute(ATTR_HEARTBEAT_MESSAGE_STRATEGY, HeartbeatMessageStrategy.class); + } + + public Object getAttribute(String key) { + return attributes.get(key); + } + + public T getAttribute(String key, Class type) { + Object value = attributes.get(key); + if (type.isInstance(value)) { + return type.cast(value); + } + return null; + } + + public Map getAttributes() { + return attributes; + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/model/ConnectionType.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/model/ConnectionType.java new file mode 100644 index 0000000..0d7a383 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/communication/model/ConnectionType.java @@ -0,0 +1,42 @@ +package com.njcn.gather.detection.util.socket.communication.model; + +import com.njcn.gather.detection.util.socket.communication.constants.SocketTransportConstants; + +/** + * Communication connection types retained by the transport foundation. + * Stage 4-A keeps the session tags in a neutral constants class so the + * Netty/WebSocket base layer no longer depends on detection helpers. + * + * @author hongawen + * @date 2026/04/07 + */ +public enum ConnectionType { + + SOURCE(SocketTransportConstants.SOURCE_SESSION_TAG, "程控源", false), + DEVICE(SocketTransportConstants.DEVICE_SESSION_TAG, "被检设备", true), + CONTRAST(SocketTransportConstants.CONTRAST_SESSION_TAG, "比对被检设备", true); + + private final String sessionTag; + + private final String displayName; + + private final boolean enableIdleMonitor; + + ConnectionType(String sessionTag, String displayName, boolean enableIdleMonitor) { + this.sessionTag = sessionTag; + this.displayName = displayName; + this.enableIdleMonitor = enableIdleMonitor; + } + + public String getSessionTag() { + return sessionTag; + } + + public String getDisplayName() { + return displayName; + } + + public boolean isEnableIdleMonitor() { + return enableIdleMonitor; + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/config/SocketConnectionConfig.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/config/SocketConnectionConfig.java new file mode 100644 index 0000000..f053029 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/config/SocketConnectionConfig.java @@ -0,0 +1,169 @@ +package com.njcn.gather.detection.util.socket.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Socket连接配置管理类 + * 定义哪些requestId需要建立通道连接,以及IP/PORT配置 + * + * @Author: hongawen + * @Date: 2024/12/10 + */ +@Component +@ConfigurationProperties(prefix = "socket") +public class SocketConnectionConfig { + + /** + * 程控源设备配置 + */ + private SourceConfig source = new SourceConfig(); + + /** + * 被检设备配置 + */ + private DeviceConfig device = new DeviceConfig(); + + @Data + public static class SourceConfig { + /** + * 程控源IP地址 + */ + private String ip; + + /** + * 程控源端口号 + */ + private Integer port; + } + + @Data + public static class DeviceConfig { + /** + * 被检设备IP地址 + */ + private String ip; + + /** + * 被检设备端口号 + */ + private Integer port; + } + + /** + * 获取程控源配置 + */ + public SourceConfig getSource() { + return source; + } + + /** + * 获取被检设备配置 + */ + public DeviceConfig getDevice() { + return device; + } + + /** + * 需要建立程控源通道的requestId集合 + * 这些requestId在发送消息时,如果程控源通道不存在,会自动建立连接 + */ + private static final Set SOURCE_CONNECTION_REQUEST_IDS = new HashSet<>(Arrays.asList( + // 源通讯检测 + "yjc_ytxjy" + // 可以根据实际业务需求添加更多requestId + )); + + /** + * 需要建立被检设备通道的requestId集合 + * 这些requestId在发送消息时,如果被检设备通道不存在,会自动建立连接 + */ + private static final Set DEVICE_CONNECTION_REQUEST_IDS = new HashSet<>(Arrays.asList( + // 连接建立 + "yjc_sbtxjy", + // ftp文件传送指令 + "FTP_SEND$01" + // 可以根据实际业务需求添加更多requestId + )); + + /** + * 检查指定的requestId是否需要建立程控源连接 + * + * @param requestId 请求ID + * @return boolean true:需要建立连接, false:不需要建立连接 + */ + public static boolean needsSourceConnection(String requestId) { + return SOURCE_CONNECTION_REQUEST_IDS.contains(requestId); + } + + /** + * 检查指定的requestId是否需要建立被检设备连接 + * + * @param requestId 请求ID + * @return boolean true:需要建立连接, false:不需要建立连接 + */ + public static boolean needsDeviceConnection(String requestId) { + return DEVICE_CONNECTION_REQUEST_IDS.contains(requestId); + } + + /** + * 添加需要建立程控源连接的requestId + * 支持运行时动态添加 + * + * @param requestId 请求ID + */ + public static void addSourceConnectionRequestId(String requestId) { + SOURCE_CONNECTION_REQUEST_IDS.add(requestId); + } + + /** + * 添加需要建立被检设备连接的requestId + * 支持运行时动态添加 + * + * @param requestId 请求ID + */ + public static void addDeviceConnectionRequestId(String requestId) { + DEVICE_CONNECTION_REQUEST_IDS.add(requestId); + } + + /** + * 移除程控源连接requestId + * + * @param requestId 请求ID + */ + public static void removeSourceConnectionRequestId(String requestId) { + SOURCE_CONNECTION_REQUEST_IDS.remove(requestId); + } + + /** + * 移除被检设备连接requestId + * + * @param requestId 请求ID + */ + public static void removeDeviceConnectionRequestId(String requestId) { + DEVICE_CONNECTION_REQUEST_IDS.remove(requestId); + } + + /** + * 获取所有需要建立程控源连接的requestId集合(只读) + * + * @return Set requestId集合 + */ + public static Set getSourceConnectionRequestIds() { + return new HashSet<>(SOURCE_CONNECTION_REQUEST_IDS); + } + + /** + * 获取所有需要建立被检设备连接的requestId集合(只读) + * + * @return Set requestId集合 + */ + public static Set getDeviceConnectionRequestIds() { + return new HashSet<>(DEVICE_CONNECTION_REQUEST_IDS); + } +} \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebServiceManager.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebServiceManager.java new file mode 100644 index 0000000..1f9783b --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebServiceManager.java @@ -0,0 +1,87 @@ +package com.njcn.gather.detection.util.socket.websocket; + +import com.alibaba.fastjson.JSON; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Generic WebSocket session manager. + * Stage 4-A removes detection payload conventions and detection parameter + * caches from this class so it remains a pure WebSocket session registry. + * + * @author wr + * @author hongawen + * @date 2026/04/07 + */ +@Slf4j +public class WebServiceManager { + + private static final Map USER_SESSIONS = new ConcurrentHashMap<>(); + + private WebServiceManager() { + } + + public static void addUser(String userId, Channel channel) { + USER_SESSIONS.put(userId, channel); + } + + public static Channel removeByUserId(String userId) { + return USER_SESSIONS.remove(userId); + } + + @Deprecated + public static void removeChannel(String channelId) { + Iterator> iterator = USER_SESSIONS.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().id().toString().equals(channelId)) { + iterator.remove(); + break; + } + } + } + + public static void sendMsg(String userId, String msg) { + Channel channel = USER_SESSIONS.get(userId); + if (Objects.nonNull(channel) && channel.isActive()) { + channel.writeAndFlush(new TextWebSocketFrame(msg)); + return; + } + log.error("WebSocket push failed because session is offline, time={}, userId={}", + LocalDateTime.now(), userId); + WebSocketHandler.cleanupSocketResources(userId); + } + + public static void sendJson(String userId, Object payload) { + Channel channel = USER_SESSIONS.get(userId); + if (Objects.nonNull(channel) && channel.isActive()) { + channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(payload))); + return; + } + log.error("WebSocket json push failed because session is offline, time={}, userId={}", + LocalDateTime.now(), userId); + WebSocketHandler.cleanupSocketResources(userId); + } + + public static int getOnlineUserCount() { + return USER_SESSIONS.size(); + } + + public static boolean isUserOnline(String userId) { + Channel channel = USER_SESSIONS.get(userId); + return Objects.nonNull(channel) && channel.isActive(); + } + + public static Set getOnlineUserIds() { + return new HashSet<>(USER_SESSIONS.keySet()); + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketConstants.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketConstants.java new file mode 100644 index 0000000..a547218 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketConstants.java @@ -0,0 +1,49 @@ +package com.njcn.gather.detection.util.socket.websocket; + +/** + * WebSocket常量管理类 + * + * @author wr + * @date 2024/12/10 + */ +public final class WebSocketConstants { + + /** + * URL参数分隔符 + */ + public static final String QUESTION_MARK = "?"; + + /** + * URL参数等号分隔符 + */ + public static final String EQUAL_TO = "="; + + /** + * 客户端心跳消息 + */ + public static final String HEARTBEAT_PING = "alive"; + + /** + * 服务端心跳响应 + */ + public static final String HEARTBEAT_PONG = "over"; + + /** + * 心跳超时最大次数 + */ + public static final int MAX_HEARTBEAT_MISS_COUNT = 3; + + /** + * WebSocket握手失败状态码 + */ + public static final int HANDSHAKE_FAILED_STATUS = 4000; + + /** + * WebSocket握手失败原因 + */ + public static final String HANDSHAKE_FAILED_REASON = "Missing required userId parameter"; + + private WebSocketConstants() { + // 私有构造函数,防止实例化 + } +} \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketHandler.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketHandler.java new file mode 100644 index 0000000..430baf5 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketHandler.java @@ -0,0 +1,154 @@ +package com.njcn.gather.detection.util.socket.websocket; + +import com.njcn.gather.detection.util.socket.SocketManager; +import com.njcn.gather.detection.util.socket.communication.model.ConnectionType; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.AttributeKey; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +import static com.njcn.gather.detection.util.socket.websocket.WebSocketConstants.HEARTBEAT_PING; +import static com.njcn.gather.detection.util.socket.websocket.WebSocketConstants.HEARTBEAT_PONG; +import static com.njcn.gather.detection.util.socket.websocket.WebSocketConstants.MAX_HEARTBEAT_MISS_COUNT; + +/** + * Generic WebSocket handler retained by the communication foundation. + * Stage 4-A keeps only handshake, heartbeat, session registry and transport + * cleanup. Detection-specific quit flows are removed from this class. + * + * @author wr + * @author hongawen + * @date 2026/04/07 + */ +@Slf4j +public class WebSocketHandler extends SimpleChannelInboundHandler { + + private static final String HEARTBEAT_RESPONSE_TEXT = HEARTBEAT_PONG; + + private int times; + + private String userId; + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("WebSocket channel active: channelId={}", ctx.channel().id()); + super.channelActive(ctx); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) { + String messageText = msg.text(); + if (HEARTBEAT_PING.equals(messageText)) { + handleHeartbeat(ctx); + return; + } + log.debug("Receive WebSocket business message: userId={}, channelId={}, message={}", + userId, ctx.channel().id(), messageText); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + log.info("WebSocket handler added: channelId={}", ctx.channel().id()); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + log.info("WebSocket handler removed: channelId={}, userId={}", ctx.channel().id(), userId); + if (userId != null) { + WebServiceManager.removeByUserId(userId); + } else { + WebServiceManager.removeChannel(ctx.channel().id().toString()); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.info("WebSocket channel inactive: channelId={}, userId={}", ctx.channel().id(), userId); + cleanupSocketResources(userId); + super.channelInactive(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { + WebSocketServerProtocolHandler.HandshakeComplete handshakeComplete = + (WebSocketServerProtocolHandler.HandshakeComplete) evt; + userId = ctx.channel().attr(AttributeKey.valueOf("userId")).get(); + log.info("WebSocket handshake complete: userId={}, channelId={}, requestUri={}", + userId, ctx.channel().id(), handshakeComplete.requestUri()); + if (userId != null) { + WebServiceManager.addUser(userId, ctx.channel()); + } + sendConnectionSuccessMessage(ctx); + return; + } + + if (evt instanceof IdleStateEvent) { + times++; + log.warn("WebSocket heartbeat miss: channelId={}, userId={}, missCount={}", + ctx.channel().id(), userId, times); + if (times > MAX_HEARTBEAT_MISS_COUNT) { + cleanupSocketResources(userId); + ctx.close(); + } + return; + } + + super.userEventTriggered(ctx, evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logExceptionByType(ctx.channel().id().toString(), cause); + cleanupSocketResources(userId); + ctx.close(); + } + + private void sendConnectionSuccessMessage(ChannelHandlerContext ctx) { + String welcomeMessage = String.format( + "{\"type\":\"connection\",\"status\":\"success\",\"message\":\"WebSocket连接建立成功\",\"userId\":\"%s\",\"timestamp\":%d}", + userId, System.currentTimeMillis()); + ctx.channel().writeAndFlush(new TextWebSocketFrame(welcomeMessage)); + } + + private void handleHeartbeat(ChannelHandlerContext ctx) { + times = 0; + ctx.channel().writeAndFlush(new TextWebSocketFrame(HEARTBEAT_RESPONSE_TEXT)); + } + + private void logExceptionByType(String channelId, Throwable cause) { + if (cause instanceof IOException) { + log.info("WebSocket network exception: channelId={}, message={}", channelId, cause.getMessage()); + } else if (cause instanceof WebSocketHandshakeException) { + log.warn("WebSocket handshake exception: channelId={}, message={}", channelId, cause.getMessage()); + } else if (cause instanceof DecoderException || cause instanceof CorruptedFrameException) { + log.error("WebSocket decode exception: channelId={}, message={}", channelId, cause.getMessage(), cause); + } else if (cause instanceof IllegalArgumentException) { + log.warn("WebSocket argument exception: channelId={}, message={}", channelId, cause.getMessage()); + } else { + log.error("WebSocket unclassified exception: channelId={}, message={}", channelId, cause.getMessage(), cause); + } + } + + /** + * Key refactor point: websocket disconnect now performs generic transport + * cleanup only, which makes this layer independent from detection flows. + */ + public static void cleanupSocketResources(String userId) { + if (userId == null || userId.trim().isEmpty()) { + return; + } + WebServiceManager.removeByUserId(userId); + for (ConnectionType connectionType : ConnectionType.values()) { + SocketManager.removeUser(userId + connectionType.getSessionTag()); + } + } +} diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketInitializer.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketInitializer.java new file mode 100644 index 0000000..021d1d8 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketInitializer.java @@ -0,0 +1,184 @@ +package com.njcn.gather.detection.util.socket.websocket; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.AttributeKey; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * WebSocket服务端管道初始化器 + * + * 职责: + * 1. 为每个新的WebSocket连接配置处理器链(Pipeline) + * 2. 按正确顺序添加各种Handler,确保数据流正确处理 + * 3. 配置HTTP到WebSocket的协议升级 + * 4. 设置心跳检测和异常处理机制 + * + * 处理流程: + * HTTP请求 → HTTP编解码 → 分块处理 → 消息聚合 → 协议升级 → 心跳检测 → 业务处理 → 异常处理 + * + * @Description: webSocket服务端自定义配置 + * @Author: wr + * @Date: 2024/12/10 14:20 + */ +@Slf4j +public class WebSocketInitializer extends ChannelInitializer { + + /** + * WebSocket访问路径 + */ + private static final String WEBSOCKET_PATH = "/hello"; + + /** + * HTTP消息最大聚合大小:512KB + * 用于WebSocket握手和消息传输 + */ + private static final int MAX_CONTENT_LENGTH = 512 * 1024; + + /** + * 心跳检测间隔:13秒 + * 13秒内没有收到客户端消息则触发空闲事件 + */ + private static final int READER_IDLE_TIME_SECONDS = 13; + + /** + * 为每个新连接初始化处理器管道 + * 注意:Handler的添加顺序非常重要,决定了数据的处理流向 + * + * @param ch 新建立的Socket通道 + * @throws Exception 初始化过程中的异常 + */ + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + // 1. HTTP协议处理器 + // HttpServerCodec = HttpRequestDecoder + HttpResponseEncoder + // 负责HTTP请求解码和HTTP响应编码 + pipeline.addLast("http-codec", new HttpServerCodec()); + + // 2. 分块写入处理器 + // 用于处理大文件的分块传输,防止内存溢出 + // 支持ChunkedInput,如ChunkedFile、ChunkedNioFile等 + pipeline.addLast("chunked-write", new ChunkedWriteHandler()); + + // 3. HTTP消息聚合器 + // 将分片的HTTP消息重新组装成完整的FullHttpRequest或FullHttpResponse + // WebSocket握手需要完整的HTTP请求,所以这个Handler必须添加 + pipeline.addLast("http-aggregator", new HttpObjectAggregator(MAX_CONTENT_LENGTH)); + + // 4. WebSocket URL预处理器 + // 在WebSocket握手之前处理URL参数,验证用户ID + pipeline.addLast("websocket-preprocessor", new WebSocketPreprocessor()); + + // 5. WebSocket协议升级处理器 + // 处理WebSocket握手,将HTTP协议升级为WebSocket协议 + // 只有访问指定路径(WEBSOCKET_PATH)的请求才会被升级 + // 升级后会移除HTTP相关的Handler,添加WebSocket相关的Handler + pipeline.addLast("websocket-protocol", new WebSocketServerProtocolHandler(WEBSOCKET_PATH)); + + // 6. 空闲状态检测器 + // 检测连接的空闲状态,用于心跳机制 + // readerIdleTime: 读空闲时间,writerIdleTime: 写空闲时间,allIdleTime: 读写空闲时间 + pipeline.addLast("idle-state", new IdleStateHandler(READER_IDLE_TIME_SECONDS, 0, 0, TimeUnit.SECONDS)); + + // 7. 自定义WebSocket业务处理器 + // 处理WebSocket帧,实现具体的业务逻辑 + // 包括心跳处理、消息路由、连接管理等 + pipeline.addLast("websocket-handler", new WebSocketHandler()); + + // 7. 全局异常处理器 + // 处理整个管道中未被捕获的异常,作为最后的异常处理兜底 + pipeline.addLast("exception-handler", new GlobalExceptionHandler()); + } + + /** + * WebSocket预处理器 + * 在WebSocket握手之前验证URL参数并清理URL + */ + private static class WebSocketPreprocessor extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof FullHttpRequest) { + FullHttpRequest request = (FullHttpRequest) msg; + String uri = request.uri(); + + log.debug("WebSocket预处理器收到HTTP请求:{}", uri); + + // 验证并提取userId + String userId = extractUserId(uri); + if (userId == null || userId.trim().isEmpty()) { + log.warn("WebSocket连接被拒绝:缺少userId参数, uri: {}", uri); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST + ); + ctx.writeAndFlush(response).addListener(f -> ctx.close()); + return; + } + + // 将userId存储到Channel属性中 + ctx.channel().attr(AttributeKey.valueOf("userId")).set(userId); + + // 清理URL参数 + if (uri.contains("?")) { + String cleanUri = uri.substring(0, uri.indexOf("?")); + request.setUri(cleanUri); + log.debug("URL已清理,原始: {}, 清理后: {}, userId: {}", uri, cleanUri, userId); + } + } + + // 继续传递给下一个Handler + super.channelRead(ctx, msg); + } + + private String extractUserId(String uri) { + if (!uri.contains("name=")) { + return null; + } + int start = uri.indexOf("name=") + 5; + int end = uri.indexOf("&", start); + if (end == -1) { + return uri.substring(start); + } else { + return uri.substring(start, end); + } + } + } + + /** + * 全局异常处理器 + * 作为管道中的最后一个Handler,捕获所有未处理的异常 + */ + private static class GlobalExceptionHandler extends ChannelInboundHandlerAdapter { + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + // 记录异常详情,便于问题排查 + log.error("WebSocket连接发生未处理异常,远程地址:{},异常信息:{}", + ctx.channel().remoteAddress(), cause.getMessage(), cause); + + // 优雅关闭连接 + if (ctx.channel().isActive()) { + ctx.close(); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.debug("WebSocket连接断开,远程地址:{}", ctx.channel().remoteAddress()); + super.channelInactive(ctx); + } + } +} + diff --git a/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketService.java b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketService.java new file mode 100644 index 0000000..df44eb0 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/detection/util/socket/websocket/WebSocketService.java @@ -0,0 +1,237 @@ +package com.njcn.gather.detection.util.socket.websocket; + + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LoggingHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + + +/** + * WebSocket服务端核心类 + * + * 职责: + * 1. 启动基于Netty的WebSocket服务器 + * 2. 管理服务器生命周期(启动/关闭) + * 3. 提供高性能的WebSocket通信支持 + * + * 特性: + * - 使用ApplicationRunner确保在Spring容器完全启动后再启动WebSocket服务 + * - 使用CompletableFuture异步启动,避免阻塞Spring Boot主线程 + * - 支持优雅关闭,确保资源正确释放 + * - 完善的异常处理和日志记录 + * + * @Description: websocket服务端 + * @Author: wr + * @Date: 2024/12/10 13:59 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class WebSocketService implements ApplicationRunner { + + /** + * WebSocket服务器监听端口 + * 默认7777端口,可通过配置文件webSocket.port自定义 + * 客户端连接地址:ws://host:port/hello?name=userId + */ + @Value("${webSocket.port:7777}") + int port; + + /** + * Netty Boss线程组 + * 专门负责接受新的客户端连接请求 + * 通常配置1个线程即可,因为接受连接的操作相对简单 + */ + EventLoopGroup bossGroup; + + /** + * Netty Worker线程组 + * 专门负责处理已建立连接的I/O操作和业务逻辑 + * 默认线程数 = CPU核心数 * 2,用于并发处理多个客户端 + */ + EventLoopGroup workerGroup; + + /** + * 服务器通道引用 + * 保存绑定端口后的Channel,用于服务器关闭时释放资源 + */ + private Channel serverChannel; + + /** + * 异步启动任务的Future对象 + * 用于管理WebSocket服务器的异步启动过程 + * 可以用来取消启动任务或检查启动状态 + */ + private CompletableFuture serverFuture; + + + + /** + * Spring Boot应用启动完成后自动调用此方法 + * 使用ApplicationRunner确保在所有Bean初始化完成后再启动WebSocket服务 + */ + @Override + public void run(ApplicationArguments args){ + // 使用CompletableFuture异步启动WebSocket服务,避免阻塞Spring Boot主线程 + // 这样可以让应用快速启动完成,WebSocket服务在后台异步启动 + serverFuture = CompletableFuture.runAsync(this::startWebSocketServer) + .exceptionally(throwable -> { + // 如果启动过程中发生异常,记录日志但不影响应用启动 + log.error("WebSocket服务启动异常", throwable); + return null; + }); + } + + /** + * 启动WebSocket服务器的核心方法 + * 此方法会一直阻塞直到服务器关闭,所以需要在异步线程中执行 + */ + private void startWebSocketServer() { + try { + // 1. 创建线程组 + // bossGroup: 专门负责接受新的客户端连接请求 + // 可以自定义线程的数量,这里使用默认值(通常为1个线程) + bossGroup = new NioEventLoopGroup(1); + + // workerGroup: 专门负责处理已建立连接的I/O操作 + // 默认创建的线程数量 = CPU 处理器数量 * 2,用于处理业务逻辑 + workerGroup = new NioEventLoopGroup(); + + // 2. 配置服务器启动参数 + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler()) + // 网络配置参数 + .option(ChannelOption.SO_BACKLOG, 128) + // TCP连接建立超时时间5秒 + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + // 子通道配置(针对每个客户端连接) + // 启用TCP keepalive机制,检测死连接 + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childHandler(new WebSocketInitializer()); + + // 3. 绑定端口并启动服务器 + ChannelFuture future = serverBootstrap.bind(port).sync(); + // 保存服务器通道引用,用于后续关闭操作 + serverChannel = future.channel(); + // 4. 监听绑定结果并记录日志 + future.addListener(f -> { + if (future.isSuccess()) { + log.info("webSocket服务启动成功,端口:{}", port); + } else { + log.error("webSocket服务启动失败,端口:{}", port); + } + }); + + // 5. 等待服务器关闭 + // 这里会一直阻塞,直到serverChannel被外部关闭 + // 这就是为什么需要在异步线程中执行此方法的原因 + future.channel().closeFuture().sync(); + + } catch (InterruptedException e) { + // 如果线程被中断(比如应用关闭),记录日志并恢复中断状态 + log.error("WebSocket服务启动过程中被中断", e); + Thread.currentThread().interrupt(); // 恢复中断状态 + } catch (Exception e) { + // 捕获其他所有异常,记录日志并抛出运行时异常 + log.error("WebSocket服务启动失败", e); + throw new RuntimeException("WebSocket服务启动失败", e); + } finally { + // 无论成功还是失败,都要清理资源 + shutdownGracefully(); + } + } + + + /** + * 优雅关闭Netty线程组资源 + * 私有方法,用于在服务器启动异常时清理资源 + */ + private void shutdownGracefully() { + // 优雅关闭接收连接的线程组 + if (bossGroup != null) { + bossGroup.shutdownGracefully(); + } + // 优雅关闭处理I/O的线程组 + if (workerGroup != null) { + workerGroup.shutdownGracefully(); + } + } + + /** + * Spring容器销毁时自动调用此方法释放资源 + * 使用@PreDestroy确保在应用关闭时优雅地关闭WebSocket服务 + */ + @PreDestroy + public void destroy() throws InterruptedException { + log.info("正在关闭WebSocket服务..."); + + // 步骤1: 首先关闭服务器通道,停止接受新的连接请求 + // 这样可以确保不会有新的客户端连接进来 + if (serverChannel != null) { + try { + // 等待最多5秒让服务器通道关闭 + serverChannel.close().awaitUninterruptibly(5, TimeUnit.SECONDS); + log.debug("服务器通道已关闭"); + } catch (Exception e) { + log.warn("关闭服务器通道时发生异常", e); + } + } + + // 步骤2: 关闭bossGroup线程组 + // bossGroup负责接受连接,现在可以安全关闭了 + if (bossGroup != null) { + try { + // 优雅关闭:静默期0秒,超时时间5秒 + // 静默期0秒意味着立即开始关闭,超时5秒后强制关闭 + bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).sync(); + log.debug("bossGroup线程组已关闭"); + } catch (InterruptedException e) { + log.warn("关闭bossGroup时被中断", e); + Thread.currentThread().interrupt(); // 恢复中断状态 + } + } + + // 步骤3: 关闭workerGroup线程组 + // workerGroup负责处理I/O,需要等待现有连接处理完成 + if (workerGroup != null) { + try { + // 等待现有任务完成,但最多等待5秒 + workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).sync(); + log.debug("workerGroup线程组已关闭"); + } catch (InterruptedException e) { + log.warn("关闭workerGroup时被中断", e); + Thread.currentThread().interrupt(); // 恢复中断状态 + } + } + + // 步骤4: 取消异步启动任务(如果还在运行) + // 这可以避免在应用关闭后还有线程在后台运行 + if (serverFuture != null && !serverFuture.isDone()) { + // true表示允许中断正在执行的任务 + boolean cancelled = serverFuture.cancel(true); + if (cancelled) { + log.debug("异步启动任务已取消"); + } + } + + log.info("webSocket服务已销毁"); + } + +} diff --git a/entrance/pom.xml b/entrance/pom.xml new file mode 100644 index 0000000..9f044dc --- /dev/null +++ b/entrance/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + com.njcn.gather + CN_Tool + 1.0.0 + + entrance + + + + com.njcn.gather + system + 1.0.0 + + + com.njcn.gather + detection + 1.0.0 + + + com.njcn.gather + user + 1.0.0 + + + + com.njcn.gather + activate-tool + 1.0.0 + + + + + entrance + + + org.springframework.boot + spring-boot-maven-plugin + + + package + + repackage + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + + src/main/resources + + **/* + + + + + + diff --git a/entrance/src/main/java/com/njcn/gather/EntranceApplication.java b/entrance/src/main/java/com/njcn/gather/EntranceApplication.java new file mode 100644 index 0000000..ea7ca1b --- /dev/null +++ b/entrance/src/main/java/com/njcn/gather/EntranceApplication.java @@ -0,0 +1,20 @@ +package com.njcn.gather; + +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + + +@Slf4j +@MapperScan("com.njcn.**.mapper") +@SpringBootApplication(scanBasePackages = "com.njcn") +//@EnableAspectJAutoProxy +public class EntranceApplication { + + public static void main(String[] args) { + SpringApplication.run(EntranceApplication.class, args); + } + +} diff --git a/entrance/src/main/resources/application.yml b/entrance/src/main/resources/application.yml new file mode 100644 index 0000000..4d3d925 --- /dev/null +++ b/entrance/src/main/resources/application.yml @@ -0,0 +1,54 @@ +server: + port: 18192 + +spring: + application: + name: entrance + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://192.168.1.22:13306/cn_tool?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true + username: root + password: njcnpqs + initial-size: 5 + min-idle: 5 + max-active: 50 + max-wait: 60000 + min-evictable-idle-time-millis: 300000 + validation-query: select 1 + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + +mybatis-plus: + mapper-locations: classpath*:com/njcn/**/mapping/*.xml + # Key refactor point: remove stale business alias packages and retain only + # the surviving foundational modules. + type-aliases-package: com.njcn.gather.system.dictionary.pojo.po + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl + global-config: + db-config: + id-type: assign_uuid + +socket: + source: + ip: 127.0.0.1 + port: 62000 + device: + ip: 127.0.0.1 + port: 61000 + +webSocket: + port: 7777 + +log: + homeDir: D:\logs + commonLevel: info + +activate: + private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo=" + public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB" diff --git a/entrance/src/main/resources/logback.xml b/entrance/src/main/resources/logback.xml new file mode 100644 index 0000000..5a87599 --- /dev/null +++ b/entrance/src/main/resources/logback.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + + + ${logHomeDir}/${log.projectName}/debug/debug.log + + + + + DEBUG + + ACCEPT + + DENY + + + + + + ${logHomeDir}/${log.projectName}/debug/debug.log.%d{yyyy-MM-dd}.%i.log + + 10MB + + ${log.maxHistory:-30} + + + + + + + + + + ${log.pattern} + + UTF-8 + + + + + + + INFO + ACCEPT + DENY + + + ${logHomeDir}/${log.projectName}/info/info.log + + + + ${logHomeDir}/${log.projectName}/info/info.log.%d{yyyy-MM-dd}.%i.log + + 10MB + ${log.maxHistory:-30} + + + + ${log.pattern} + + UTF-8 + + + + + + + + ${logHomeDir}/${log.projectName}/error/error.log + + + ERROR + ACCEPT + DENY + + + + ${logHomeDir}/${log.projectName}/error/error.log.%d{yyyy-MM-dd}.%i.log + + 10MB + ${log.maxHistory:-30} + + + + ${log.pattern} + + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ce81522 --- /dev/null +++ b/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + com.njcn.gather + CN_Tool + 1.0.0 + pom + CN_Tool + + + entrance + system + user + detection + tools + + + + + nexus-releases + Nexus Release Repository + http://192.168.1.22:8001/nexus/content/repositories/releases/ + + + nexus-snapshots + Nexus Snapshot Repository + http://192.168.1.22:8001/nexus/content/repositories/snapshots/ + + + + + 2.3.12.RELEASE + UTF-8 + UTF-8 + UTF-8 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.2.2.RELEASE + + true + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + UTF-8 + + + + + + src/main/resources + true + + + src/main/java + + **/*.xml + + + + + diff --git a/system/Readme.md b/system/Readme.md new file mode 100644 index 0000000..3d336e5 --- /dev/null +++ b/system/Readme.md @@ -0,0 +1,9 @@ +#### 简介 + 系统模块主要包含以下功能: +* 审计日志管理 +* 字典、树形字典管理 +* 版本注册 +* 主题管理 +* 系统文件资源管理 +* 定时任务管理 + \ No newline at end of file diff --git a/system/pom.xml b/system/pom.xml new file mode 100644 index 0000000..d0a7a61 --- /dev/null +++ b/system/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + com.njcn.gather + CN_Tool + 1.0.0 + + system + + + com.njcn + njcn-common + 0.0.1 + + + + com.njcn + mybatis-plus + 0.0.1 + + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + + com.njcn.gather + user + 1.0.0 + + + com.alibaba + fastjson + 1.2.83 + + + + + + + + + + + + + + + + diff --git a/system/src/main/java/com/njcn/gather/system/cfg/controller/SysTestConfigController.java b/system/src/main/java/com/njcn/gather/system/cfg/controller/SysTestConfigController.java new file mode 100644 index 0000000..784af70 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/controller/SysTestConfigController.java @@ -0,0 +1,80 @@ +package com.njcn.gather.system.cfg.controller; + +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.cfg.pojo.param.SysTestConfigParam; +import com.njcn.gather.system.cfg.pojo.po.SysTestConfig; +import com.njcn.gather.system.cfg.service.ISysTestConfigService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + + +/** + * @author caozehui + * @date 2024-11-16 + */ +@Slf4j +@Api(tags = "检测相关配置") +@RestController +@RequestMapping("/sysTestConfig") +@RequiredArgsConstructor +public class SysTestConfigController extends BaseController { + private final ISysTestConfigService sysTestConfigService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getConfig") + @ApiOperation("获取检测相关配置信息") + public HttpResult getConfig() { + String methodDescribe = getMethodDescribe("getConfig"); + LogUtil.njcnDebug(log, "{}", methodDescribe); + SysTestConfig sysTestConfig = sysTestConfigService.getOneConfig(); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, sysTestConfig, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改检测相关配置信息") + @ApiImplicitParam(name = "sysTestConfig", value = "检测相关配置信息", required = true) + public HttpResult update(@RequestBody @Validated SysTestConfigParam.UpdateParam sysTestConfig) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{}", methodDescribe); + boolean result = sysTestConfigService.updateTestConfig(sysTestConfig); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @ApiOperation("获取当前场景") + @GetMapping("/getCurrentScene") + public HttpResult getCurrentScene() { + String methodDescribe = getMethodDescribe("getCurrentScene"); + LogUtil.njcnDebug(log, "{}", methodDescribe); + String currrentScene = sysTestConfigService.getCurrrentScene(); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, currrentScene, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @ApiOperation("获取是否在检测时同时生成报告") + @GetMapping("/getAutoGenerate") + public HttpResult getAutoGenerate() { + String methodDescribe = getMethodDescribe("getAutoGenerate"); + LogUtil.njcnDebug(log, "{}", methodDescribe); + Integer autoGenerate = sysTestConfigService.getAutoGenerate(); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, autoGenerate, methodDescribe); + } +} + diff --git a/system/src/main/java/com/njcn/gather/system/cfg/mapper/SysTestConfigMapper.java b/system/src/main/java/com/njcn/gather/system/cfg/mapper/SysTestConfigMapper.java new file mode 100644 index 0000000..54d8a5a --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/mapper/SysTestConfigMapper.java @@ -0,0 +1,13 @@ +package com.njcn.gather.system.cfg.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.system.cfg.pojo.po.SysTestConfig; + +/** + * @author caozehui + * @date 2024-11-16 + */ +public interface SysTestConfigMapper extends MPJBaseMapper { + +} + diff --git a/system/src/main/java/com/njcn/gather/system/cfg/mapper/mapping/SysTestConfigMapper.xml b/system/src/main/java/com/njcn/gather/system/cfg/mapper/mapping/SysTestConfigMapper.xml new file mode 100644 index 0000000..17fd037 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/mapper/mapping/SysTestConfigMapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/system/src/main/java/com/njcn/gather/system/cfg/pojo/enums/SceneEnum.java b/system/src/main/java/com/njcn/gather/system/cfg/pojo/enums/SceneEnum.java new file mode 100644 index 0000000..02c2ebc --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/pojo/enums/SceneEnum.java @@ -0,0 +1,42 @@ +package com.njcn.gather.system.cfg.pojo.enums; + +import lombok.Getter; + +/** + * @author caozehui + * @data 2025-03-25 + */ +@Getter +public enum SceneEnum { + /** + * 省级平台 + */ + PROVINCE_PLATFORM("0", "province_platform"), + + /** + * 设备出场 + */ + LEAVE_FACTORY_TEST("1", "leave_factory_test"), + + /** + * 研发自测 + */ + SELF_TEST("2", "self_test"); + + private String value; + private String msg; + + SceneEnum(String value, String msg) { + this.value = value; + this.msg = msg; + } + + public static SceneEnum getSceneEnum(String value) { + for (SceneEnum sceneEnum : SceneEnum.values()) { + if (sceneEnum.getValue().equals(value)) { + return sceneEnum; + } + } + return null; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/cfg/pojo/param/SysTestConfigParam.java b/system/src/main/java/com/njcn/gather/system/cfg/pojo/param/SysTestConfigParam.java new file mode 100644 index 0000000..89de591 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/pojo/param/SysTestConfigParam.java @@ -0,0 +1,40 @@ +package com.njcn.gather.system.cfg.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.system.pojo.constant.SystemValidMessage; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.*; + +/** + * @author caozehui + * @data 2024/11/16 + */ +@Data +public class SysTestConfigParam { + + @ApiModelProperty(value = "检测报告是否自动生成0 否;1是") + @Min(value = 0, message = SystemValidMessage.AUTO_GENERATE_FORMAT_ERROR) + @Max(value = 1, message = SystemValidMessage.AUTO_GENERATE_FORMAT_ERROR) + private Integer autoGenerate; + + @ApiModelProperty(value = "最大检测次数") + private Integer maxTime; + + @ApiModelProperty(value = "数据精度") + private Integer scale; + + @ApiModelProperty(value = "场景") + private String scene; + + @ApiModelProperty(value = "比对监测后,当电压、电流不符合时,是否对标准设备进行系数校准") + private Integer coefficient; + + @Data + public static class UpdateParam extends SysTestConfigParam { + @ApiModelProperty("id") + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.ID_FORMAT_ERROR) + private String id; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/cfg/pojo/po/SysTestConfig.java b/system/src/main/java/com/njcn/gather/system/cfg/pojo/po/SysTestConfig.java new file mode 100644 index 0000000..f881bee --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/pojo/po/SysTestConfig.java @@ -0,0 +1,65 @@ +package com.njcn.gather.system.cfg.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * @author caozehui + * @date 2024-11-16 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_test_config") +public class SysTestConfig extends BaseEntity implements Serializable { + private static final long serialVersionUID = 352471858515754310L; + /** + * 系统配置表Id + */ + private String id; + + /** + * 检测报告是否自动生成: 0 否;1 是 + */ + @TableField("Auto_Generate") + private Integer autoGenerate; + + /** + * 最大检测次数,默认3次 + */ + @TableField("Max_Time") + private Integer maxTime; + + /** + * 数据处理规则, 关联字典(所有值、部分值、cp95值、平均值、任意值),默认任意值 + */ +// @TableField("Data_Rule") +// private String dataRule; + + /** + * 业务场景 + */ + @TableField("Scene") + private String scene; + + /** + * 小数点精度 + */ + private Integer scale; + + /** + * 比对监测后,当电压、电流不符合时,是否对标准设备进行系数校准 + */ + private Integer coefficient; + + + /** + * 状态:0-删除 1-正常 + */ + private Integer state; +} + diff --git a/system/src/main/java/com/njcn/gather/system/cfg/service/ISysTestConfigService.java b/system/src/main/java/com/njcn/gather/system/cfg/service/ISysTestConfigService.java new file mode 100644 index 0000000..c490339 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/service/ISysTestConfigService.java @@ -0,0 +1,41 @@ +package com.njcn.gather.system.cfg.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.cfg.pojo.param.SysTestConfigParam; +import com.njcn.gather.system.cfg.pojo.po.SysTestConfig; + +/** + * @author caozehui + * @date 2024-11-16 + */ +public interface ISysTestConfigService extends IService { + + /** + * 添加检测配置 + * @param scene 场景 + * @return 是否添加成功 + */ + boolean addTestConfig(String scene); + + /** + * 更新检测配置 + * @param param 检测配置 + * @return 是否更新成功 + */ + boolean updateTestConfig(SysTestConfigParam.UpdateParam param); + + /** + * 获取检测配置 + * @return + */ + SysTestConfig getOneConfig(); + + String getCurrrentScene(); + + /** + * 获取是否在检测时自动生成报告 + * + * @return 0-否,1-是 + */ + Integer getAutoGenerate(); +} diff --git a/system/src/main/java/com/njcn/gather/system/cfg/service/impl/SysTestConfigServiceImpl.java b/system/src/main/java/com/njcn/gather/system/cfg/service/impl/SysTestConfigServiceImpl.java new file mode 100644 index 0000000..8383266 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/cfg/service/impl/SysTestConfigServiceImpl.java @@ -0,0 +1,78 @@ +package com.njcn.gather.system.cfg.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.gather.system.cfg.mapper.SysTestConfigMapper; +import com.njcn.gather.system.cfg.pojo.param.SysTestConfigParam; +import com.njcn.gather.system.cfg.pojo.po.SysTestConfig; +import com.njcn.gather.system.cfg.service.ISysTestConfigService; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.gather.system.dictionary.service.IDictDataService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author caozehui + * @date 2024-11-16 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysTestConfigServiceImpl extends ServiceImpl implements ISysTestConfigService { + + private final IDictDataService dictDataService; + + + @Override + @Transactional + public boolean addTestConfig(String scene) { + SysTestConfig sysTestConfig = new SysTestConfig(); + sysTestConfig.setAutoGenerate(1); + // 最大被检次数默认为3次 + sysTestConfig.setMaxTime(3); + //sysTestConfig.setDataRule("46cf964bd76fb12a19cfb1700442eeeb"); // 任意值 + sysTestConfig.setScene(scene); + sysTestConfig.setState(DataStateEnum.ENABLE.getCode()); + return this.save(sysTestConfig); + } + + @Override + @Transactional + public boolean updateTestConfig(SysTestConfigParam.UpdateParam param) { + SysTestConfig oneConfig = this.getOneConfig(); + oneConfig.setAutoGenerate(ObjectUtil.isNotNull(param.getAutoGenerate()) ? param.getAutoGenerate() : oneConfig.getAutoGenerate()); + oneConfig.setScale(ObjectUtil.isNotNull(param.getScale()) ? param.getScale() : oneConfig.getScale()); + oneConfig.setMaxTime(ObjectUtil.isNotNull(param.getMaxTime()) ? param.getMaxTime() : oneConfig.getMaxTime()); + oneConfig.setScene(StringUtils.isNotBlank(param.getScene()) ? param.getScene() : oneConfig.getScene()); + oneConfig.setCoefficient(param.getCoefficient()); + return this.updateById(oneConfig); + } + + @Override + public SysTestConfig getOneConfig() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("state", DataStateEnum.ENABLE.getCode()); + queryWrapper.last("LIMIT 1"); + return this.getOne(queryWrapper); + } + + @Override + public String getCurrrentScene() { + String scene = getOneConfig().getScene(); + DictData dictData = dictDataService.getDictDataById(scene); + if (ObjectUtil.isNotNull(dictData)) { + return dictData.getValue(); + } + return null; + } + + @Override + public Integer getAutoGenerate() { + return getOneConfig().getAutoGenerate(); + } +} diff --git a/system/src/main/java/com/njcn/gather/system/config/LogExecutorConfig.java b/system/src/main/java/com/njcn/gather/system/config/LogExecutorConfig.java new file mode 100644 index 0000000..215a7a5 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/config/LogExecutorConfig.java @@ -0,0 +1,31 @@ +package com.njcn.gather.system.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@Configuration +public class LogExecutorConfig { + + @Bean(name = "logAuditExecutor", destroyMethod = "shutdown") + public ExecutorService logAuditExecutor() { + AtomicInteger threadIndex = new AtomicInteger(1); + return new ThreadPoolExecutor( + 4, 8, 30, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), + runnable -> { + Thread thread = new Thread(runnable); + thread.setName("log-audit-" + threadIndex.getAndIncrement()); + return thread; + }, + (runnable, executor) -> log.warn("审计日志线程池已满,丢弃本次日志任务") + ); + } +} diff --git a/system/src/main/java/com/njcn/gather/system/config/WebConfig.java b/system/src/main/java/com/njcn/gather/system/config/WebConfig.java new file mode 100644 index 0000000..6a56e2b --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/config/WebConfig.java @@ -0,0 +1,45 @@ +package com.njcn.gather.system.config; + +import cn.hutool.extra.spring.SpringUtil; +import com.njcn.common.bean.CustomCacheUtil; +import org.springframework.boot.web.servlet.MultipartConfigFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.unit.DataSize; + +import javax.servlet.MultipartConfigElement; + +/** + * @author caozehui + * @data 2025-03-24 + */ +@Configuration +public class WebConfig { + + /** + * 将自定缓存工具类注入到spring容器中 + * + * @return + */ + @Bean + public CustomCacheUtil customCacheUtil() { + CustomCacheUtil customCacheUtil = SpringUtil.getBean(CustomCacheUtil.CACHE_NAME); + return customCacheUtil; + } + + /** + * 配置上传文件大小限制 + * + * @return + */ + @Bean + public MultipartConfigElement multipartConfigElement() { + MultipartConfigFactory factory = new MultipartConfigFactory(); + // 单个文件最大6MB + factory.setMaxFileSize(DataSize.ofMegabytes(1024)); + // 整个请求最大12MB + factory.setMaxRequestSize(DataSize.ofMegabytes(2048)); + return factory.createMultipartConfig(); + } + +} diff --git a/system/src/main/java/com/njcn/gather/system/config/advice/LogAdvice.java b/system/src/main/java/com/njcn/gather/system/config/advice/LogAdvice.java new file mode 100644 index 0000000..f690a75 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/config/advice/LogAdvice.java @@ -0,0 +1,182 @@ +package com.njcn.gather.system.config.advice; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.gather.system.log.pojo.dto.SysLogAuditRecord; +import com.njcn.gather.system.log.service.ISysLogAuditService; +import com.njcn.web.utils.ReflectCommonUtil; +import com.njcn.web.utils.RequestUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import javax.annotation.Nonnull; +import javax.annotation.Resource; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * @author caozehui + * @data 2024-12-2 + */ +@Slf4j +@ControllerAdvice +public class LogAdvice implements ResponseBodyAdvice { + + @Resource + private ISysLogAuditService logService; + + @Resource(name = "logAuditExecutor") + private Executor logAuditExecutor; + + private static final List UN_LOG_INFO = Collections.singletonList("未知业务"); + + private static final List FILTER_CODE = Arrays.asList( + CommonResponseEnum.SUCCESS.getCode(), + CommonResponseEnum.FAIL.getCode(), + CommonResponseEnum.NO_DATA.getCode() + ); + + @Override + public boolean supports(MethodParameter returnType, Class converterType) { + return true; + } + + @Override + public Object beforeBodyWrite(Object body, @Nonnull MethodParameter returnType, @Nonnull MediaType selectedContentType, + @Nonnull Class selectedConverterType, @Nonnull ServerHttpRequest request, + @Nonnull ServerHttpResponse response) { + if (body instanceof HttpResult) { + HttpResult httpResult = (HttpResult) body; + if (FILTER_CODE.contains(httpResult.getCode())) { + Method method = returnType.getMethod(); + String methodDescribe = resolveMethodDescribe(method); + if (!UN_LOG_INFO.contains(methodDescribe)) { + SysLogAuditRecord logRecord = buildAdviceLogRecord(method, httpResult, methodDescribe); + submitLogTask(() -> logService.recodeAdviceLog(logRecord)); + } + } + } + return body; + } + + private SysLogAuditRecord buildAdviceLogRecord(Method method, HttpResult httpResult, String methodDescribe) { + Integer level = resolveOperateLevel(method); + return SysLogAuditRecord.builder() + .userId(resolveUserId()) + .loginName(resolveLoginName()) + .ip(resolveUserIp()) + .operate(methodDescribe) + .operateType(resolveOperateType(method)) + .result(CommonResponseEnum.FAIL.getCode().equalsIgnoreCase(httpResult.getCode()) + ? CommonResponseEnum.FAIL.getMessage() + : CommonResponseEnum.SUCCESS.getMessage()) + .type(resolveEventType(method)) + .level(level) + .warn(level == 1 ? 1 : 0) + .build(); + } + + private void submitLogTask(Runnable task) { + try { + logAuditExecutor.execute(() -> { + try { + task.run(); + } catch (Exception e) { + log.error("异步记录审计日志失败", e); + } + }); + } catch (RuntimeException e) { + log.error("提交审计日志任务失败", e); + } + } + + private String resolveMethodDescribe(Method method) { + if (method == null) { + return "未知业务"; + } + try { + String methodDescribe = ReflectCommonUtil.getMethodDescribeByMethod(method); + return StrUtil.isBlank(methodDescribe) ? "未知业务" : methodDescribe; + } catch (Exception e) { + log.warn("解析审计日志方法描述失败,method={}", method.getName(), e); + return "未知业务"; + } + } + + private Integer resolveEventType(Method method) { + try { + if (method == null) { + return 1; + } + String type = ReflectCommonUtil.getOperateInfoByMethod(method).getOperateType(); + return "业务事件".equalsIgnoreCase(type) ? 0 : 1; + } catch (Exception e) { + return 1; + } + } + + private Integer resolveOperateLevel(Method method) { + try { + if (method == null) { + return 0; + } + String level = ReflectCommonUtil.getOperateInfoByMethod(method).getOperateLevel(); + if ("中等".equals(level)) { + return 1; + } + if ("严重".equals(level)) { + return 2; + } + } catch (Exception e) { + return 0; + } + return 0; + } + + private String resolveOperateType(Method method) { + try { + if (method == null) { + return ""; + } + return ReflectCommonUtil.getOperateTypeByMethod(method); + } catch (Exception e) { + return ""; + } + } + + private String resolveUserId() { + try { + String userId = RequestUtil.getUserId(); + return StrUtil.isBlank(userId) ? "" : userId; + } catch (Exception e) { + return ""; + } + } + + private String resolveLoginName() { + try { + String loginName = RequestUtil.getLoginName(); + return StrUtil.isBlank(loginName) ? "" : loginName; + } catch (Exception e) { + return ""; + } + } + + private String resolveUserIp() { + try { + String userIp = RequestUtil.getUserIp(); + return StrUtil.isBlank(userIp) ? "" : userIp; + } catch (Exception e) { + return ""; + } + } +} diff --git a/system/src/main/java/com/njcn/gather/system/config/handler/ControllerUtil.java b/system/src/main/java/com/njcn/gather/system/config/handler/ControllerUtil.java new file mode 100644 index 0000000..1fffc7f --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/config/handler/ControllerUtil.java @@ -0,0 +1,37 @@ +package com.njcn.gather.system.config.handler; + +import com.njcn.common.pojo.constant.LogInfo; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * @author hongawen + * @version 1.0.0 + * @date 2021年06月22日 10:25 + */ +@Slf4j +public class ControllerUtil { + + /** + * 针对methodArgumentNotValidException 异常的处理 + * @author cdf + */ + public static String getMethodArgumentNotValidException(MethodArgumentNotValidException methodArgumentNotValidException) { + String operate = LogInfo.UNKNOWN_OPERATE; + Method method = null; + try { + method = methodArgumentNotValidException.getParameter().getMethod(); + if (!Objects.isNull(method) && method.isAnnotationPresent(ApiOperation.class)) { + ApiOperation apiOperation = method.getAnnotation(ApiOperation.class); + operate = apiOperation.value(); + } + }catch (Exception e){ + log.error("根据方法参数非法异常获取@ApiOperation注解值失败,参数非法异常信息:{},方法名:{},异常信息:{}",methodArgumentNotValidException.getMessage(),method,e.getMessage()); + } + return operate; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/config/handler/GlobalBusinessExceptionHandler.java b/system/src/main/java/com/njcn/gather/system/config/handler/GlobalBusinessExceptionHandler.java new file mode 100644 index 0000000..7a8bf48 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/config/handler/GlobalBusinessExceptionHandler.java @@ -0,0 +1,335 @@ +package com.njcn.gather.system.config.handler; + +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.log.pojo.dto.SysLogAuditRecord; +import com.njcn.gather.system.log.service.ISysLogAuditService; +import com.njcn.gather.system.pojo.enums.SystemResponseEnum; +import com.njcn.web.utils.HttpResultUtil; +import com.njcn.web.utils.ReflectCommonUtil; +import com.njcn.web.utils.RequestUtil; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONException; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.util.NestedServletException; + +import javax.annotation.Resource; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.NoSuchFileException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 全局通用业务异常处理器 + * + * @author hongawen + * @version 1.0.0 + * @date 2021年04月20日 18:04 + */ +@Slf4j +@RestControllerAdvice +public class GlobalBusinessExceptionHandler { + + @Resource + private ISysLogAuditService sysLogAuditService; + + @Resource(name = "logAuditExecutor") + private Executor logAuditExecutor; + + @ExceptionHandler(BusinessException.class) + public HttpResult handleBusinessException(BusinessException businessException) { + String operate = resolveMethodDescribeByException(businessException); + recodeBusinessExceptionLog(businessException, businessException.getMessage()); + return HttpResultUtil.assembleBusinessExceptionResult(businessException, null, operate); + } + + @ExceptionHandler(NullPointerException.class) + public HttpResult handleNullPointerException(NullPointerException nullPointerException) { + LogUtil.logExceptionStackInfo(CommonResponseEnum.NULL_POINTER_EXCEPTION.getMessage(), nullPointerException); + recodeBusinessExceptionLog(nullPointerException, CommonResponseEnum.NULL_POINTER_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.NULL_POINTER_EXCEPTION, null, resolveMethodDescribeByException(nullPointerException)); + } + + @ExceptionHandler(ArithmeticException.class) + public HttpResult handleArithmeticException(ArithmeticException arithmeticException) { + LogUtil.logExceptionStackInfo(CommonResponseEnum.ARITHMETIC_EXCEPTION.getMessage(), arithmeticException); + recodeBusinessExceptionLog(arithmeticException, CommonResponseEnum.ARITHMETIC_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.ARITHMETIC_EXCEPTION, null, resolveMethodDescribeByException(arithmeticException)); + } + + @ExceptionHandler(ClassCastException.class) + public HttpResult handleClassCastException(ClassCastException classCastException) { + LogUtil.logExceptionStackInfo(CommonResponseEnum.CLASS_CAST_EXCEPTION.getMessage(), classCastException); + recodeBusinessExceptionLog(classCastException, CommonResponseEnum.CLASS_CAST_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.CLASS_CAST_EXCEPTION, null, resolveMethodDescribeByException(classCastException)); + } + + @ExceptionHandler(IndexOutOfBoundsException.class) + public HttpResult handleIndexOutOfBoundsException(IndexOutOfBoundsException indexOutOfBoundsException) { + LogUtil.logExceptionStackInfo(CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION.getMessage(), indexOutOfBoundsException); + recodeBusinessExceptionLog(indexOutOfBoundsException, CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION, null, resolveMethodDescribeByException(indexOutOfBoundsException)); + } + + @ExceptionHandler(NoSuchFileException.class) + public HttpResult handleNoSuchFileException(NoSuchFileException noSuchFileException) { + String filePath = noSuchFileException.getFile(); + log.warn("文件未找到异常 - 文件路径: {}", filePath, noSuchFileException); + recodeBusinessExceptionLog(noSuchFileException, SystemResponseEnum.FILE_NOT_FOUND.getMessage()); + return HttpResultUtil.assembleResult(SystemResponseEnum.FILE_NOT_FOUND.getCode(), null, + StrFormatter.format("{}{}{}", resolveMethodDescribeByException(noSuchFileException), + StrUtil.C_COMMA, SystemResponseEnum.FILE_NOT_FOUND.getMessage())); + } + + @ExceptionHandler(IOException.class) + public HttpResult handleIOException(IOException ioException) { + if (ioException instanceof NoSuchFileException) { + return handleNoSuchFileException((NoSuchFileException) ioException); + } + LogUtil.logExceptionStackInfo(SystemResponseEnum.FILE_IO_ERROR.getMessage(), ioException); + recodeBusinessExceptionLog(ioException, SystemResponseEnum.FILE_IO_ERROR.getMessage()); + return HttpResultUtil.assembleResult(SystemResponseEnum.FILE_IO_ERROR.getCode(), null, + StrFormatter.format("{}{}{}", resolveMethodDescribeByException(ioException), + StrUtil.C_COMMA, SystemResponseEnum.FILE_IO_ERROR.getMessage())); + } + + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public HttpResult httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException httpMediaTypeNotSupportedException) { + LogUtil.logExceptionStackInfo(CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION.getMessage(), httpMediaTypeNotSupportedException); + recodeBusinessExceptionLog(httpMediaTypeNotSupportedException, CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION, null, resolveMethodDescribeByException(httpMediaTypeNotSupportedException)); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public HttpResult methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException methodArgumentNotValidException) { + String messages = methodArgumentNotValidException.getBindingResult().getAllErrors() + .stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(";")); + LogUtil.njcnDebug(log, "参数校验异常,异常为:{}", messages); + recodeBusinessExceptionLog(methodArgumentNotValidException, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, messages, ControllerUtil.getMethodArgumentNotValidException(methodArgumentNotValidException)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public HttpResult constraintViolationExceptionExceptionHandler(ConstraintViolationException constraintViolationException) { + String exceptionMessage = constraintViolationException.getMessage(); + StringBuilder messages = new StringBuilder(); + if (exceptionMessage.indexOf(StrUtil.COMMA) > 0) { + String[] tempMessage = exceptionMessage.split(StrUtil.COMMA); + Stream.of(tempMessage).forEach(message -> messages.append(message.substring(message.indexOf(StrUtil.COLON) + 2)).append(';')); + } else { + messages.append(exceptionMessage.substring(exceptionMessage.indexOf(StrUtil.COLON) + 2)); + } + LogUtil.njcnDebug(log, "参数校验异常,异常为:{}", messages); + recodeBusinessExceptionLog(constraintViolationException, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage()); + List> constraintViolationList = new ArrayList<>(constraintViolationException.getConstraintViolations()); + ConstraintViolation constraintViolation = constraintViolationList.get(0); + Class rootBeanClass = constraintViolation.getRootBeanClass(); + if (rootBeanClass.getName().endsWith("Controller")) { + String methodName = constraintViolation.getPropertyPath().toString().substring(0, constraintViolation.getPropertyPath().toString().indexOf(StrUtil.DOT)); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, messages.toString(), resolveMethodDescribeByClassAndMethodName(rootBeanClass, methodName)); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, messages.toString(), resolveMethodDescribeByException(constraintViolationException)); + } + } + + @ExceptionHandler(IllegalArgumentException.class) + public HttpResult handleIndexOutOfBoundsException(IllegalArgumentException illegalArgumentException) { + LogUtil.logExceptionStackInfo(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage(), illegalArgumentException); + recodeBusinessExceptionLog(illegalArgumentException, CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION, illegalArgumentException.getMessage(), resolveMethodDescribeByException(illegalArgumentException)); + } + + @ExceptionHandler(Exception.class) + public HttpResult handleException(Exception exception) { + Exception tempException = exception; + String exceptionCause = CommonResponseEnum.UN_DECLARE.getMessage(); + String code = CommonResponseEnum.UN_DECLARE.getCode(); + if (exception instanceof NestedServletException) { + Throwable cause = exception.getCause(); + if (cause instanceof AssertionError) { + if (cause.getCause() instanceof BusinessException) { + tempException = (BusinessException) cause.getCause(); + BusinessException tempBusinessException = (BusinessException) cause.getCause(); + exceptionCause = tempBusinessException.getMessage(); + code = tempBusinessException.getCode(); + } + } + } + LogUtil.logExceptionStackInfo(exceptionCause, tempException); + recodeBusinessExceptionLog(exception, exceptionCause); + return HttpResultUtil.assembleResult(code, null, StrFormatter.format("{}{}{}", resolveMethodDescribeByException(tempException), StrUtil.C_COMMA, exceptionCause)); + } + + @ExceptionHandler(JSONException.class) + public HttpResult handleIndexOutOfBoundsException(JSONException jsonException) { + LogUtil.logExceptionStackInfo(CommonResponseEnum.JSON_CONVERT_EXCEPTION.getMessage(), jsonException); + recodeBusinessExceptionLog(jsonException, CommonResponseEnum.JSON_CONVERT_EXCEPTION.getMessage()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.JSON_CONVERT_EXCEPTION, jsonException.getMessage(), resolveMethodDescribeByException(jsonException)); + } + + private void recodeBusinessExceptionLog(Exception businessException, String message) { + SysLogAuditRecord logRecord = buildExceptionLogRecord(businessException, message); + submitLogTask(() -> sysLogAuditService.recodeBusinessExceptionLog(logRecord)); + } + + private SysLogAuditRecord buildExceptionLogRecord(Exception exception, String message) { + Method method = resolveMethod(exception); + Integer level = resolveOperateLevel(method); + return SysLogAuditRecord.builder() + .userId(resolveUserId()) + .loginName(resolveLoginName()) + .ip(resolveUserIp()) + .operate(resolveExceptionOperate(method, exception)) + .operateType(resolveOperateType(method)) + .result(CommonResponseEnum.FAIL.getMessage()) + .reason(message) + .type(resolveEventType(method)) + .level(level) + .warn(level == 1 ? 1 : 0) + .build(); + } + + private Method resolveMethod(Exception exception) { + if (exception instanceof MethodArgumentNotValidException) { + MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) exception; + return methodArgumentNotValidException.getParameter().getMethod(); + } + try { + return ReflectCommonUtil.getMethod(exception); + } catch (Exception e) { + return null; + } + } + + private String resolveExceptionOperate(Method method, Exception exception) { + if (method != null) { + try { + String methodDescribe = ReflectCommonUtil.getMethodDescribeByMethod(method); + if (StrUtil.isNotBlank(methodDescribe)) { + return methodDescribe; + } + } catch (Exception e) { + log.warn("解析异常日志方法描述失败,method={}", method.getName(), e); + } + } + try { + return resolveMethodDescribeByException(exception); + } catch (Exception e) { + return "未知业务"; + } + } + + private Integer resolveEventType(Method method) { + try { + if (method == null) { + return 1; + } + String type = ReflectCommonUtil.getOperateInfoByMethod(method).getOperateType(); + return "业务事件".equalsIgnoreCase(type) ? 0 : 1; + } catch (Exception e) { + return 1; + } + } + + private Integer resolveOperateLevel(Method method) { + try { + if (method == null) { + return 0; + } + String level = ReflectCommonUtil.getOperateInfoByMethod(method).getOperateLevel(); + if ("中等".equals(level)) { + return 1; + } + if ("严重".equals(level)) { + return 2; + } + } catch (Exception e) { + return 0; + } + return 0; + } + + private String resolveOperateType(Method method) { + try { + if (method == null) { + return ""; + } + return ReflectCommonUtil.getOperateTypeByMethod(method); + } catch (Exception e) { + return ""; + } + } + + private String resolveMethodDescribeByException(Exception exception) { + try { + String methodDescribe = ReflectCommonUtil.getMethodDescribeByException(exception); + return StrUtil.isBlank(methodDescribe) ? "未知业务" : methodDescribe; + } catch (Exception e) { + return "未知业务"; + } + } + + private String resolveMethodDescribeByClassAndMethodName(Class rootBeanClass, String methodName) { + try { + String methodDescribe = ReflectCommonUtil.getMethodDescribeByClassAndMethodName(rootBeanClass, methodName); + return StrUtil.isBlank(methodDescribe) ? "未知业务" : methodDescribe; + } catch (Exception e) { + return "未知业务"; + } + } + + private String resolveUserId() { + try { + String userId = RequestUtil.getUserId(); + return StrUtil.isBlank(userId) ? "" : userId; + } catch (Exception e) { + return ""; + } + } + + private String resolveLoginName() { + try { + String loginName = RequestUtil.getLoginName(); + return StrUtil.isBlank(loginName) ? "" : loginName; + } catch (Exception e) { + return ""; + } + } + + private String resolveUserIp() { + try { + String userIp = RequestUtil.getUserIp(); + return StrUtil.isBlank(userIp) ? "" : userIp; + } catch (Exception e) { + return ""; + } + } + + private void submitLogTask(Runnable task) { + try { + logAuditExecutor.execute(() -> { + try { + task.run(); + } catch (Exception e) { + log.error("异步记录异常审计日志失败", e); + } + }); + } catch (RuntimeException e) { + log.error("提交异常审计日志任务失败", e); + } + } +} diff --git a/system/src/main/java/com/njcn/gather/system/config/handler/NonWebAutoFillValueHandler.java b/system/src/main/java/com/njcn/gather/system/config/handler/NonWebAutoFillValueHandler.java new file mode 100644 index 0000000..810c2e9 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/config/handler/NonWebAutoFillValueHandler.java @@ -0,0 +1,65 @@ +package com.njcn.gather.system.config.handler; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.db.mybatisplus.handler.AutoFillValueHandler; +import com.njcn.web.utils.RequestUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import java.util.function.Supplier; + +@Primary +@Component +@Slf4j +public class NonWebAutoFillValueHandler extends AutoFillValueHandler { + + /** + * 当前用户ID的线程本地变量,用于非Web环境 + */ + private static final ThreadLocal CURRENT_USER_ID = new ThreadLocal<>(); + + + @Override + public Supplier getUserIdSupplier() { + return () -> { + try { + // 首先尝试从Web环境获取用户ID + String userId = RequestUtil.getUserId(); + String actualUserId = StrUtil.isBlank(userId) ? "未知用户" : userId; + return actualUserId; + } catch (BusinessException e) { + // 如果是"当前请求web环境为空"异常,则尝试从线程本地变量获取 + if (e.getMessage().contains("当前请求web环境为空")) { + String userId = CURRENT_USER_ID.get(); + if (userId != null) { + return userId; + } + // 如果线程本地变量中也没有用户ID,则返回默认值 + log.warn("无法获取当前用户ID"); + return "未知用户"; + } + // 其他异常直接抛出 + throw e; + } + }; + } + + /** + * 在非Web环境中设置当前用户ID + * + * @param userId 用户ID + */ + public static void setCurrentUserId(String userId) { + CURRENT_USER_ID.set(userId); + } + + /** + * 清除当前线程的用户ID设置 + */ + public static void clearCurrentUserId() { + CURRENT_USER_ID.remove(); + } + +} \ No newline at end of file diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictDataController.java b/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictDataController.java new file mode 100644 index 0000000..3682091 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictDataController.java @@ -0,0 +1,141 @@ +package com.njcn.gather.system.dictionary.controller; + + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.dictionary.pojo.param.DictDataParam; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.gather.system.dictionary.service.IDictDataService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.pojo.dto.SimpleTreeDTO; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author hongawen + * @since 2021-12-13 + */ +@Validated +@Slf4j +@Api(tags = "字典数据操作") +@RestController +@RequestMapping("/dictData") +@RequiredArgsConstructor +public class DictDataController extends BaseController { + + private final IDictDataService dictDataService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @PostMapping("/listByTypeId") + @ApiOperation("根据字典类型id查询字典数据") + @ApiImplicitParam(name = "queryParam", value = "查询参数", required = true) + public HttpResult> listByTypeId(@RequestBody @Validated DictDataParam.QueryParam queryParam) { + String methodDescribe = getMethodDescribe("listByTypeId"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, queryParam); + Page result = dictDataService.getDictDataByTypeId(queryParam); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) + @PostMapping("/add") + @ApiOperation("新增字典数据") + @ApiImplicitParam(name = "dictDataParam", value = "字典数据", required = true) + public HttpResult add(@RequestBody @Validated DictDataParam dictDataParam) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},字典数据为:{}", methodDescribe, dictDataParam); + boolean result = dictDataService.addDictData(dictDataParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改字典数据") + @ApiImplicitParam(name = "updateParam", value = "字典数据", required = true) + public HttpResult update(@RequestBody @Validated DictDataParam.UpdateParam updateParam) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{},字典数据为:{}", methodDescribe, updateParam); + boolean result = dictDataService.updateDictData(updateParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/delete") + @ApiOperation("删除字典数据") + @ApiImplicitParam(name = "ids", value = "字典索引", required = true, dataTypeClass = List.class) + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + LogUtil.njcnDebug(log, "{},字典ID数据为:{}", methodDescribe, String.join(StrUtil.COMMA, ids)); + boolean result = dictDataService.deleteDictData(ids); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getDicDataById") + @ApiOperation("根据字典id查询字典数据") + @ApiImplicitParam(name = "dicIndex", value = "字典id", required = true) + public HttpResult getDicDataById(@RequestParam("dicIndex") String dicIndex) { + String methodDescribe = getMethodDescribe("getDicDataById"); + DictData result = dictDataService.getDictDataById(dicIndex); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getDicDataByCode") + @ApiOperation("根据字典code查询字典数据") + @ApiImplicitParam(name = "code", value = "字典code", required = true) + public HttpResult getDicDataByCode(@RequestParam("code") String code) { + String methodDescribe = getMethodDescribe("getDicDataByCode"); + DictData result = dictDataService.getDictDataByCode(code); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/dictDataCache") + @ApiOperation("获取所有字典数据缓存到前端") + public HttpResult> dictDataCache() { + String methodDescribe = getMethodDescribe("dictDataCache"); + LogUtil.njcnDebug(log, "{},获取所有字典数据缓存到前端", methodDescribe); + List dictData = dictDataService.dictDataCache(); + if (CollectionUtil.isNotEmpty(dictData)) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, dictData, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.NO_DATA, null, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DOWNLOAD) + @PostMapping("/export") + @ApiOperation("导出字典数据") + @ApiImplicitParam(name = "queryParam", value = "查询参数", required = true) + public void export(@RequestBody @Validated DictDataParam.QueryParam queryParam) { + dictDataService.exportDictData(queryParam); + } +} + diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictTreeController.java b/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictTreeController.java new file mode 100644 index 0000000..b0ca975 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictTreeController.java @@ -0,0 +1,113 @@ +package com.njcn.gather.system.dictionary.controller; + + +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.dictionary.pojo.param.DictTreeParam; +import com.njcn.gather.system.dictionary.pojo.po.DictTree; +import com.njcn.gather.system.dictionary.service.IDictTreeService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +/** + *

+ * 前端控制器 + *

+ * + * @author hongawen + * @since 2021-12-13 + */ +@Validated +@Slf4j +@Api(tags = "字典树操作") +@RestController +@RequestMapping("/dictTree") +@RequiredArgsConstructor +public class DictTreeController extends BaseController { + + private final IDictTreeService dictTreeService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getTreeByCode") + @ApiOperation("按照code查询字典树") + @ApiImplicitParam(name = "code", value = "查询参数", required = true) + public HttpResult> getTreeByCode(@RequestParam("code") String code) { + String methodDescribe = getMethodDescribe("getTreeByCode"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, code); + List result = dictTreeService.getTreeByCode(code); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getTreeByName") + @ApiOperation("按照name模糊查询字典树") + @ApiImplicitParam(name = "keyword", value = "查询参数", required = true) + public HttpResult> getTreeByName(@RequestParam("name") String name) { + String methodDescribe = getMethodDescribe("getTreeByName"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, name); + List result = dictTreeService.getTreeByName(name); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) + @PostMapping("/add") + @ApiOperation("新增字典树数据") + @ApiImplicitParam(name = "dictTreeParam", value = "字典数据", required = true) + public HttpResult add(@RequestBody @Validated DictTreeParam dictTreeParam) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},字典数据为:{}", methodDescribe, dictTreeParam); + boolean result = dictTreeService.addDictTree(dictTreeParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改字典树数据") + @ApiImplicitParam(name = "dicParam", value = "数据", required = true) + public HttpResult update(@RequestBody @Validated DictTreeParam.UpdateParam dicParam) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{},更新的信息为:{}", methodDescribe, dicParam); + boolean result = dictTreeService.updateDictTree(dicParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DELETE) + @PostMapping("/delete") + @ApiOperation("删除字典树数据") + @ApiImplicitParam(name = "id", value = "id", required = true) + public HttpResult delete(@RequestParam @Validated String id) { + String methodDescribe = getMethodDescribe("delete"); + LogUtil.njcnDebug(log, "{},删除的id为:{}", methodDescribe, id); + boolean result = dictTreeService.deleteDictTree(id); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } +} + + diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictTypeController.java b/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictTypeController.java new file mode 100644 index 0000000..3de62a9 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/controller/DictTypeController.java @@ -0,0 +1,117 @@ +package com.njcn.gather.system.dictionary.controller; + + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.dictionary.pojo.param.DictTypeParam; +import com.njcn.gather.system.dictionary.pojo.po.DictType; +import com.njcn.gather.system.dictionary.service.IDictTypeService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author hongawen + * @since 2021-12-13 + */ +@Slf4j +@Api(tags = "字典类型表操作") +@RestController +@RequestMapping("/dictType") +@RequiredArgsConstructor +public class DictTypeController extends BaseController { + + private final IDictTypeService dictTypeService; + + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @PostMapping("/list") + @ApiOperation("查询字典类型") + @ApiImplicitParam(name = "queryParam", value = "查询参数", required = true) + public HttpResult> list(@RequestBody @Validated DictTypeParam.QueryParam queryParam) { + String methodDescribe = getMethodDescribe("list"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, queryParam); + Page result = dictTypeService.listDictTypes(queryParam); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/listAll") + @ApiOperation("查询所有字典类型数据") + public HttpResult> listAll() { + String methodDescribe = getMethodDescribe("listAll"); + LogUtil.njcnDebug(log, "{}", methodDescribe); + List dictTypeList = dictTypeService.list(new LambdaQueryWrapper().eq(DictType::getState, DataStateEnum.ENABLE.getCode())); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, dictTypeList, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) + @PostMapping("/add") + @ApiOperation("新增字典类型") + @ApiImplicitParam(name = "dictTypeParam", value = "字典类型数据", required = true) + public HttpResult add(@RequestBody @Validated DictTypeParam dictTypeParam) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},字典类型数据为:{}", methodDescribe, dictTypeParam); + boolean result = dictTypeService.addDictType(dictTypeParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改字典类型") + @ApiImplicitParam(name = "updateParam", value = "字典类型数据", required = true) + public HttpResult update(@RequestBody @Validated DictTypeParam.UpdateParam updateParam) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{},字典类型数据为:{}", methodDescribe, updateParam); + boolean result = dictTypeService.updateDictType(updateParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DELETE) + @PostMapping("/delete") + @ApiOperation("删除字典类型") + @ApiImplicitParam(name = "ids", value = "字典索引", required = true) + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + LogUtil.njcnDebug(log, "{},字典ID数据为:{}", methodDescribe, String.join(StrUtil.COMMA, ids)); + boolean result = dictTypeService.deleteDictType(ids); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DOWNLOAD) + @PostMapping("/export") + @ApiOperation("导出字典类型数据") + @ApiImplicitParam(name = "queryParam", value = "查询参数", required = true) + public void export(@RequestBody @Validated DictTypeParam.QueryParam queryParam) { + dictTypeService.exportDictType(queryParam); + } +} + diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictDataMapper.java b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictDataMapper.java new file mode 100644 index 0000000..219c615 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictDataMapper.java @@ -0,0 +1,19 @@ +package com.njcn.gather.system.dictionary.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.system.dictionary.pojo.po.DictData; + + + +/** + *

+ * Mapper 接口 + *

+ * + * @author hongawen + * @since 2021-12-13 + */ +public interface DictDataMapper extends MPJBaseMapper { + + +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictTreeMapper.java b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictTreeMapper.java new file mode 100644 index 0000000..c94df21 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictTreeMapper.java @@ -0,0 +1,23 @@ +package com.njcn.gather.system.dictionary.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.system.dictionary.pojo.po.DictTree; +import com.njcn.gather.system.dictionary.pojo.vo.DictTreeVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author caozehui + * @data 2024/11/8 + */ +public interface DictTreeMapper extends BaseMapper { + List queryLastLevelById(@Param("id") String id); + + /** + * 获取电压相角、电流相角的id列表 + * + * @return + */ + List getPhaseAngleIds(); +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictTypeMapper.java b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictTypeMapper.java new file mode 100644 index 0000000..717cd48 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/DictTypeMapper.java @@ -0,0 +1,15 @@ +package com.njcn.gather.system.dictionary.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.system.dictionary.pojo.po.DictType; + + +/** + + * + * @author hongawen + * @since 2021-12-13 + */ +public interface DictTypeMapper extends MPJBaseMapper { + +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictDataMapper.xml b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictDataMapper.xml new file mode 100644 index 0000000..e917862 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictDataMapper.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictTreeMapper.xml b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictTreeMapper.xml new file mode 100644 index 0000000..4e34038 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictTreeMapper.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictTypeMapper.xml b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictTypeMapper.xml new file mode 100644 index 0000000..faedbfb --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/mapper/mapping/DictTypeMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/dto/DictDataCache.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/dto/DictDataCache.java new file mode 100644 index 0000000..0330456 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/dto/DictDataCache.java @@ -0,0 +1,33 @@ +package com.njcn.gather.system.dictionary.pojo.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author hongawen + * @version 1.0 + * @data 2024/10/30 15:52 + */ +@Data +public class DictDataCache implements Serializable { + + private String id; + + private String name; + + private String code; + + private String value; + + private int sort; + + private String typeId; + + private String typeName; + + private String typeCode; + + private Integer algoDescribe; + +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/enums/DictDataEnum.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/enums/DictDataEnum.java new file mode 100644 index 0000000..363c3ee --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/enums/DictDataEnum.java @@ -0,0 +1,46 @@ +package com.njcn.gather.system.dictionary.pojo.enums; + +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +/** + * @author caozehui + * @data 2024-12-12 + */ +@Getter +public enum DictDataEnum { + + /** + * Key cleanup point: only keep registration-related platform capability + * types that are still referenced by the retained activation flow. + */ + DIGITAL("数字式", "Digital"), + SIMULATE("模拟式", "Simulate"), + CONTRAST("比对式", "Contrast"); + + private final String name; + private final String code; + + DictDataEnum(String name, String code) { + this.name = name; + this.code = code; + } + + public static String getMsgByValue(Integer name) { + for (DictDataEnum state : DictDataEnum.values()) { + if (state.getName().equals(name)) { + return state.getCode(); + } + } + return null; + } + + public static DictDataEnum getDictDataEnumByCode(String code) { + for (DictDataEnum steadyIndicatorEnum : DictDataEnum.values()) { + if (StringUtils.equals(code, steadyIndicatorEnum.getCode())) { + return steadyIndicatorEnum; + } + } + return null; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictDataParam.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictDataParam.java new file mode 100644 index 0000000..f4b960c --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictDataParam.java @@ -0,0 +1,97 @@ +package com.njcn.gather.system.dictionary.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.system.pojo.constant.SystemValidMessage; +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.*; + +/** + * @author hongawen + * @version 1.0.0 + * @date 2021年12月17日 15:49 + */ +@Data +public class DictDataParam { + + + @ApiModelProperty("字典类型id") + @NotBlank(message = SystemValidMessage.DICT_TYPE_ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.DICT_TYPE_ID_FORMAT_ERROR) + private String typeId; + + + @ApiModelProperty("名称") + @NotBlank(message = SystemValidMessage.NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.DICT_DATA_NAME_REGEX, message = SystemValidMessage.DICT_DATA_NAME_FORMAT_ERROR) + private String name; + + + @ApiModelProperty("编码") + @NotBlank(message = SystemValidMessage.CODE_NOT_BLANK) + @Pattern(regexp = PatternRegex.DICT_DATA_CODE_REGEX, message = SystemValidMessage.DICT_DATA_CODE_FORMAT_ERROR) + private String code; + + + @ApiModelProperty("排序") + @NotNull(message = SystemValidMessage.SORT_NOT_NULL) + @Min(value = 1, message = SystemValidMessage.SORT_FORMAT_ERROR) + @Max(value = 999, message = SystemValidMessage.SORT_FORMAT_ERROR) + private Integer sort; + + + @ApiModelProperty("事件等级:0-普通;1-中等;2-严重(默认为0)") + private Integer level; + + @ApiModelProperty("与高级算法内部Id描述对应") + private Integer algoDescribe; + + //todo 待定 + @ApiModelProperty("字典值") + private String value; + + /** + * 是否开启使用Value值:0-关闭;1-开启(默认为0) + */ + @ApiModelProperty("是否开启使用Value值") + private Integer openValue; + + + /** + * 更新操作实体 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static class UpdateParam extends DictDataParam { + + /** + * 表Id + */ + @ApiModelProperty("id") + @NotBlank(message = SystemValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.ID_FORMAT_ERROR) + private String id; + } + + /** + * 根据字典类型id分页查询字典数据 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("字典类型id") + @NotBlank(message = SystemValidMessage.DICT_TYPE_ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.DICT_TYPE_ID_FORMAT_ERROR) + private String typeId; + + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("编码") + private String code; + } + +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictTreeParam.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictTreeParam.java new file mode 100644 index 0000000..2fb6396 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictTreeParam.java @@ -0,0 +1,81 @@ +package com.njcn.gather.system.dictionary.pojo.param; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.system.pojo.constant.SystemValidMessage; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * @author caozehui + * @data 2024/11/8 + */ +@Data +public class DictTreeParam { + /** + * 父id + */ + @ApiModelProperty(value = "父id") + private String pid; + + + /** + * 名称 + */ + @ApiModelProperty(value = "名称") + @NotBlank(message = SystemValidMessage.NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.DICT_NAME_REGEX, message = SystemValidMessage.DICT_TYPE_NAME_FORMAT_ERROR) + private String name; + + /** + * 编码 + */ + @ApiModelProperty(value = "编码") + @TableField(value = "编码") + @NotBlank(message = SystemValidMessage.CODE_NOT_BLANK) + @Pattern(regexp = PatternRegex.DICT_CODE_REGEX, message = SystemValidMessage.DICT_TYPE_CODE_FORMAT_ERROR) + private String code; + + /** + * 用于区分多种类型的字典树 0.台账对象类型 1.自定义报表指标类型 + */ + @ApiModelProperty(value = "0.台账对象类型 1.自定义报表指标类型") + private Integer type; + + /** + * 根据type自定义内容,type:0用于区分对象类型是101电网侧 102用户侧 + */ + @ApiModelProperty(value = "根据type自定义内容,type:0用于区分对象类型是101电网侧 102用户侧") + private String extend; + + /** + * 排序 + */ + @ApiModelProperty(value = "排序") + private Integer sort; + + /** + * 描述 + */ + @ApiModelProperty(value = "描述") + private String remark; + + + /** + * 更新操作实体 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static class UpdateParam extends DictTreeParam { + + + @ApiModelProperty("id") + @NotBlank(message = SystemValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.ID_FORMAT_ERROR) + private String id; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictTypeParam.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictTypeParam.java new file mode 100644 index 0000000..f683e34 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/param/DictTypeParam.java @@ -0,0 +1,85 @@ +package com.njcn.gather.system.dictionary.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.system.pojo.constant.SystemValidMessage; +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.*; + +/** + * @author hongawen + * @version 1.0 + * @data 2024/10/30 14:39 + */ +@Data +public class DictTypeParam { + + @ApiModelProperty("名称") + @NotBlank(message = SystemValidMessage.NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.DICT_NAME_REGEX, message = SystemValidMessage.DICT_TYPE_NAME_FORMAT_ERROR) + private String name; + + @ApiModelProperty("编码") + @NotBlank(message = SystemValidMessage.CODE_NOT_BLANK) + @Pattern(regexp = PatternRegex.DICT_CODE_REGEX, message = SystemValidMessage.DICT_TYPE_CODE_FORMAT_ERROR) + private String code; + + + @ApiModelProperty("排序") + @NotNull(message = SystemValidMessage.SORT_NOT_NULL) + @Min(value = 1, message = SystemValidMessage.SORT_FORMAT_ERROR) + @Max(value = 999, message = SystemValidMessage.SORT_FORMAT_ERROR) + private Integer sort; + + + @ApiModelProperty("开启等级:0-不开启;1-开启,默认不开启") + @NotNull(message = SystemValidMessage.OPEN_LEVEL_NOT_NULL) + @Min(value = 0, message = SystemValidMessage.OPEN_LEVEL_FORMAT_ERROR) + @Max(value = 1, message = SystemValidMessage.OPEN_LEVEL_FORMAT_ERROR) + private Integer openLevel; + + + @ApiModelProperty("开启算法描述:0-不开启;1-开启,默认不开启") + @NotNull(message = SystemValidMessage.OPEN_DESCRIBE_NOT_NULL) + @Min(value = 0, message = SystemValidMessage.OPEN_DESCRIBE_FORMAT_ERROR) + @Max(value = 1, message = SystemValidMessage.OPEN_DESCRIBE_FORMAT_ERROR) + private Integer openDescribe; + + + @ApiModelProperty("描述") + private String remark; + + /** + * 更新操作实体 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static class UpdateParam extends DictTypeParam { + + + @ApiModelProperty("id") + @NotBlank(message = SystemValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.ID_FORMAT_ERROR) + private String id; + + } + + /** + * 分页查询实体 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("编码") + private String code; + + } + +} + diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictData.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictData.java new file mode 100644 index 0000000..e08c64b --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictData.java @@ -0,0 +1,73 @@ +package com.njcn.gather.system.dictionary.pojo.po; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * + * @author hongawen + * @since 2021-12-13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_dict_data") +public class DictData extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 字典数据表Id + */ + private String id; + + /** + * 字典类型表Id + */ + private String typeId; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 排序 + */ + private Integer sort; + + /** + * 事件等级:0-普通;1-中等;2-严重(默认为0) + */ + private Integer level; + + /** + * 与高级算法内部Id描述对应; + */ + private Integer algoDescribe; + + /** + * 目前只用于表示电压等级数值 + */ + @TableField(fill = FieldFill.UPDATE) + private String value; + + /** + * 是否开启使用Value值:0-关闭;1-开启(默认为0) + */ + private Integer openValue; + + /** + * 状态:0-删除 1-正常 + */ + private Integer state; + +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictTree.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictTree.java new file mode 100644 index 0000000..9b01f3e --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictTree.java @@ -0,0 +1,85 @@ +package com.njcn.gather.system.dictionary.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; + +import java.util.List; + +/** + * @author caozehui + * @data 2024/11/8 + */ +@Data +@TableName(value = "sys_dict_tree") +public class DictTree extends BaseEntity { + /** + * 主键 + */ + @TableId(value = "id", type = IdType.ASSIGN_UUID) + private String id; + + /** + * 父id + */ + @TableField(value = "pid") + private String pid; + + /** + * 父ids + */ + @TableField(value = "pids") + private String pids; + + /** + * 名称 + */ + @TableField(value = "name") + private String name; + + /** + * 编码 + */ + @TableField(value = "code") + private String code; + + /** + * 用于区分多种类型的字典树 0.台账对象类型 1.自定义报表指标类型 + */ + private Integer type; + + /** + * 根据type自定义内容,type:0用于区分对象类型是101电网侧 102用户侧 + */ + private String extend; + + /** + * 排序 + */ + @TableField(value = "sort") + private Integer sort; + + /** + * 描述 + */ + @TableField(value = "remark") + private String remark; + + /** + * 状态(字典 0正常 1停用 2删除) + */ + @TableField(value = "state") + private Integer state; + + /** + * 子类 + */ + @TableField(exist = false) + private List children; + +// @TableField(exist = false) +// private Integer level; +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictType.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictType.java new file mode 100644 index 0000000..fb0059f --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/po/DictType.java @@ -0,0 +1,62 @@ +package com.njcn.gather.system.dictionary.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * + * @author hongawen + * @since 2021-12-13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_dict_type") +public class DictType extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 字典类型表Id + */ + private String id; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 排序 + */ + private Integer sort; + + /** + * 开启等级:0-不开启;1-开启,默认不开启 + */ + private Integer openLevel; + + + /** + * 开启描述:0-不开启;1-开启,默认不开启 + */ + private Integer openDescribe; + + + /** + * 描述 + */ + private String remark; + + /** + * 状态:0-删除 1-正常 + */ + private Integer state; + +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictDataExcel.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictDataExcel.java new file mode 100644 index 0000000..5ac7998 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictDataExcel.java @@ -0,0 +1,64 @@ +package com.njcn.gather.system.dictionary.pojo.vo; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author caozehui + * @data 2024/11/6 + */ +@Data +public class DictDataExcel implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 字典数据表Id + */ +// @Excel(name = "字典数据id", width = 40) +// private String id; + + /** + * 字典类型表Id + */ +// @Excel(name = "字典类型id", width = 40) +// private String typeId; + + /** + * 名称 + */ + @Excel(name = "名称", width = 20) + private String name; + + /** + * 编码 + */ + @Excel(name = "编码", width = 20) + private String code; + + /** + * 排序 + */ + @Excel(name = "排序", width = 15) + private Integer sort; + + /** + * 事件等级:0-普通;1-中等;2-严重(默认为0) + */ + @Excel(name = "事件等级", width = 15, replace = {"普通_0", "中等_1", "严重_2"}) + private Integer level; + + /** + * 与高级算法内部Id描述对应; + */ + @Excel(name = "高级算法内部id", width = 15) + private Integer algoDescribe; + + /** + * 目前只用于表示电压等级数值 + */ + @Excel(name = "数值", width = 15) + private String value; +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictTreeVO.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictTreeVO.java new file mode 100644 index 0000000..1613cb1 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictTreeVO.java @@ -0,0 +1,66 @@ +package com.njcn.gather.system.dictionary.pojo.vo; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author caozehui + * @data 2024/11/8 + */ +@Data +public class DictTreeVO implements Serializable { + + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private String id; + + /** + * 父id + */ + private String pid; + /** + * 父类名称 + */ + private String pname; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 用于区分多种类型的字典树 0.台账对象类型 1.自定义报表指标类型 + */ + private Integer type; + + /** + * 根据type自定义内容,type:0用于区分对象类型是101电网侧 102用户侧 + */ + private String extend; + + /** + * 排序 + */ + private Integer sort; + + /** + * 描述 + */ + private String remark; + + /** + * 状态(字典 0正常 1停用 2删除) + */ + private String state; + +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictTypeExcel.java b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictTypeExcel.java new file mode 100644 index 0000000..65d3379 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/pojo/vo/DictTypeExcel.java @@ -0,0 +1,56 @@ +package com.njcn.gather.system.dictionary.pojo.vo; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import cn.afterturn.easypoi.excel.annotation.ExcelCollection; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @author caozehui + * @date 2024/11/5 + */ +@Data +public class DictTypeExcel implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Excel(name = "名称", width = 20, needMerge = true) + private String name; + + /** + * 编码 + */ + @Excel(name = "编码", width = 20, needMerge = true) + private String code; + + /** + * 排序 + */ + @Excel(name = "排序", width = 15, needMerge = true) + private Integer sort; + + /** + * 开启等级:0-不开启;1-开启,默认不开启 + */ + @Excel(name = "开启等级", width = 15, replace = {"开启_1", "不开启_0"}, needMerge = true) + private Integer openLevel; + + /** + * 开启描述:0-不开启;1-开启,默认不开启 + */ + @Excel(name = "开启描述", width = 15, replace = {"开启_1", "不开启_0"}, needMerge = true) + private Integer openDescribe; + + /** + * 描述 + */ + @Excel(name = "描述", width = 50, needMerge = true) + private String remark; + + @ExcelCollection(name = "字典内容") + private List dictDataExcels; +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictDataService.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictDataService.java new file mode 100644 index 0000000..463c64e --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictDataService.java @@ -0,0 +1,105 @@ +package com.njcn.gather.system.dictionary.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.dictionary.pojo.param.DictDataParam; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.web.pojo.dto.SimpleTreeDTO; + +import java.util.List; + +/** + *

+ * 服务类 + *

+ * + * @author hongawen + * @since 2021-12-13 + */ +public interface IDictDataService extends IService { + + + /** + * 根据字典类型id查询字典信息 + * @param queryParam 查询参数 + * @return 操作结果 + */ + Page getDictDataByTypeId(DictDataParam.QueryParam queryParam); + + /** + * 根据字典类型id查询该类型下所有字典数据 + * + * @param typeId + * @return + */ + List listDictDataByTypeId(String typeId); + + /** + * 根据字典类型id查询字典信息 + * @param typeId 字典类型id + * @return 操作结果 + */ + List getDictDataByTypeId(String typeId); + + /** + * 新增数据字典 + * @param dictDataParam 字典数据 + * @return 操作结果 + */ + boolean addDictData(DictDataParam dictDataParam); + + + /** + * 更新字典数据 + * @param updateParam 字典数据 + * @return 操作结果 + */ + boolean updateDictData(DictDataParam.UpdateParam updateParam); + + /** + * 批量逻辑删除字典数据 + * @param ids 字典id集合 + * @return 操作结果 + */ + boolean deleteDictData(List ids); + + /** + * 根据字典id获取字典数据 + * @param id 查询参数 + * @return 根据字典id查询字典数据 + */ + DictData getDictDataById(String id); + + /** + * 根据字典名称获取字典数据 + * @param name 字典名称 + * @return 根据字典名称查询字典数据 + */ + DictData getDictDataByName(String name); + + /** + * 根据字典code获取字典数据 + * @param code 字典code + * @return 根据字典code查询字典数据 + */ + DictData getDictDataByCode(String code); + + /** + * 获取所有字典数据基础信息 + * @return 返回所有字典数据 + */ + List dictDataCache(); + + /** + * 导出字典数据 + * @param queryParam 查询参数 + */ + void exportDictData(DictDataParam.QueryParam queryParam); + + /** + * 根据字典类型id删除字典数据 + * @param ids 字典类型id集合 + * @return 成功返回true,失败返回false + */ + boolean deleteDictDataByDictTypeId(List ids); +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTreeService.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTreeService.java new file mode 100644 index 0000000..0d8e9d0 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTreeService.java @@ -0,0 +1,86 @@ +package com.njcn.gather.system.dictionary.service; + +import com.njcn.gather.system.dictionary.pojo.param.DictTreeParam; +import com.njcn.gather.system.dictionary.pojo.po.DictTree; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.dictionary.pojo.vo.DictTreeVO; + +import java.util.List; + + +/** + * @author caozehui + * @data 2024/11/8 + */ +public interface IDictTreeService extends IService { + + /** + * 根据code查询字典树 + * + * @param code 编码 + * @return 字典树 + */ + List getTreeByCode(String code); + + /** + * 根据name查询字典树 + * + * @param name 编码 + * @return 字典树 + */ + List getTreeByName(String name); + + boolean addDictTree(DictTreeParam dictTreeParam); + + boolean updateDictTree(DictTreeParam.UpdateParam param); + + boolean deleteDictTree(String id); + + /** + * 根据id查询字典数据 + * + * @param id id + */ + DictTree queryById(String id); + + /** + * 查询所有字典树 + * + * @return + */ + List queryTree(); + + /** + * 根据字典树id查询字典树 + * + * @param ids + * @return + */ + List getDictTreeById(List ids); + + DictTree getDictTreeByCode(String code); + + List listByFatherIds(List fatherIdList); + + /** + * 获取父级字典树 + * @param id 字典树ID + * @return 父级字典树 + */ + DictTree queryParentById(String id); + + /** + * 根据id获取所有子节点的ID + * @param scriptId 字典树ID + * @return 所有子节点ID + */ + List getChildIds(String scriptId); + + /** + * 测试项排个序 + * @param scriptList 测试项 + * @return 有序的测试项 + */ + List sort(List scriptList); +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTypeService.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTypeService.java new file mode 100644 index 0000000..dbc387f --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTypeService.java @@ -0,0 +1,59 @@ +package com.njcn.gather.system.dictionary.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.dictionary.pojo.param.DictTypeParam; +import com.njcn.gather.system.dictionary.pojo.po.DictType; + +import java.util.List; + +/** + * @author hongawen + * @since 2021-12-13 + */ +public interface IDictTypeService extends IService { + + /** + * 根据前台传递参数,分页查询字典类型数据 + * @param queryParam 查询参数 + * @return 字典列表 + */ + Page listDictTypes(DictTypeParam.QueryParam queryParam); + + /** + * 新增字典类型数据 + * + * @param dictTypeParam 字典类型数据 + * @return 操作结果 + */ + boolean addDictType(DictTypeParam dictTypeParam); + + /** + * 修改字典类型 + * + * @param updateParam 字典类型数据 + * @return 操作结果 + */ + boolean updateDictType(DictTypeParam.UpdateParam updateParam); + + /** + * 批量逻辑删除字典类型数据 + * @param ids id集合 + * @return 操作结果 + */ + boolean deleteDictType(List ids); + + /** + * 导出字典类型数据 + * @param queryParam 查询参数 + */ + void exportDictType(DictTypeParam.QueryParam queryParam); + + /** + * 根据code获取字典类型数据 + * + * @param code 字典类型code + * @return + */ + DictType getByCode(String code); +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictDataServiceImpl.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictDataServiceImpl.java new file mode 100644 index 0000000..8ed26dc --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictDataServiceImpl.java @@ -0,0 +1,212 @@ +package com.njcn.gather.system.dictionary.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.db.mybatisplus.constant.DbConstant; +import com.njcn.gather.system.dictionary.mapper.DictDataMapper; +import com.njcn.gather.system.dictionary.pojo.dto.DictDataCache; +import com.njcn.gather.system.dictionary.pojo.param.DictDataParam; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.gather.system.dictionary.pojo.po.DictType; +import com.njcn.gather.system.dictionary.pojo.vo.DictDataExcel; +import com.njcn.gather.system.dictionary.service.IDictDataService; +import com.njcn.gather.system.pojo.enums.SystemResponseEnum; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.pojo.dto.SimpleDTO; +import com.njcn.web.pojo.dto.SimpleTreeDTO; +import com.njcn.web.utils.ExcelUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author hongawen + * @since 2021-12-13 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DictDataServiceImpl extends ServiceImpl implements IDictDataService { + + @Override + public Page getDictDataByTypeId(DictDataParam.QueryParam queryParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(queryParam)) { + queryWrapper.like(StrUtil.isNotBlank(queryParam.getName()), "sys_dict_data.name", queryParam.getName()) + .like(StrUtil.isNotBlank(queryParam.getCode()), "sys_dict_data.code", queryParam.getCode()); + //排序 + if (ObjectUtil.isAllNotEmpty(queryParam.getSortBy(), queryParam.getOrderBy())) { + queryWrapper.orderBy(true, queryParam.getOrderBy().equals(DbConstant.ASC), StrUtil.toUnderlineCase(queryParam.getSortBy())); + } else { + //没有排序参数,默认根据sort字段排序,没有排序字段的,根据updateTime更新时间排序 + queryWrapper.orderBy(true, true, "sys_dict_data.sort").orderByDesc("sys_dict_data.update_time"); + } + } else { + queryWrapper.orderBy(true, true, "sys_dict_data.sort").orderByDesc("sys_dict_data.update_time"); + } + queryWrapper.ne("sys_dict_data.state", DataStateEnum.DELETED.getCode()) + .eq("sys_dict_data.type_id", queryParam.getTypeId()); + //初始化分页数据 + return this.baseMapper.selectPage(new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)), queryWrapper); + } + + @Override + public List listDictDataByTypeId(String typeId) { + return this.lambdaQuery().eq(DictData::getTypeId, typeId).eq(DictData::getState, DataStateEnum.ENABLE.getCode()).list(); + } + + @Override + public List getDictDataByTypeId(String typeId) { + return this.lambdaQuery().eq(DictData::getTypeId, typeId).eq(DictData::getState, DataStateEnum.ENABLE.getCode()).list(); + } + + @Override + @Transactional + public boolean addDictData(DictDataParam dictDataParam) { + dictDataParam.setName(dictDataParam.getName().trim()); + checkDicDataName(dictDataParam, false); + DictData dictData = new DictData(); + BeanUtil.copyProperties(dictDataParam, dictData); + //默认为正常状态 + dictData.setState(DataStateEnum.ENABLE.getCode()); + return this.save(dictData); + } + + + @Override + @Transactional + public boolean updateDictData(DictDataParam.UpdateParam updateParam) { + updateParam.setName(updateParam.getName().trim()); + checkDicDataName(updateParam, true); + DictData dictData = new DictData(); + BeanUtil.copyProperties(updateParam, dictData); + return this.updateById(dictData); + } + + @Override + @Transactional + public boolean deleteDictData(List ids) { + return this.lambdaUpdate() + .set(DictData::getState, DataStateEnum.DELETED.getCode()) + .in(DictData::getId, ids) + .update(); + } + + + @Override + public DictData getDictDataById(String id) { + return this.lambdaQuery().eq(DictData::getId, id).eq(DictData::getState, DataStateEnum.ENABLE.getCode()).one(); + } + + @Override + public DictData getDictDataByName(String name) { + return this.lambdaQuery().eq(DictData::getName, name).eq(DictData::getState, DataStateEnum.ENABLE.getCode()).one(); + } + + @Override + public DictData getDictDataByCode(String code) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(DictData::getCode, code) + .eq(DictData::getState, DataStateEnum.ENABLE.getCode()); + return this.baseMapper.selectOne(queryWrapper); + } + + @Override + public List dictDataCache() { + MPJLambdaWrapper dictTypeWrapper = new MPJLambdaWrapper() + .eq(DictData::getState, DataStateEnum.ENABLE.getCode()) + .selectAll(DictData.class) + .selectAs(DictType::getId, DictDataCache::getTypeId) + .selectAs(DictType::getName, DictDataCache::getTypeName) + .selectAs(DictType::getCode, DictDataCache::getTypeCode) + .leftJoin(DictType.class, DictType::getId, DictData::getTypeId) + .eq(DictType::getState, DataStateEnum.ENABLE.getCode()); + List allDictData = this.getBaseMapper().selectJoinList(DictDataCache.class, dictTypeWrapper); + + Map> dictDataCacheMap = allDictData.stream() + .collect(Collectors.groupingBy(DictDataCache::getTypeId)); + return dictDataCacheMap.keySet().stream().map(typeId -> { + SimpleTreeDTO simpleTreeDTO = new SimpleTreeDTO(); + List dictDataCaches = dictDataCacheMap.get(typeId); + List simpleDTOList = dictDataCaches.stream().map(dictDataCache -> { + simpleTreeDTO.setCode(dictDataCache.getTypeCode()); + simpleTreeDTO.setId(dictDataCache.getTypeId()); + simpleTreeDTO.setName(dictDataCache.getTypeName()); + SimpleDTO simpleDTO = new SimpleDTO(); + simpleDTO.setCode(dictDataCache.getCode()); + simpleDTO.setId(dictDataCache.getId()); + simpleDTO.setName(dictDataCache.getName()); + simpleDTO.setSort(dictDataCache.getSort()); + simpleDTO.setValue(dictDataCache.getValue()); + simpleDTO.setAlgoDescribe(dictDataCache.getAlgoDescribe()); + return simpleDTO; + }).sorted(Comparator.comparing(SimpleDTO::getSort)).collect(Collectors.toList()); + simpleTreeDTO.setChildren(simpleDTOList); + return simpleTreeDTO; + }).collect(Collectors.toList()); + } + + @Override + @Transactional + public void exportDictData(DictDataParam.QueryParam queryParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(queryParam)) { + queryWrapper.like(StrUtil.isNotBlank(queryParam.getName()), "sys_dict_data.name", queryParam.getName()) + .like(StrUtil.isNotBlank(queryParam.getCode()), "sys_dict_data.code", queryParam.getCode()); + //排序 + if (ObjectUtil.isAllNotEmpty(queryParam.getSortBy(), queryParam.getOrderBy())) { + queryWrapper.orderBy(true, queryParam.getOrderBy().equals(DbConstant.ASC), StrUtil.toUnderlineCase(queryParam.getSortBy())); + } else { + //没有排序参数,默认根据sort字段排序,没有排序字段的,根据updateTime更新时间排序 + queryWrapper.orderBy(true, true, "sys_dict_data.sort").orderByDesc("sys_dict_data.update_time"); + } + } + queryWrapper.ne("sys_dict_data.state", DataStateEnum.DELETED.getCode()) + .eq("sys_dict_data.type_id", queryParam.getTypeId()); + List dictDatas = this.list(queryWrapper); + List dictDataExcels = BeanUtil.copyToList(dictDatas, DictDataExcel.class); + ExcelUtil.exportExcel("字典数据导出数据.xlsx", "字典数据", DictDataExcel.class, dictDataExcels); + } + + @Override + @Transactional + public boolean deleteDictDataByDictTypeId(List ids) { + return this.lambdaUpdate().in(DictData::getTypeId, ids).set(DictData::getState, DataStateEnum.DELETED.getCode()).update(); + } + + + /** + * 校验参数,检查是否存在相同名称的字典类型 + */ + private void checkDicDataName(DictDataParam dictDataParam, boolean isExcludeSelf) { + LambdaQueryWrapper dictDataLambdaQueryWrapper = new LambdaQueryWrapper<>(); + dictDataLambdaQueryWrapper.eq(DictData::getTypeId, dictDataParam.getTypeId()) + .eq(DictData::getState, DataStateEnum.ENABLE.getCode()) + .and(w -> w.eq(DictData::getName, dictDataParam.getName()).or().eq(DictData::getCode, dictDataParam.getCode())); + //更新的时候,需排除当前记录 + if (isExcludeSelf) { + if (dictDataParam instanceof DictDataParam.UpdateParam) { + dictDataLambdaQueryWrapper.ne(DictData::getId, ((DictDataParam.UpdateParam) dictDataParam).getId()); + } + } + int countByAccount = this.count(dictDataLambdaQueryWrapper); + //大于等于1个则表示重复 + if (countByAccount >= 1) { + throw new BusinessException(SystemResponseEnum.DICT_DATA_REPEAT); + } + } +} diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTreeServiceImpl.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTreeServiceImpl.java new file mode 100644 index 0000000..b821df4 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTreeServiceImpl.java @@ -0,0 +1,233 @@ +package com.njcn.gather.system.dictionary.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +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 com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.system.dictionary.mapper.DictTreeMapper; +import com.njcn.gather.system.dictionary.pojo.param.DictTreeParam; +import com.njcn.gather.system.dictionary.pojo.po.DictTree; +import com.njcn.gather.system.dictionary.service.IDictTreeService; +import com.njcn.gather.system.pojo.constant.DictConst; +import com.njcn.gather.system.pojo.enums.SystemResponseEnum; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @author caozehui + * @data 2024/11/8 + */ +@Service +@RequiredArgsConstructor +public class DictTreeServiceImpl extends ServiceImpl implements IDictTreeService { + + + @Override + public List getTreeByCode(String code) { + List dictTree = this.queryTree(); + + if (ObjectUtil.isNotEmpty(dictTree)) { + dictTree = dictTree.stream().filter(item -> item.getCode().equals(code)).collect(Collectors.toList()); + } + + return dictTree; + } + + @Override + public List getTreeByName(String name) { + List dictTree = this.queryTree(); + + if (ObjectUtil.isNotEmpty(dictTree) && StrUtil.isNotBlank(name)) { + dictTree = dictTree.stream().filter(item -> item.getName().contains(name)).collect(Collectors.toList()); + } + return dictTree; + } + + @Override + @Transactional + public boolean addDictTree(DictTreeParam dictTreeParam) { + dictTreeParam.setName(dictTreeParam.getName().trim()); + checkRepeat(dictTreeParam, false); + boolean result; + DictTree dictTree = new DictTree(); + BeanUtils.copyProperties(dictTreeParam, dictTree); + if (!Objects.equals(dictTree.getPid(), DictConst.FATHER_ID)) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("id", dictTree.getPid()); + DictTree instance = this.baseMapper.selectOne(queryWrapper); + dictTree.setPids(instance.getPids() + StrPool.COMMA + instance.getId()); + } else { + dictTree.setPids(DictConst.FATHER_ID); + } + dictTree.setState(DictConst.ENABLE); + result = this.save(dictTree); + return result; + } + + @Override + @Transactional + public boolean updateDictTree(DictTreeParam.UpdateParam param) { + param.setName(param.getName().trim()); + DictTree dictTree = this.getById(param.getId()); + if ("975f63baeb6f653c54fca226a9ae36ca".equals(param.getId()) || dictTree.getPids().contains("975f63baeb6f653c54fca226a9ae36ca")) { + throw new BusinessException(SystemResponseEnum.CAN_NOT_UPDATE_USED_DICT); + } + checkRepeat(param, true); + DictTree copyDictTree = new DictTree(); + BeanUtils.copyProperties(param, copyDictTree); + return this.updateById(copyDictTree); + } + + @Override + @Transactional + public boolean deleteDictTree(String id) { + boolean result = false; + DictTree dictTree = this.getById(id); + if ("975f63baeb6f653c54fca226a9ae36ca".equals(id) || dictTree.getPids().contains("975f63baeb6f653c54fca226a9ae36ca")) { + throw new BusinessException(SystemResponseEnum.CAN_NOT_DELETE_USED_DICT); + } + + List childrenList = this.lambdaQuery().eq(DictTree::getState, DictConst.ENABLE).eq(DictTree::getPid, id).list(); + if (CollectionUtils.isEmpty(childrenList)) { + result = this.lambdaUpdate().set(DictTree::getState, DictConst.DELETE).in(DictTree::getId, id).update(); + } else { + throw new BusinessException(SystemResponseEnum.EXISTS_CHILDREN_NOT_DELETE); + } + return result; + } + + @Override + public DictTree queryById(String id) { + return this.lambdaQuery().eq(DictTree::getId, id).eq(DictTree::getState, DictConst.ENABLE).one(); + } + + @Override + public List queryTree() { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(DictTree::getState, DictConst.ENABLE); + List dictTreeList = this.list(lambdaQueryWrapper); + return dictTreeList.stream().filter(item -> DictConst.FATHER_ID.equals(item.getPid())).peek(item -> { +// item.setLevel(0); + item.setChildren(getChildren(item, dictTreeList)); + }).sorted(Comparator.comparingInt(DictTree::getSort)).collect(Collectors.toList()); + } + + @Override + public List getDictTreeById(List ids) { + return this.list(new LambdaQueryWrapper() + .in(CollUtil.isNotEmpty(ids), DictTree::getId, ids) + ); + } + + @Override + public DictTree getDictTreeByCode(String code) { + return this.getOne(new LambdaQueryWrapper() + .eq(DictTree::getCode, code) + ); + } + + @Override + public List listByFatherIds(List fatherIdList) { + if (CollUtil.isNotEmpty(fatherIdList)) { + return this.lambdaQuery().in(DictTree::getPid, fatherIdList).eq(DictTree::getState, DictConst.ENABLE).list(); + } + return null; + } + + @Override + public DictTree queryParentById(String id) { + DictTree temp = this.lambdaQuery().eq(DictTree::getId, id).eq(DictTree::getState, DictConst.ENABLE).one(); + return this.lambdaQuery().eq(DictTree::getId, temp.getPid()).one(); + } + + @Override + public List getChildIds(String scriptId) { + List subTree = this.lambdaQuery().eq(DictTree::getPid, scriptId) + .eq(DictTree::getState, DictConst.ENABLE) + .list(); + if(CollectionUtil.isNotEmpty(subTree)){ + return subTree.stream().map(DictTree::getId).collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + @Override + public List sort(List scriptList) { + if (CollectionUtil.isEmpty(scriptList)) { + return Collections.emptyList(); + } + + return this.lambdaQuery() + .in(DictTree::getId, scriptList) + .orderByAsc(DictTree::getSort) + .list() + .stream() + .map(DictTree::getId) + .collect(Collectors.toList()); + } + + private void checkRepeat(DictTreeParam dictTreeParam, boolean isExcludeSelf) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DictTree::getPid, dictTreeParam.getPid()) // 同一父节点下不能有相同的code + .eq(DictTree::getCode, dictTreeParam.getCode()) + .eq(DictTree::getState, DictConst.ENABLE); + if (isExcludeSelf) { + if (dictTreeParam instanceof DictTreeParam.UpdateParam) { + wrapper.ne(DictTree::getId, ((DictTreeParam.UpdateParam) dictTreeParam).getId()); + } + } + int count = this.count(wrapper); + if (count > 0) { + throw new BusinessException(SystemResponseEnum.CODE_REPEAT); + } + } + + private List filterTreeByName(List tree, String keyword) { + if (CollectionUtils.isEmpty(tree) || !StrUtil.isNotBlank(keyword)) { + return tree; + } + filter(tree, keyword); + return tree; + } + + private void filter(List list, String keyword) { + for (int i = list.size() - 1; i >= 0; i--) { + DictTree dictTree = list.get(i); + List children = dictTree.getChildren(); + if (!dictTree.getName().contains(keyword)) { + if (!CollectionUtils.isEmpty(children)) { + filter(children, keyword); + } + if (CollectionUtils.isEmpty(dictTree.getChildren())) { + list.remove(i); + } + } +// else { +// if (!CollectionUtils.isEmpty(children)) { +// filter(children, keyword); +// } +// } + } + } + + private List getChildren(DictTree dictTree, List all) { + return all.stream().filter(item -> item.getPid().equals(dictTree.getId())).peek(item -> { +// item.setLevel(dictTree.getLevel() + 1); + item.setChildren(getChildren(item, all)); + }).sorted(Comparator.comparingInt(DictTree::getSort)).collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTypeServiceImpl.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTypeServiceImpl.java new file mode 100644 index 0000000..c184f30 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTypeServiceImpl.java @@ -0,0 +1,145 @@ +package com.njcn.gather.system.dictionary.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.db.mybatisplus.constant.DbConstant; +import com.njcn.gather.system.dictionary.mapper.DictTypeMapper; +import com.njcn.gather.system.dictionary.pojo.param.DictTypeParam; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.gather.system.dictionary.pojo.po.DictType; +import com.njcn.gather.system.dictionary.pojo.vo.DictDataExcel; +import com.njcn.gather.system.dictionary.pojo.vo.DictTypeExcel; +import com.njcn.gather.system.dictionary.service.IDictDataService; +import com.njcn.gather.system.dictionary.service.IDictTypeService; +import com.njcn.gather.system.pojo.enums.SystemResponseEnum; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.utils.ExcelUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author hongawen + * @since 2021-12-13 + */ +@Service +@RequiredArgsConstructor +public class DictTypeServiceImpl extends ServiceImpl implements IDictTypeService { + private final IDictDataService dictDataService; + + @Override + public Page listDictTypes(DictTypeParam.QueryParam queryParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(queryParam)) { + queryWrapper.like(StrUtil.isNotBlank(queryParam.getName()), "sys_dict_type.name", queryParam.getName()) + .like(StrUtil.isNotBlank(queryParam.getCode()), "sys_dict_type.code", queryParam.getCode()); + //排序 + if (ObjectUtil.isAllNotEmpty(queryParam.getSortBy(), queryParam.getOrderBy())) { + queryWrapper.orderBy(true, queryParam.getOrderBy().equals(DbConstant.ASC), StrUtil.toUnderlineCase(queryParam.getSortBy())); + } else { + //没有排序参数,默认根据sort字段排序,没有排序字段的,根据updateTime更新时间排序 + queryWrapper.orderBy(true, true, "sys_dict_type.sort").orderByDesc("sys_dict_type.update_time"); + } + } else { + queryWrapper.orderBy(true, true, "sys_dict_type.sort").orderByDesc("sys_dict_type.update_time"); + } + queryWrapper.ne("sys_dict_type.state", DataStateEnum.DELETED.getCode()); + return this.baseMapper.selectPage(new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)), queryWrapper); + } + + @Override + @Transactional + public boolean addDictType(DictTypeParam dictTypeParam) { + dictTypeParam.setName(dictTypeParam.getName().trim()); + checkDicTypeName(dictTypeParam, false); + DictType dictType = new DictType(); + BeanUtil.copyProperties(dictTypeParam, dictType); + //默认为正常状态 + dictType.setState(DataStateEnum.ENABLE.getCode()); + return this.save(dictType); + } + + @Override + @Transactional + public boolean updateDictType(DictTypeParam.UpdateParam updateParam) { + updateParam.setName(updateParam.getName().trim()); + checkDicTypeName(updateParam, true); + DictType dictType = new DictType(); + BeanUtil.copyProperties(updateParam, dictType); + return this.updateById(dictType); + } + + @Override + @Transactional + public boolean deleteDictType(List ids) { + dictDataService.deleteDictDataByDictTypeId(ids); + return this.lambdaUpdate() + .set(DictType::getState, DataStateEnum.DELETED.getCode()) + .in(DictType::getId, ids) + .update(); + } + + @Override + public void exportDictType(DictTypeParam.QueryParam queryParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(queryParam)) { + queryWrapper.like(StrUtil.isNotBlank(queryParam.getName()), "sys_dict_type.name", queryParam.getName()) + .like(StrUtil.isNotBlank(queryParam.getCode()), "sys_dict_type.code", queryParam.getCode()); + //排序 + if (ObjectUtil.isAllNotEmpty(queryParam.getSortBy(), queryParam.getOrderBy())) { + queryWrapper.orderBy(true, queryParam.getOrderBy().equals(DbConstant.ASC), StrUtil.toUnderlineCase(queryParam.getSortBy())); + } else { + //没有排序参数,默认根据sort字段排序,没有排序字段的,根据updateTime更新时间排序 + queryWrapper.orderBy(true, true, "sys_dict_type.sort").orderByDesc("sys_dict_type.update_time"); + } + } + queryWrapper.ne("sys_dict_type.state", DataStateEnum.DELETED.getCode()); + List dictTypes = this.list(queryWrapper); + + List dictTypeVOS = new ArrayList<>(); + dictTypes.forEach(dictType -> { + DictTypeExcel dictTypeExcel = BeanUtil.copyProperties(dictType, DictTypeExcel.class); + List dictDataList = dictDataService.listDictDataByTypeId(dictType.getId()); + List dictDataExcels = BeanUtil.copyToList(dictDataList, DictDataExcel.class); + dictTypeExcel.setDictDataExcels(dictDataExcels); + dictTypeVOS.add(dictTypeExcel); + }); + + ExcelUtil.exportExcel("字典类型导出数据.xlsx", "字典类型", DictTypeExcel.class, dictTypeVOS); + } + + @Override + public DictType getByCode(String code) { + return this.lambdaQuery().eq(DictType::getCode, code).eq(DictType::getState, DataStateEnum.ENABLE.getCode()).one(); + } + + /** + * 校验参数,检查是否存在相同名称的字典类型 + */ + private void checkDicTypeName(DictTypeParam dictTypeParam, boolean isExcludeSelf) { + LambdaQueryWrapper dictTypeLambdaQueryWrapper = new LambdaQueryWrapper<>(); + dictTypeLambdaQueryWrapper.eq(DictType::getState, DataStateEnum.ENABLE.getCode()) + .and(w -> w.eq(DictType::getCode, dictTypeParam.getCode()).or().eq(DictType::getName, dictTypeParam.getName())); + //更新的时候,需排除当前记录 + if (isExcludeSelf) { + if (dictTypeParam instanceof DictTypeParam.UpdateParam) { + dictTypeLambdaQueryWrapper.ne(DictType::getId, ((DictTypeParam.UpdateParam) dictTypeParam).getId()); + } + } + int countByAccount = this.count(dictTypeLambdaQueryWrapper); + //大于等于1个则表示重复 + if (countByAccount >= 1) { + throw new BusinessException(SystemResponseEnum.DICT_TYPE_REPEAT); + } + } +} diff --git a/system/src/main/java/com/njcn/gather/system/log/controller/SysLogController.java b/system/src/main/java/com/njcn/gather/system/log/controller/SysLogController.java new file mode 100644 index 0000000..45c782e --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/controller/SysLogController.java @@ -0,0 +1,70 @@ +package com.njcn.gather.system.log.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.log.pojo.param.SysLogParam; +import com.njcn.gather.system.log.pojo.po.SysLogAudit; +import com.njcn.gather.system.log.service.ISysLogAuditService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +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.RestController; + + +/** + * @author caozehui + * @date 2024-11-29 + */ +@Slf4j +@Api(tags = "日志管理") +@RestController +@RequestMapping("/sysLog") +@RequiredArgsConstructor +public class SysLogController extends BaseController { + private final ISysLogAuditService sysLogAuditService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @PostMapping("/list") + @ApiOperation(value = "获取日志列表") + @ApiImplicitParam(name = "param", value = "查询参数", required = true) + public HttpResult> list(@RequestBody @Validated SysLogParam.QueryParam param) { + String methodDescribe = getMethodDescribe("list"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, param); + Page result = sysLogAuditService.listSysLogAudit(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DOWNLOAD) + @PostMapping("/exportCSV") + @ApiOperation("日志导出为csv文件") + @ApiImplicitParam(name = "param", value = "查询参数", required = true) + public void exportCSV(@RequestBody @Validated SysLogParam.QueryParam param) { + String methodDescribe = getMethodDescribe("export"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, param); + sysLogAuditService.exportCSV(param); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DOWNLOAD) + @PostMapping("/analyse") + @ApiOperation("日志分析") + @ApiImplicitParam(name = "param", value = "查询参数", required = true) + public void analyse(@RequestBody @Validated SysLogParam.QueryParam param) { + String methodDescribe = getMethodDescribe("analyze"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, param); + sysLogAuditService.analyse(param); + } +} + diff --git a/system/src/main/java/com/njcn/gather/system/log/mapper/SysLogAuditMapper.java b/system/src/main/java/com/njcn/gather/system/log/mapper/SysLogAuditMapper.java new file mode 100644 index 0000000..c3e9a29 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/mapper/SysLogAuditMapper.java @@ -0,0 +1,13 @@ +package com.njcn.gather.system.log.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.system.log.pojo.po.SysLogAudit; + +/** + * @author caozehui + * @date 2024-11-29 + */ +public interface SysLogAuditMapper extends MPJBaseMapper { + +} + diff --git a/system/src/main/java/com/njcn/gather/system/log/mapper/SysLogRunMapper.java b/system/src/main/java/com/njcn/gather/system/log/mapper/SysLogRunMapper.java new file mode 100644 index 0000000..b019da6 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/mapper/SysLogRunMapper.java @@ -0,0 +1,13 @@ +package com.njcn.gather.system.log.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.system.log.pojo.po.SysLogRun; + +/** + * @author caozehui + * @date 2024-11-29 + */ +public interface SysLogRunMapper extends MPJBaseMapper { + +} + diff --git a/system/src/main/java/com/njcn/gather/system/log/mapper/mapping/SysLogAuditMapper.xml b/system/src/main/java/com/njcn/gather/system/log/mapper/mapping/SysLogAuditMapper.xml new file mode 100644 index 0000000..625ad44 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/mapper/mapping/SysLogAuditMapper.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/system/src/main/java/com/njcn/gather/system/log/mapper/mapping/SysLogRunMapper.xml b/system/src/main/java/com/njcn/gather/system/log/mapper/mapping/SysLogRunMapper.xml new file mode 100644 index 0000000..cbb8b62 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/mapper/mapping/SysLogRunMapper.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/system/src/main/java/com/njcn/gather/system/log/pojo/dto/SysLogAuditRecord.java b/system/src/main/java/com/njcn/gather/system/log/pojo/dto/SysLogAuditRecord.java new file mode 100644 index 0000000..372bbd2 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/pojo/dto/SysLogAuditRecord.java @@ -0,0 +1,33 @@ +package com.njcn.gather.system.log.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SysLogAuditRecord { + + private String userId; + + private String loginName; + + private String ip; + + private String operate; + + private String operateType; + + private String result; + + private String reason; + + private Integer type; + + private Integer level; + + private Integer warn; +} diff --git a/system/src/main/java/com/njcn/gather/system/log/pojo/enums/LogLevelEnum.java b/system/src/main/java/com/njcn/gather/system/log/pojo/enums/LogLevelEnum.java new file mode 100644 index 0000000..f67a9e7 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/pojo/enums/LogLevelEnum.java @@ -0,0 +1,28 @@ +package com.njcn.gather.system.log.pojo.enums; + +import lombok.Getter; + +/** + * @author caozehui + * @data 2025-02-12 + */ +@Getter +public enum LogLevelEnum { + DEBUG(1, "DEBUG"), + INFO(2, "INFO"), + WARN(3, "WARN"), + ERROR(4, "ERROR"), + FATAL(5, "FATAL"); + + private final int code; + private final String msg; + + LogLevelEnum(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public static LogLevelEnum getEnum(int code) { + return LogLevelEnum.values()[code - 1]; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/log/pojo/enums/LogOperationTypeEnum.java b/system/src/main/java/com/njcn/gather/system/log/pojo/enums/LogOperationTypeEnum.java new file mode 100644 index 0000000..f6797d7 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/pojo/enums/LogOperationTypeEnum.java @@ -0,0 +1,18 @@ +package com.njcn.gather.system.log.pojo.enums; + +import lombok.Getter; + +/** + * @author caozehui + * @data 2025-02-12 + */ +@Getter +public enum LogOperationTypeEnum { + OPERATE("操作日志"), + WARNING("告警日志"); + + private String msg; + LogOperationTypeEnum(String msg) { + this.msg = msg; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/log/pojo/param/SysLogParam.java b/system/src/main/java/com/njcn/gather/system/log/pojo/param/SysLogParam.java new file mode 100644 index 0000000..2ce72a2 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/pojo/param/SysLogParam.java @@ -0,0 +1,26 @@ +package com.njcn.gather.system.log.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.system.pojo.constant.SystemValidMessage; +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Pattern; + +/** + * @author caozehui + * @data 2024-11-25 + */ +@Data +public class SysLogParam { + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + + @ApiModelProperty("操作用户") + private String userName; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/log/pojo/po/SysLogAudit.java b/system/src/main/java/com/njcn/gather/system/log/pojo/po/SysLogAudit.java new file mode 100644 index 0000000..1fc0db2 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/pojo/po/SysLogAudit.java @@ -0,0 +1,105 @@ +package com.njcn.gather.system.log.pojo.po; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author caozehui + * @date 2024-11-29 + */ +@Data +@TableName("sys_log_audit") +public class SysLogAudit implements Serializable { + private static final long serialVersionUID = -56962081010894238L; + /** + * 审计日志Id + */ + private String id; + + /** + * 登录名 + */ + @Excel(name = "登录名") + private String loginName; + + + /** + * 用户名 + */ + @Excel(name = "用户名") + private String userName; + + + /** + * IP + */ + @Excel(name = "IP", width = 20) + private String ip; + + /** + * 操作内容 + */ + @Excel(name = "操作内容", width = 80) + private String operate; + + + /** + * 操作类型 (比如:查询、新增、删除、下载等等) + */ + @Excel(name = "操作类型") + private String operateType; + + /** + * 事件结果 0失败 1成功 + */ + @Excel(name = "事件结果", replace = {"失败_0", "成功_1"}) + private String result; + + /** + * 失败原因 + */ + @Excel(name = "失败原因") + private String reason; + + /** + * 事件类型 + */ + @Excel(name = "事件类型", replace = {"业务事件_0", "系统事件_1"}) + private Integer type; + + + /** + * 事件严重度(0.普通 1.中等 2.严重) + */ + @Excel(name = "事件严重度", replace = {"普通_0", "中等_1", "严重_2"}) + private Integer level; + + /** + * 告警标志(0:未告警;1:已告警),默认未告警 + */ + @Excel(name = "告警标志", replace = {"未告警_0", "已告警_1"}) + private Integer warn; + + + /** + * 日志发生时间 + */ + @Excel(name = "日志发生时间", width = 30, exportFormat = "yyyy-MM-dd HH:mm:ss") + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime logTime; +} + diff --git a/system/src/main/java/com/njcn/gather/system/log/pojo/po/SysLogRun.java b/system/src/main/java/com/njcn/gather/system/log/pojo/po/SysLogRun.java new file mode 100644 index 0000000..40d4e33 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/pojo/po/SysLogRun.java @@ -0,0 +1,58 @@ +package com.njcn.gather.system.log.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author caozehui + * @date 2024-11-29 + */ +@Data +@TableName("sys_log_run") +public class SysLogRun implements Serializable { + private static final long serialVersionUID = -37126398654461376L; + /** + * 运行日志Id + */ + private String id; + + /** + * 类型(数据源、被检设备) + */ + private String type; + + /** + * IP + */ + private String ip; + + /** + * 事件结果 + */ + private String result; + + /** + * 事件描述 + */ + private String remark; + + /** + * 告警标志(0:未告警;1:已告警),默认未告警 + */ + private Integer warn; + + /** + * 创建用户 + */ + private String createBy; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + +} + diff --git a/system/src/main/java/com/njcn/gather/system/log/pojo/vo/SysLogVO.java b/system/src/main/java/com/njcn/gather/system/log/pojo/vo/SysLogVO.java new file mode 100644 index 0000000..c2b834b --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/pojo/vo/SysLogVO.java @@ -0,0 +1,42 @@ +package com.njcn.gather.system.log.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @author caozehui + * @data 2024-11-25 + */ +@Data +public class SysLogVO { + + private String id; + + /** + * 操作类型,审计日志独有字段。 + */ + private String operateType; + + /** + * 类型(数据源、被检设备),运行日志独有字段。 + */ + private String type; + + private String ip; + + private String result; + + private String remark; + + /** + * 事件严重度(0.普通 1.中等 2.严重),审计日志独有字段。 + */ + private Integer level; + + private Integer warn; + + private String createBy; + + private LocalDateTime createTime; +} diff --git a/system/src/main/java/com/njcn/gather/system/log/service/ISysLogAuditService.java b/system/src/main/java/com/njcn/gather/system/log/service/ISysLogAuditService.java new file mode 100644 index 0000000..3b24f3b --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/service/ISysLogAuditService.java @@ -0,0 +1,45 @@ +package com.njcn.gather.system.log.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.log.pojo.dto.SysLogAuditRecord; +import com.njcn.gather.system.log.pojo.param.SysLogParam; +import com.njcn.gather.system.log.pojo.po.SysLogAudit; + +/** + * @author caozehui + * @date 2024-11-29 + */ +public interface ISysLogAuditService extends IService { + + /** + * 分页查询审计日志 + * + * @param param 查询参数 + * @return 分页结果 + */ + Page listSysLogAudit(SysLogParam.QueryParam param); + + /** + * 导出审计日志数据 + * + * @param param 查询参数 + */ + void exportCSV(SysLogParam.QueryParam param); + + /** + * 分析日志 + * + * @param param 查询参数 + */ + void analyse(SysLogParam.QueryParam param); + + void recodeAdviceLog(SysLogAuditRecord logRecord); + + /** + * 全局异常拦截器捕获异常后的日志记录入库 + * + * @param logRecord 日志记录 + */ + void recodeBusinessExceptionLog(SysLogAuditRecord logRecord); +} diff --git a/system/src/main/java/com/njcn/gather/system/log/service/ISysLogRunService.java b/system/src/main/java/com/njcn/gather/system/log/service/ISysLogRunService.java new file mode 100644 index 0000000..e341927 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/service/ISysLogRunService.java @@ -0,0 +1,12 @@ +package com.njcn.gather.system.log.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.log.pojo.po.SysLogRun; + +/** + * @author caozehui + * @date 2024-11-29 + */ +public interface ISysLogRunService extends IService { + +} diff --git a/system/src/main/java/com/njcn/gather/system/log/service/impl/SysLogAuditServiceImpl.java b/system/src/main/java/com/njcn/gather/system/log/service/impl/SysLogAuditServiceImpl.java new file mode 100644 index 0000000..fc653dd --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/service/impl/SysLogAuditServiceImpl.java @@ -0,0 +1,167 @@ +package com.njcn.gather.system.log.service.impl; + +import cn.afterturn.easypoi.csv.entity.CsvExportParams; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.constant.LogInfo; +import com.njcn.db.mybatisplus.constant.UserConstant; +import com.njcn.gather.system.log.mapper.SysLogAuditMapper; +import com.njcn.gather.system.log.pojo.dto.SysLogAuditRecord; +import com.njcn.gather.system.log.pojo.param.SysLogParam; +import com.njcn.gather.system.log.pojo.po.SysLogAudit; +import com.njcn.gather.system.log.service.ISysLogAuditService; +import com.njcn.gather.system.log.util.CSVUtil; +import com.njcn.gather.user.user.pojo.po.SysUser; +import com.njcn.gather.user.user.service.ISysUserService; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.utils.HttpServletUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author caozehui + * @date 2024-11-29 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysLogAuditServiceImpl extends ServiceImpl implements ISysLogAuditService { + + private final ISysUserService sysUserService; + + @Override + public Page listSysLogAudit(SysLogParam.QueryParam param) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(param)) { + queryWrapper.like(StrUtil.isNotBlank(param.getUserName()), "sys_log_audit.User_name", param.getUserName()) + .ge(StrUtil.isNotBlank(param.getSearchBeginTime()), "sys_log_audit.Log_time", LocalDateTimeUtil.parse(param.getSearchBeginTime() + " 00:00:00", DatePattern.NORM_DATETIME_FORMATTER)) + .le(StrUtil.isNotBlank(param.getSearchEndTime()), "sys_log_audit.Log_time", LocalDateTimeUtil.parse(param.getSearchEndTime() + " 23:59:59", DatePattern.NORM_DATETIME_FORMATTER)); + } + queryWrapper.orderByDesc("sys_log_audit.Log_time"); + return this.page(new Page<>(PageFactory.getPageNum(param), PageFactory.getPageSize(param)), queryWrapper); + } + + @Override + public void exportCSV(SysLogParam.QueryParam param) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(param)) { + queryWrapper.like(StrUtil.isNotBlank(param.getUserName()), "sys_log_audit.User_name", param.getUserName()) + .ge(StrUtil.isNotBlank(param.getSearchBeginTime()), "sys_log_audit.Log_time", LocalDateTimeUtil.parse(param.getSearchBeginTime() + " 00:00:00", DatePattern.NORM_DATETIME_FORMATTER)) + .le(StrUtil.isNotBlank(param.getSearchEndTime()), "sys_log_audit.Log_time", LocalDateTimeUtil.parse(param.getSearchEndTime() + " 23:59:59", DatePattern.NORM_DATETIME_FORMATTER)); + } + queryWrapper.orderByDesc("sys_log_audit.Log_time"); + List list = this.list(queryWrapper); + CsvExportParams params = new CsvExportParams(); + CSVUtil.exportCsv("日志数据.csv", params, SysLogAudit.class, list); + } + + /** + * 暂时废弃,有需要再重新写 + * + * @param param . + */ + @Override + @Deprecated + public void analyse(SysLogParam.QueryParam param) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(param)) { + queryWrapper.like(StrUtil.isNotBlank(param.getUserName()), "sys_log_audit.User_name", param.getUserName()) + .between(StrUtil.isAllNotBlank(param.getSearchBeginTime(), param.getSearchEndTime()), "sys_log_audit.Log_time", param.getSearchBeginTime(), param.getSearchEndTime()); + } + List list = this.list(queryWrapper); + Map collect = list.stream().collect(Collectors.groupingBy(SysLogAudit::getLoginName, Collectors.counting())); + + this.exportAnalyseExcel("分析结果", collect); + } + + @Override + public void recodeAdviceLog(SysLogAuditRecord logRecord) { + saveAuditLog(logRecord); + } + + @Override + public void recodeBusinessExceptionLog(SysLogAuditRecord logRecord) { + saveAuditLog(logRecord); + } + + private void saveAuditLog(SysLogAuditRecord logRecord) { + if (ObjectUtil.isNull(logRecord)) { + return; + } + SysLogAudit sysLogAudit = new SysLogAudit(); + fillOperator(sysLogAudit, logRecord); + sysLogAudit.setIp(logRecord.getIp()); + sysLogAudit.setOperate(logRecord.getOperate()); + sysLogAudit.setOperateType(logRecord.getOperateType()); + sysLogAudit.setResult(logRecord.getResult()); + sysLogAudit.setReason(logRecord.getReason()); + sysLogAudit.setType(logRecord.getType() == null ? 1 : logRecord.getType()); + Integer level = logRecord.getLevel() == null ? 0 : logRecord.getLevel(); + sysLogAudit.setLevel(level); + Integer warn = logRecord.getWarn() == null ? (level == 1 ? 1 : 0) : logRecord.getWarn(); + sysLogAudit.setWarn(warn); + sysLogAudit.setLogTime(LocalDateTime.now()); + this.save(sysLogAudit); + } + + private void fillOperator(SysLogAudit sysLogAudit, SysLogAuditRecord logRecord) { + String userId = logRecord.getUserId(); + String loginName = logRecord.getLoginName(); + + if (StrUtil.isNotBlank(userId)) { + SysUser sysUser = sysUserService.getById(userId); + if (sysUser != null) { + sysLogAudit.setUserName(sysUser.getName()); + sysLogAudit.setLoginName(sysUser.getLoginName()); + return; + } + } + + if (StrUtil.isNotBlank(loginName) && !LogInfo.UNKNOWN_USER.equalsIgnoreCase(loginName)) { + sysLogAudit.setUserName(loginName); + sysLogAudit.setLoginName(loginName); + } else { + sysLogAudit.setUserName(UserConstant.UN_LOGIN); + sysLogAudit.setLoginName(UserConstant.UN_LOGIN); + } + } + + private void exportAnalyseExcel(String fileName, Map collect) { + HttpServletResponse response = HttpServletUtil.getResponse(); + XSSFWorkbook wb = new XSSFWorkbook(); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + fileName = URLEncoder.encode(fileName, CharsetUtil.UTF_8); + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentType("application/octet-stream;charset=UTF-8"); + + XSSFSheet sheet = wb.createSheet("sheet1"); + + //createPieChart(sheet, collect.keySet().stream().collect(Collectors.toList()), collect.values().stream().collect(Collectors.toList())); + + wb.write(outputStream); + wb.close(); + } catch (IOException e) { + log.error(">>> 导出数据异常:{}", e.getMessage()); + } + } +} diff --git a/system/src/main/java/com/njcn/gather/system/log/service/impl/SysLogRunServiceImpl.java b/system/src/main/java/com/njcn/gather/system/log/service/impl/SysLogRunServiceImpl.java new file mode 100644 index 0000000..83ab275 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/service/impl/SysLogRunServiceImpl.java @@ -0,0 +1,20 @@ +package com.njcn.gather.system.log.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.gather.system.log.pojo.po.SysLogRun; +import com.njcn.gather.system.log.mapper.SysLogRunMapper; +import com.njcn.gather.system.log.service.ISysLogRunService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author caozehui + * @date 2024-11-29 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysLogRunServiceImpl extends ServiceImpl implements ISysLogRunService { + +} diff --git a/system/src/main/java/com/njcn/gather/system/log/util/CSVUtil.java b/system/src/main/java/com/njcn/gather/system/log/util/CSVUtil.java new file mode 100644 index 0000000..bf76f6f --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/log/util/CSVUtil.java @@ -0,0 +1,123 @@ +package com.njcn.gather.system.log.util; + + +import cn.afterturn.easypoi.csv.CsvExportUtil; +import cn.afterturn.easypoi.csv.entity.CsvExportParams; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.njcn.web.utils.HttpServletUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @author caozehui + * @data 2024-12-2 + */ +@Slf4j +public class CSVUtil { + + /** + * CSV文件列分隔符 + */ + private static final String CSV_COLUMN_SEPARATOR = ","; + + /** + * CSV文件行分隔符 + */ + private static final String CSV_ROW_SEPARATOR = System.lineSeparator(); + + /** + * 导出CSV文件 + * + * @param fileName 文件名 + * @param titles 表头 + * @param keys 字段名 + * @param dataList 数据集 + */ + public static void export(String fileName, String[] titles, String[] keys, List> dataList) { + // 保证线程安全 + StringBuffer buf = new StringBuffer(); + + // 组装表头 + for (String title : titles) { + buf.append(title).append(CSV_COLUMN_SEPARATOR); + } + buf.append(CSV_ROW_SEPARATOR); + // 组装数据 + if (CollectionUtils.isNotEmpty(dataList)) { + for (Map map : dataList) { + for (String key : keys) { + if (ObjectUtil.isEmpty(map.get(key))) { + buf.append("").append(CSV_COLUMN_SEPARATOR); + } else { + // 如果数据内容中包含逗号,则进行使用!替换 + buf.append(map.get(key).toString().replaceAll(",", "!")).append(CSV_COLUMN_SEPARATOR); + } + } + buf.deleteCharAt(buf.length() - 1).append(CSV_ROW_SEPARATOR); + } + } + + HttpServletResponse response = HttpServletUtil.getResponse(); + + try { + ServletOutputStream os = response.getOutputStream(); + Throwable var1 = null; + + try { + fileName = URLEncoder.encode(fileName, "UTF-8"); + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentType("application/octet-stream;charset=UTF-8"); + // 写出响应 + os.write(buf.toString().getBytes("UTF-8")); + os.flush(); + } catch (Throwable var2) { + var1 = var2; + throw var2; + } finally { + if (os != null) { + if (var1 != null) { + try { + os.close(); + } catch (Throwable var3) { + var1.addSuppressed(var3); + } + } else { + os.close(); + } + } + } + } catch (IOException var4) { + IOException e = var4; + log.error(">>> 导出数据异常:{}", e.getMessage()); + } + } + + + public static void exportCsv(String fileName, CsvExportParams params, Class pojoClass, Collection dataSet) { + HttpServletResponse response = HttpServletUtil.getResponse(); + ServletOutputStream os = null; + try { + os = response.getOutputStream(); + fileName = URLEncoder.encode(fileName, "UTF-8"); + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentType("application/octet-stream;charset=UTF-8"); + + CsvExportUtil.exportCsv(params, pojoClass, dataSet, os); + os.flush(); + os.close(); + } catch (Exception e) { + log.error(">>> 导出数据异常:{}", e.getMessage()); + } + } +} diff --git a/system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java b/system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java new file mode 100644 index 0000000..b44c542 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java @@ -0,0 +1,21 @@ +package com.njcn.gather.system.pojo.constant; + +/** + * @author caozehui + * @data 2024/11/8 + */ +public interface DictConst { + /** + * 状态 0-正常;1-停用;2-删除 默认正常 + */ + int ENABLE = 0; + + int PAUSE = 1; + + int DELETE = 2; + + /** + * 顶层父类的pid + */ + String FATHER_ID = "0"; +} diff --git a/system/src/main/java/com/njcn/gather/system/pojo/constant/SystemValidMessage.java b/system/src/main/java/com/njcn/gather/system/pojo/constant/SystemValidMessage.java new file mode 100644 index 0000000..517fe18 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/pojo/constant/SystemValidMessage.java @@ -0,0 +1,95 @@ +package com.njcn.gather.system.pojo.constant; + +/** + * @author hongawen + * @version 1.0 + * @data 2024/10/30 14:46 + */ +public interface SystemValidMessage { + + + String MISS_PREFIX = "字段不能为空,请检查"; + + + String ID_NOT_BLANK = "id不能为空,请检查id参数"; + + String ID_FORMAT_ERROR = "id格式错误,请检查id参数"; + + String DICT_TYPE_ID_NOT_BLANK = "typeId不能为空,请检查typeId参数"; + + String DICT_TYPE_ID_FORMAT_ERROR = "typeId格式错误,请检查typeId参数"; + + String NAME_NOT_BLANK = "名称不能为空,请检查name参数"; + + String DICT_TYPE_NAME_FORMAT_ERROR = "名称格式错误,只能包含字母、数字、中文、下划线、中划线、空格,长度为1~32个字符"; + String DICT_TYPE_CODE_FORMAT_ERROR = "编码格式错误,只能包含字母、数字、下划线,长度为1~30个字符"; + + String INDUSTRY_NOT_BLANK = "行业不能为空,请检查industry参数"; + String INDUSTRY_FORMAT_ERROR = "行业格式错误,请检查industry参数"; + String ADDR_NOT_BLANK = "所属区域不能为空,请检查addr参数"; + + String CODE_NOT_BLANK = "编码不能为空,请检查code参数"; + + String SORT_NOT_NULL = "排序不能为空,请检查sort参数"; + + String SORT_FORMAT_ERROR = "排序范围在1至999,请检查sort参数"; + + String OPEN_LEVEL_NOT_NULL = "开启等级不能为空,请检查openLevel参数"; + + String OPEN_LEVEL_FORMAT_ERROR = "开启等级格式错误,请检查openLevel参数"; + + String OPEN_DESCRIBE_NOT_NULL = "开启描述不能为空,请检查openDescribe参数"; + + String OPEN_DESCRIBE_FORMAT_ERROR = "开启描述格式错误,请检查openDescribe参数"; + + String AREA_NOT_BLANK = "行政区域不能为空,请检查area参数"; + + String AREA_FORMAT_ERROR = "行政区域格式错误,请检查area参数"; + + String PID_NOT_BLANK = "父节点不能为空,请检查pid参数"; + + String PID_FORMAT_ERROR = "父节点格式错误,请检查pid参数"; + + String COLOR_NOT_BLANK = "主题色不能为空,请检查color参数"; + + String COLOR_FORMAT_ERROR = "主题色格式错误,请检查color参数"; + + String LOGO_NOT_BLANK = "iconUrl不能为空,请检查iconUrl参数"; + + String FAVICON_NOT_BLANK = "faviconUrl不能为空,请检查faviconUrl参数"; + + String REMARK_NOT_BLANK = "描述不能为空,请检查remark参数"; + + String REMARK_FORMAT_ERROR = "描述格式错误,请检查remark参数"; + + String PARAM_FORMAT_ERROR = "参数值非法"; + + String IP_FORMAT_ERROR = "IP格式非法"; + + String DEVICE_VERSION_NOT_BLANK = "装置版本json文件不能为空,请检查deviceVersionFile参数"; + + String PHASE_NOT_BLANK = "相别不能为空,请检查phase参数"; + + String DATA_TYPE_NOT_BLANK = "数据模型不能为空,请检查dataType参数"; + + String CLASS_ID_NOT_BLANK = "数据表表名不能为空,请检查classId参数"; + + String AUTO_GENERATE_NOT_NULL = "是否自动生成不能为空,请检查autoGenerate参数"; + + String MAX_RECHECK_NOT_NULL = "最大检测次数不能为空,请检查maxRecheck参数"; + + String DATA_RULE_NOT_BLANK = "数据处理规则不能为空,请检查dataRule参数"; + + String DATA_RULE_FORMAT_ERROR = "数据处理规则格式错误,请检查dataRule参数"; + + String TYPE_NOT_BLANK = "版本类型不能为空,请检查type参数"; + + String AUTO_GENERATE_FORMAT_ERROR = "是否自动生成格式错误,请检查autoGenerate参数"; + + String USER_ID_FORMAT_ERROR = "用户id格式错误,请检查userId参数"; + String DICT_DATA_NAME_FORMAT_ERROR = "字典数据名称格式错误,只能包含字母、数字、中文、下划线、中划线、空格、点、斜线、反斜线、百分号、摄氏度符号,长度为1~32个字符"; + String DICT_DATA_CODE_FORMAT_ERROR = "字典数据编码格式错误,只能包含字母、数字、中文、下划线、中划线、空格、点、斜线、反斜线、百分号、摄氏度符号,长度为1~30个字符"; + String DICT_PQ_OTHER_NAME_FORMAT_ERROR = "别名格式错误,只能包含字母、数字、中文、下划线、中划线、空格,长度最大为32个字符"; + String DICT_PQ_SHOW_NAME_FORMAT_ERROR = "显示名称格式错误,只能包含字母、数字、中文、下划线、中划线、空格,长度最大为32个字符"; + String SCALE_NOT_NULL = "数据精度不能为空,请检查scale参数"; +} diff --git a/system/src/main/java/com/njcn/gather/system/pojo/enums/DicDataEnum.java b/system/src/main/java/com/njcn/gather/system/pojo/enums/DicDataEnum.java new file mode 100644 index 0000000..deeb988 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/pojo/enums/DicDataEnum.java @@ -0,0 +1,57 @@ +package com.njcn.gather.system.pojo.enums; + +import lombok.Getter; + +/** + * @author cdf + * @version 1.0.0 + */ +@Getter +public enum DicDataEnum { + + + FREQ("FREQ","频率"), + V("V","电压"), + I("I","电流"), + IMBV("IMBV","三相电压不平衡度"), + IMBA("IMBA","三相电流不平衡度"), + + HV("HV","谐波电压"), + HI("HI","谐波电流"), + HP("HP","谐波有功功率"), + HSV("HSV","间谐波电压"), + HSI("HSI","间谐波电流"), + VOLTAGE("VOLTAGE","暂态"), + F("F","闪变"), + + + + ; + + + private final String code; + + private final String message; + + DicDataEnum(String code, String message) { + this.code = code; + this.message = message; + } + + public static DicDataEnum getEnumByCode(String code) { + for (DicDataEnum e : DicDataEnum.values()) { + if (e.getCode().equals(code)) { + return e; + } + } + return null; + } + + public static String getMessageByCode(String code) { + DicDataEnum e = getEnumByCode(code); + if (e!= null) { + return e.getMessage(); + } + return ""; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/pojo/enums/SystemResponseEnum.java b/system/src/main/java/com/njcn/gather/system/pojo/enums/SystemResponseEnum.java new file mode 100644 index 0000000..5570429 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/pojo/enums/SystemResponseEnum.java @@ -0,0 +1,42 @@ +package com.njcn.gather.system.pojo.enums; + +import lombok.Getter; + +/** + * @author hongawen + * @version 1.0.0 + * @date 2021年12月20日 09:56 + */ +@Getter +public enum SystemResponseEnum { + + /** + * 系统模块异常响应码的范围: + * A01000 ~ A01 + */ + DICT_TYPE_REPEAT("A01000", "字典类型名称或编码重复"), + DICT_DATA_REPEAT("A01002", "字典数据名称或编码重复"), + DICT_PQ_NAME_EXIST("A01003", "当前数据模型及相别下已存在相同名称"), + EXISTS_CHILDREN_NOT_DELETE("A01004", "当前字典下存在子字典,不能删除"), + CODE_REPEAT("A01005", "该层级下已存在相同的编码"), + CAN_NOT_DELETE_USED_DICT("A01006", "该字典在使用中,不能删除"), + CAN_NOT_UPDATE_USED_DICT("A01007", "该字典在使用中,不能修改"), + LOG_RECORD_FAILED("A01008", "日志记录失败"), + + + GET_MAC_ADDRESS_FAILED("A01040", "获取本机MAC地址失败"), + REGISTRATION_CODE_FORMAT_ERROR("A01041", "注册码格式错误"), + MAC_ADDRESS_NOT_MATCH("A01042","注册码中的MAC地址与本机MAC地址不匹配"), + REGISTRATION_CODE_EXPIRED("A01043","注册码已过期"), + FILE_NOT_FOUND("A01050", "模板文件不存在"), + FILE_IO_ERROR("A01051", "文件读写异常"),; + + private final String code; + + private final String message; + + SystemResponseEnum(String code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/reg/controller/SysRegResController.java b/system/src/main/java/com/njcn/gather/system/reg/controller/SysRegResController.java new file mode 100644 index 0000000..809e880 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/controller/SysRegResController.java @@ -0,0 +1,90 @@ +package com.njcn.gather.system.reg.controller; + +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.reg.pojo.param.SysRegResParam; +import com.njcn.gather.system.reg.pojo.po.SysRegRes; +import com.njcn.gather.system.reg.pojo.vo.SysRegResVO; +import com.njcn.gather.system.reg.service.ISysRegResService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + + +/** + * @author caozehui + * @date 2024-11-21 + */ +@Slf4j +@Api(tags = "注册版本管理") +@RestController +@RequestMapping("/sysRegRes") +@RequiredArgsConstructor +public class SysRegResController extends BaseController { + private final ISysRegResService sysRegResService; + +// @OperateInfo(info = LogEnum.SYSTEM_COMMON) +// @GetMapping("/list") +// @ApiOperation("查询注册版本列表") +// public HttpResult listRegRes() { +// String methodDescribe = getMethodDescribe("listRegRes"); +// LogUtil.njcnDebug(log, "{},查询参数为空", methodDescribe); +// SysRegResVO result = sysRegResService.listRegRes(); +// return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); +// } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getRegResByType") + @ApiOperation("根据类型id查询配置") + @ApiImplicitParam(name = "typeId", value = "类型id,字典值", required = true) + public HttpResult getRegResByType(@RequestParam("typeId") String typeId) { + String methodDescribe = getMethodDescribe("listByTypeId"); + LogUtil.njcnDebug(log, "{},查询参数为:{}", methodDescribe, typeId); + SysRegRes result = sysRegResService.getRegResByType(typeId); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + +// @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) +// @PostMapping("/add") +// @ApiOperation("新增注册版本") +// @ApiImplicitParam(name = "sysRegRes", value = "注册版本对象", required = true) +// public HttpResult addRegRes(@RequestBody @Validated SysRegResParam param) { +// String methodDescribe = getMethodDescribe("addRegRes"); +// LogUtil.njcnDebug(log, "{},新增参数为:{}", methodDescribe, param); +// boolean result = sysRegResService.addRegRes(param); +// if (result) { +// return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); +// } else { +// return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); +// } +// } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改配置") + @ApiImplicitParam(name = "param", value = "注册版本更新对象", required = true) + public HttpResult updateRegRes(@RequestBody @Validated SysRegResParam.UpdateParam param) { + String methodDescribe = getMethodDescribe("updateRegRes"); + LogUtil.njcnDebug(log, "{},更新参数为:{}", methodDescribe, param); + boolean result = sysRegResService.updateRegRes(param); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } +} + diff --git a/system/src/main/java/com/njcn/gather/system/reg/mapper/SysRegResMapper.java b/system/src/main/java/com/njcn/gather/system/reg/mapper/SysRegResMapper.java new file mode 100644 index 0000000..3db039b --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/mapper/SysRegResMapper.java @@ -0,0 +1,13 @@ +package com.njcn.gather.system.reg.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.system.reg.pojo.po.SysRegRes; + +/** + * @author caozehui + * @date 2024-11-21 + */ +public interface SysRegResMapper extends MPJBaseMapper { + +} + diff --git a/system/src/main/java/com/njcn/gather/system/reg/mapper/mapping/SysRegResMapper.xml b/system/src/main/java/com/njcn/gather/system/reg/mapper/mapping/SysRegResMapper.xml new file mode 100644 index 0000000..92c2462 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/mapper/mapping/SysRegResMapper.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/system/src/main/java/com/njcn/gather/system/reg/pojo/RegResValidMessage.java b/system/src/main/java/com/njcn/gather/system/reg/pojo/RegResValidMessage.java new file mode 100644 index 0000000..895dce9 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/pojo/RegResValidMessage.java @@ -0,0 +1,9 @@ +package com.njcn.gather.system.reg.pojo; + +/** + * @author caozehui + * @data 2025-02-11 + */ +public interface RegResValidMessage { + String CODE_NOT_BLANK="注册码不能为空"; +} diff --git a/system/src/main/java/com/njcn/gather/system/reg/pojo/dto/RegInfoData.java b/system/src/main/java/com/njcn/gather/system/reg/pojo/dto/RegInfoData.java new file mode 100644 index 0000000..de98eaf --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/pojo/dto/RegInfoData.java @@ -0,0 +1,33 @@ +package com.njcn.gather.system.reg.pojo.dto; + +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * @author caozehui + * @data 2025-02-11 + */ +@Data +public class RegInfoData { + /** + * mac地址 + */ + private String macAddress; + + /** + * 注册模式 + */ + private List typeList; + + /** + * 到期日期 + */ + private List expireDateList; + + /** + * 使用场景 + */ + private String scene; +} diff --git a/system/src/main/java/com/njcn/gather/system/reg/pojo/enums/RegStatusEnum.java b/system/src/main/java/com/njcn/gather/system/reg/pojo/enums/RegStatusEnum.java new file mode 100644 index 0000000..199d7ee --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/pojo/enums/RegStatusEnum.java @@ -0,0 +1,21 @@ +package com.njcn.gather.system.reg.pojo.enums; + +import lombok.Getter; + +/** + * @author caozehui + * @data 2025-03-13 + */ +@Getter +public enum RegStatusEnum { + UNREGISTERED(0, "未激活"), + REGISTERED(1, "已激活"), + EXPIRED(2, "已过期"); + + private int code; + private String message; + RegStatusEnum(int code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/system/src/main/java/com/njcn/gather/system/reg/pojo/param/SysRegResParam.java b/system/src/main/java/com/njcn/gather/system/reg/pojo/param/SysRegResParam.java new file mode 100644 index 0000000..23395d5 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/pojo/param/SysRegResParam.java @@ -0,0 +1,52 @@ +package com.njcn.gather.system.reg.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.system.pojo.constant.SystemValidMessage; +import com.njcn.gather.system.reg.pojo.RegResValidMessage; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * @author caozehui + * @data 2024-11-21 + */ +@Data +public class SysRegResParam { + +// @ApiModelProperty("版本类型") +// @NotBlank(message = SystemValidMessage.TYPE_NOT_BLANK) +// @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.ID_FORMAT_ERROR) +// private String type; + + @ApiModelProperty("注册码") + @NotBlank(message = RegResValidMessage.CODE_NOT_BLANK) + private String code; + +// @ApiModelProperty("密钥") +// private String licenseKey; + + @Data + public static class UpdateParam { + + @ApiModelProperty("id") + @NotBlank(message = SystemValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = SystemValidMessage.ID_FORMAT_ERROR) + private String id; + + @ApiModelProperty("录波数据有效组数") + private Integer waveRecord; + + @ApiModelProperty("实时数据有效组数") + private Integer realTime; + + @ApiModelProperty("统计数据有效组数") + private Integer statistics; + + @ApiModelProperty("短闪数据有效组数") + private Integer flicker; + + } +} diff --git a/system/src/main/java/com/njcn/gather/system/reg/pojo/po/SysRegRes.java b/system/src/main/java/com/njcn/gather/system/reg/pojo/po/SysRegRes.java new file mode 100644 index 0000000..4ad2fc4 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/pojo/po/SysRegRes.java @@ -0,0 +1,72 @@ +package com.njcn.gather.system.reg.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDate; + +/** + * @author caozehui + * @date 2024-11-21 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_reg_res") +public class SysRegRes extends BaseEntity implements Serializable { + private static final long serialVersionUID = 801772692898301698L; + /** + * 版本注册表Id + */ + private String id; + + /** + * 版本类型(模拟式、数字式、比对式) + */ + private String type; + + /** + * 注册码 + */ + private String code; + + /** + * 密钥 + */ + private String licenseKey; + + /** + * 到期时间 + */ +// private LocalDate expireDate; + private String expireDate; + + /** + * 录波数据有效组数 + */ + private Integer waveRecord; + + /** + * 实时数据有效组数 + */ + private Integer realTime; + + /** + * 统计数据有效组数 + */ + private Integer statistics; + + /** + * 短闪数据有效组数 + */ + private Integer flicker; + + /** + * 状态:0-删除 1-正常 + */ + private Integer state; + +} + diff --git a/system/src/main/java/com/njcn/gather/system/reg/pojo/vo/SysRegResVO.java b/system/src/main/java/com/njcn/gather/system/reg/pojo/vo/SysRegResVO.java new file mode 100644 index 0000000..f8f245b --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/pojo/vo/SysRegResVO.java @@ -0,0 +1,48 @@ +package com.njcn.gather.system.reg.pojo.vo; + +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * @author caozehui + * @data 2024-11-25 + */ +@Data +public class SysRegResVO { + /** + * 注册码。如果未激活,则为null + */ + private String code; + + /** + * 模拟式激活状态。0:未激活;1:已激活;2:已过期 + */ + private int simulateStatus; + + /** + * 模拟式激活到期日期。如果未激活,则为null + */ + private String simulateExpireDate; + + /** + * 数字式激活状态。0:未激活;1:已激活;2:已过期 + */ + private int digitalStatus; + + /** + * 数字式激活到期日期。如果未激活,则为null + */ + private String digitalExpireDate; + + /** + * 比对式激活状态。0:未激活;1:已激活;2:已过期 + */ + private int contrastStatus; + + /** + * 比对式激活到期日期。如果未激活,则为null + */ + private String contrastExpireDate; +} diff --git a/system/src/main/java/com/njcn/gather/system/reg/service/ISysRegResService.java b/system/src/main/java/com/njcn/gather/system/reg/service/ISysRegResService.java new file mode 100644 index 0000000..71367c5 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/service/ISysRegResService.java @@ -0,0 +1,47 @@ +package com.njcn.gather.system.reg.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.reg.pojo.param.SysRegResParam; +import com.njcn.gather.system.reg.pojo.po.SysRegRes; +import com.njcn.gather.system.reg.pojo.vo.SysRegResVO; + +import java.util.List; +import java.util.Map; + +/** + * @author caozehui + * @date 2024-11-21 + */ +public interface ISysRegResService extends IService { + /** + * 查询版本注册信息 + * @return + */ + SysRegRes getRegResByType(String type); + + /** + * 新增版本注册表 + * @param sysRegResParam 版本注册表参数 + * @return 成功返回true,失败返回false + */ + boolean addRegRes(SysRegResParam sysRegResParam); + + /** + * 修改配置 + * @param param + * @return 成功返回true,失败返回false + */ + boolean updateRegRes(SysRegResParam.UpdateParam param); + + /** + * 查询版本注册表列表 + * @return 版本注册信息 + */ + SysRegResVO listRegRes(); + + /** + * 获取比对式的注册信息 + * @return + */ + SysRegRes getContrastRegRes(); +} diff --git a/system/src/main/java/com/njcn/gather/system/reg/service/impl/SysRegResServiceImpl.java b/system/src/main/java/com/njcn/gather/system/reg/service/impl/SysRegResServiceImpl.java new file mode 100644 index 0000000..66f1608 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/reg/service/impl/SysRegResServiceImpl.java @@ -0,0 +1,326 @@ +package com.njcn.gather.system.reg.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.common.utils.EncryptionUtil; +import com.njcn.gather.system.cfg.service.ISysTestConfigService; +import com.njcn.gather.system.dictionary.pojo.enums.DictDataEnum; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.gather.system.dictionary.service.IDictDataService; +import com.njcn.gather.system.pojo.enums.SystemResponseEnum; +import com.njcn.gather.system.reg.mapper.SysRegResMapper; +import com.njcn.gather.system.reg.pojo.dto.RegInfoData; +import com.njcn.gather.system.reg.pojo.enums.RegStatusEnum; +import com.njcn.gather.system.reg.pojo.param.SysRegResParam; +import com.njcn.gather.system.reg.pojo.po.SysRegRes; +import com.njcn.gather.system.reg.pojo.vo.SysRegResVO; +import com.njcn.gather.system.reg.service.ISysRegResService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.crypto.Cipher; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysRegResServiceImpl extends ServiceImpl implements ISysRegResService { + + private final ISysTestConfigService sysTestConfigService; + + /** + * 固定私钥 + */ + private static final String fixedPrivateKeyStr = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTC6MQkjHAIyPQ\n" + + "12tfSBsmS12OspsmJORW2cf7s+xwe3lJ3nb9KohjAuRBG4jLceAXdZoCuk1+Xt8l\n" + + "kesXp930DWmb7SBHLa7imGgvIm/9hP7Y48yDJqM2Ia6CFbMCmLYCiU3oDLZ2DuUq\n" + + "QbE2j9QuTTMiOVHBy4M82XwkEXNfIQez9PB+Osex2X9FesyMoWvsoOcnDD8iWmbP\n" + + "Ag0syuutt4YkrU8IWC5v2vyTxb+N+F36wqbC8td1wOgUrf2K3kQtTe7xB1b3i5RI\n" + + "DAsjxPd1a3uzJVjJ7Onla+B4dkFGOSlX/w8/GJQzMgegskKEPRONyXRWCaVpBt9O\n" + + "6yDjKz7vAgMBAAECggEATnj+vowlnJRUXnSjM5AbrD8MwCEQSHwiPVsIHcLWkTKQ\n" + + "NFPYeaVFhk9OcRkcYc1rbj2nsQj2BJ2hKpaZzDd6c6NDGBvxSxYk95OE9bW/34wC\n" + + "uMHnSwLkYB3hBfSslbQTxVipk7WaaMZ8FpzLmIaddkP1Ve3rRPx3xXn2y3CDriRs\n" + + "WxVKm4+ri8Ncnk0dBqjEBNwgdsvRlxPMAB9t4mTxS6NENriIWgiBGDiMFT3dstkw\n" + + "tIvxX/gdrLCd9CbV1iZgH+a/CiKIYcSGzPiYfwEi446cPZwfwYo65C1HBS1mMJju\n" + + "FwLw0r9OQ9LEsO+ar6d7hss4RmHgRR1JdQyBoq/wbQKBgQD1kkE1TfJpFmCpWR/P\n" + + "wdl88ABnwEZtaKKMS1A+T2+ywEl3y+iUPoouJ+69zG0s7NuwpeJ2c1S0BB+yFRWx\n" + + "ExGjJ2Z0GbK++f2R38Y3kvj2lGiB4AZ1sentTuatGHWRzwEd913swrKd/fkFBA/I\n" + + "x38Mmq7wDqkqIxNh20W2jWWd3QKBgQDcAgg39l5zYeNIUdm/PQINd4G1UjCTcgvn\n" + + "+7AFpuoCYfx9ReXzvLjGDs8VmuoBIg5Y4bWIUhTSQVQIIRE12EXrysawHs7loegp\n" + + "FTU6cZAjxT72sRhesr12f5s5Hd3zaMB0Y8PUljAG7lvAVNPQDUkAL7+bmYLxWokq\n" + + "G/FHJooBOwKBgQCidDWVKNKTuI0Lmv0TeL8DCtaJzEYK/OyDeRNFlVFkZBZ2HLvo\n" + + "zhKlhB9JCiKzVKHlE2hkSdmgGRZKve4SrXW+hEMfzRxVgJXB2dKMUztGDFmyiVxc\n" + + "oe0J42dw3Txx0AqCI3HMPeTh5fDF47D5dxhSY0YVYu2ABaI920wb/yBZNQKBgGlz\n" + + "P+Uy3QqIvJuJP8j9wOIbibwS7N1/KF3EsRXEbx09Qfv5aMJujlG//1nnqolofV/0\n" + + "r0HrtbchQNm0n78jLkBaLOl1ms1N0Sz/0Ud17xR2EjvDnl6lZVJKz2eM/TkR2Ezx\n" + + "FIfshJCN5sRE5FEwTPEd8cTuy2hLcLsSMY9c1YDJAoGBAMIPLraI8c7h4hOCNE66\n" + + "Eg2nEOcqCzmIYTS6ARCPYrOC4cx23+uAScbZCq+vBk34KSJyun1OgfvnRmVerpUU\n" + + "lld8v62FR3FnNwSY/zeySP8ENCzAUluql0yHzgpBwF3NNYihVdYY4Lm3vlUe4fTt\n" + + "krNC84FOePuC5qaIefZ03oRX\n" + + "-----END PRIVATE KEY-----"; + + private final IDictDataService dictDataService; + + @Override + public SysRegRes getRegResByType(String type) { + return this.lambdaQuery().eq(SysRegRes::getType, type).eq(SysRegRes::getState, DataStateEnum.ENABLE.getCode()).one(); + } + + @Override + @Transactional + public boolean addRegRes(SysRegResParam sysRegResParam) { + List regResList = new ArrayList<>(); + + RegInfoData regInfoData = decodeRegistrationCode(sysRegResParam.getCode()); + if (ObjectUtil.isNotNull(regInfoData)) { + // 对比Mac地址 + String mac = getMAC(); + if (!mac.equals(regInfoData.getMacAddress())) { + throw new BusinessException(SystemResponseEnum.MAC_ADDRESS_NOT_MATCH); + } + + List expireDateList = regInfoData.getExpireDateList(); + for (int i = 0; i < regInfoData.getTypeList().size(); i++) { + expireDateList.set(i, formatDate(expireDateList.get(i))); + } + // 比对到期日期 + String maxExpireDate = expireDateList.stream().max((a, b) -> a.compareTo(b)).get(); + if (LocalDate.parse(maxExpireDate).isBefore(LocalDate.now())) { + throw new BusinessException(SystemResponseEnum.REGISTRATION_CODE_EXPIRED); + } + + sysTestConfigService.addTestConfig(regInfoData.getScene()); + + for (int i = 0; i < regInfoData.getTypeList().size(); i++) { + // 忽略过期的 +// if (isExpire(expireDateList.get(i))) { +// continue; +// } + + SysRegRes sysRegRes = new SysRegRes(); + BeanUtil.copyProperties(sysRegResParam, sysRegRes); + sysRegRes.setState(DataStateEnum.ENABLE.getCode()); + sysRegRes.setCode(sysRegResParam.getCode()); + sysRegRes.setExpireDate(EncryptionUtil.encodeString(1, formatDate(regInfoData.getExpireDateList().get(i)))); + + DictData dictData = dictDataService.getDictDataByCode(regInfoData.getTypeList().get(i)); + if (ObjectUtil.isNotNull(dictData)) { + sysRegRes.setType(dictData.getId()); + } + + sysRegRes.setRealTime(20); + sysRegRes.setStatistics(5); + sysRegRes.setFlicker(1); + + if (regInfoData.getTypeList().get(i).equals(DictDataEnum.CONTRAST.getCode())) { + sysRegRes.setWaveRecord(1); + sysRegRes.setRealTime(200); + sysRegRes.setStatistics(5); + sysRegRes.setFlicker(3); + } + + regResList.add(sysRegRes); + } + } + + // 删除原有的所有数据 + this.remove(null); + return this.saveBatch(regResList); + } + + @Override + @Transactional + public boolean updateRegRes(SysRegResParam.UpdateParam param) { + SysRegRes sysRegRes = new SysRegRes(); + BeanUtil.copyProperties(param, sysRegRes); + if (sysRegRes.getWaveRecord() == -1) { + sysRegRes.setWaveRecord(null); + } + return this.updateById(sysRegRes); + } + + @Override + public SysRegResVO listRegRes() { + // todo 需要切面检测是否到期,到期则状态设为0 + SysRegResVO sysRegResVO = new SysRegResVO(); + List regResList = this.lambdaQuery().eq(SysRegRes::getState, DataStateEnum.ENABLE.getCode()).list(); + if (ObjectUtil.isNotEmpty(regResList)) { + sysRegResVO.setCode(regResList.get(0).getCode()); + sysRegResVO.setSimulateStatus(RegStatusEnum.UNREGISTERED.getCode()); + sysRegResVO.setDigitalStatus(RegStatusEnum.UNREGISTERED.getCode()); + sysRegResVO.setContrastStatus(RegStatusEnum.UNREGISTERED.getCode()); + for (SysRegRes regRes : regResList) { + DictData dictData = dictDataService.getDictDataById(regRes.getType()); + String s = EncryptionUtil.decoderString(1, regRes.getExpireDate()); + boolean expire = isExpire(s); + if (ObjectUtil.isNotNull(dictData)) { + if (dictData.getCode().equals(DictDataEnum.SIMULATE.getCode())) { + if (expire) { + sysRegResVO.setSimulateStatus(RegStatusEnum.EXPIRED.getCode()); + } else { + sysRegResVO.setSimulateStatus(RegStatusEnum.REGISTERED.getCode()); + } + sysRegResVO.setSimulateExpireDate(s); + } + if (dictData.getCode().equals(DictDataEnum.DIGITAL.getCode())) { + if (expire) { + sysRegResVO.setDigitalStatus(RegStatusEnum.EXPIRED.getCode()); + } else { + sysRegResVO.setDigitalStatus(RegStatusEnum.REGISTERED.getCode()); + } + sysRegResVO.setDigitalExpireDate(s); + } + if (dictData.getCode().equals(DictDataEnum.CONTRAST.getCode())) { + if (expire) { + sysRegResVO.setContrastStatus(RegStatusEnum.EXPIRED.getCode()); + } else { + sysRegResVO.setContrastStatus(RegStatusEnum.REGISTERED.getCode()); + } + sysRegResVO.setContrastExpireDate(s); + } + } else { + throw new BusinessException("字典数据缺失,请联系管理员!"); + } + } + } + return sysRegResVO; + } + + @Override + public SysRegRes getContrastRegRes() { + return this.getRegResByType("7cd65363a6bf675ae408f28a281b77d4"); + } + + /** + * 格式化日期 (将日期格式化为yyyy-MM-dd) + * + * @param date + * @return + */ + private String formatDate(String date) { + String[] split = date.split("-"); + if (split[1].length() == 1) { + split[1] = "0" + split[1]; + } + if (split[2].length() == 1) { + split[2] = "0" + split[2]; + } + return split[0] + "-" + split[1] + "-" + split[2]; + } + + /** + * 判断是否到期 + * + * @param expireDate 到期日期 + * @return + */ + private boolean isExpire(String expireDate) { + expireDate = formatDate(expireDate); + String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + if (today.compareTo(expireDate) >= 0) { + return true; + } + return false; + } + + /** + * 使用hutool工具获取本机mac地址 + * + * @return mac地址 + */ + private String getMAC() { + InetAddress inetAddress = null; + try { + inetAddress = InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + throw new BusinessException(SystemResponseEnum.GET_MAC_ADDRESS_FAILED); + } + return NetUtil.getMacAddress(inetAddress); + } + + /** + * 解析 PEM 格式的密钥 + * + * @param pem PEM 格式的密钥 + * @return 密钥对象 + * @throws Exception + */ + private Key parsePemKey(String pem) throws Exception { + // 去掉 PEM 格式中的头部和尾部 + String cleanPem = pem + .replaceAll("-----BEGIN (.*?)-----", "") + .replaceAll("-----END (.*?)-----", "") + .replaceAll("\\s+", ""); // 移除换行和空格 + + byte[] decodedKey = Base64.getDecoder().decode(cleanPem); + + // 根据密钥类型导入密钥 + if (pem.contains("PRIVATE")) { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + return privateKey; + } else { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(keySpec); + return publicKey; + } + } + + /** + * 解析注册码 + * + * @param encryptedCode 加密后的注册码 + * @return + */ + private RegInfoData decodeRegistrationCode(String encryptedCode) { + try { + byte[] encryptedData = Base64.getDecoder().decode(encryptedCode); + Key privateKey = parsePemKey(fixedPrivateKeyStr); + + // 使用 RSA/OAEP 解密 + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] decryptedData = cipher.doFinal(encryptedData); + + // 将解密后的字节数组转换为字符串 + String decodedString = new String(decryptedData, StandardCharsets.UTF_8); + + ObjectMapper objectMapper = new ObjectMapper(); + RegInfoData regInfoData = objectMapper.readValue(decodedString, RegInfoData.class); + if (ObjectUtil.isNull(regInfoData) || ObjectUtil.isNull(regInfoData.getScene()) || regInfoData.getTypeList().size() == 0 || regInfoData.getTypeList().size() != regInfoData.getExpireDateList().size()) { + throw new BusinessException(SystemResponseEnum.REGISTRATION_CODE_FORMAT_ERROR); + } + return regInfoData; + } catch (Exception e) { + throw new BusinessException(SystemResponseEnum.REGISTRATION_CODE_FORMAT_ERROR); + } + } +} diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..97fdf20 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,60 @@ +# Tools 模块说明 + +## 当前状态 + +`tools` 当前是工具能力聚合模块,但在本仓库内已经完成一次收口。 + +当前真实保留的子模块只有: + +- `activate-tool` + +因此,`tools` 现阶段不是一个包含多个通用工具的完整工具市场,而是一个仅保留激活能力的聚合模块。 + +## 当前结构 + +```text +tools/ +└── activate-tool/ +``` + +## activate-tool 的职责 + +`activate-tool` 当前提供的能力主要围绕设备授权与许可证: + +- 生成设备申请码 +- 生成激活码 +- 校验激活码 +- 读取本地许可证信息 + +从接口层看,当前主要围绕 `/activate/*` 路径提供能力。 + +## 模块定位 + +当前 `activate-tool` 更适合作为平台级基础能力模块,而不是业务检测模块的一部分。 + +它的职责边界建议理解为: + +- 负责授权相关的编码、解码和许可证文件处理 +- 不负责检测业务本身 +- 不负责系统级注册资源管理的全部逻辑 + +## 依赖关系 + +`tools/activate-tool` 当前主要依赖: + +- `com.njcn:njcn-common` +- `com.njcn:spingboot2.3.12` + +并由 `entrance` 模块直接聚合使用。 + +## 文档说明 + +在本次 `P0` 收口前,`tools/README.md` 曾描述多个不存在于当前仓库中的工具子模块。 +该描述已不再作为当前项目结构依据。 + +如果后续重新引入新的工具子模块,应: + +- 同步更新 `tools/pom.xml` +- 同步更新本说明文档 +- 在 `docs` 下补充模块边界与职责说明 + diff --git a/tools/activate-tool/pom.xml b/tools/activate-tool/pom.xml new file mode 100644 index 0000000..7bf7a50 --- /dev/null +++ b/tools/activate-tool/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.njcn.gather + tools + 1.0.0 + + + activate-tool + + + + + + + com.njcn + njcn-common + 0.0.1 + + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + \ No newline at end of file diff --git a/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/config/ActivateProperties.java b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/config/ActivateProperties.java new file mode 100644 index 0000000..a0c4fd8 --- /dev/null +++ b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/config/ActivateProperties.java @@ -0,0 +1,33 @@ +package com.njcn.gather.tool.active.config; + +import cn.hutool.system.SystemUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.File; + +@Data +@ConfigurationProperties(prefix = "activate") +public class ActivateProperties { + private final String LICENSE_FILE_NAME = "license.key"; + + /** + * RSA公钥 + */ + private String publicKey; + /** + * RSA私钥 + */ + private String privateKey; + + /** + * 密钥文件所在目录,默认为当前项目目录 + */ + private String licenseDir = System.getProperty(SystemUtil.USER_DIR); + + public String getLicenseFilePath() { + return licenseDir + File.separator + LICENSE_FILE_NAME; + } + + +} diff --git a/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/controller/ActivateController.java b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/controller/ActivateController.java new file mode 100644 index 0000000..de3931b --- /dev/null +++ b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/controller/ActivateController.java @@ -0,0 +1,113 @@ +package com.njcn.gather.tool.active.controller; + +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.tool.active.service.ActivateService; +import com.njcn.gather.tool.active.vo.ActivationCodePlaintext; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.*; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.RestController; + +@Slf4j +@Api(tags = "设备激活管理") +@RestController +@RequestMapping("/activate") +@RequiredArgsConstructor +public class ActivateController extends BaseController { + + private final ActivateService activateService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("生成设备申请码") + @PostMapping("/generateApplicationCode") + public HttpResult generateApplicationCode() { + String methodDescribe = getMethodDescribe("generateApplicationCode"); + LogUtil.njcnDebug(log, "{},生成设备申请码", methodDescribe); + String applicationCode = activateService.generateApplicationCode(); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, applicationCode, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("验证设备激活码") + @ApiImplicitParam(name = "params", value = "验证设备激活码参数", required = true) + @PostMapping("/verifyActivationCode") + public HttpResult verifyActivationCode(@RequestBody VerifyActivationCodeParams params) { + String methodDescribe = getMethodDescribe("verifyActivationCode"); + LogUtil.njcnDebug(log, "{},验证设备激活码\":{}", methodDescribe, JSONUtil.toJsonStr(params)); + ActivationCodePlaintext activationCodePlaintext = activateService.verifyActivationCode(params.getActivationCode()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, activationCodePlaintext, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("获取许可信息") + @PostMapping("/getLicense") + public HttpResult getLicense() { + String methodDescribe = getMethodDescribe("checkLicense"); + LogUtil.njcnDebug(log, "{},获取许可信息", methodDescribe); + ActivationCodePlaintext activationCodePlaintext = null; + try { + activationCodePlaintext = activateService.readLicenseFile(); + } catch (BusinessException e) { + methodDescribe = "无效的许可信息"; + } + String macAddress = NetUtil.getLocalMacAddress(); + if (activationCodePlaintext == null) { + activationCodePlaintext = new ActivationCodePlaintext(); + activationCodePlaintext.setMacAddress(macAddress); + activationCodePlaintext.init(); + } else { + // 校验mac地址 + String licenseMacAddress = activationCodePlaintext.getMacAddress(); + if (StrUtil.isNotEmpty(licenseMacAddress)) { + if (!StrUtil.equals(licenseMacAddress, macAddress)) { + log.error("mac地址不匹配,无效的许可文件,本机mac:{},许可mac:{}", macAddress, licenseMacAddress); + methodDescribe = "mac地址不匹配,无效的许可文件"; + activationCodePlaintext = new ActivationCodePlaintext(); + activationCodePlaintext.setMacAddress(macAddress); + activationCodePlaintext.init(); + } + } + } + + + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, activationCodePlaintext, methodDescribe); + } + + @Data + public static class VerifyActivationCodeParams { + @ApiModelProperty(value = "激活码") + private String activationCode; + } + + + @ApiOperation("生成设备激活码") + @ApiImplicitParam(name = "params", value = "参数", required = true, dataType = "ApplicationCodeParams") + @PostMapping("/generateActivationCode") + public HttpResult generateActivationCode(@RequestBody ApplicationCodeParams params) { + String activationCode = activateService.generateActivationCode(params.getApplicationCode(), params.getActivationModule()); + return HttpResultUtil.assembleResult(CommonResponseEnum.SUCCESS.getCode(), activationCode, ""); + } + + @ApiModel("生成设备激活码参数") + @Data + public static class ApplicationCodeParams { + @ApiModelProperty(value = "设备申请码", required = true) + private String applicationCode; + @ApiModelProperty(value = "激活模块", required = true) + private ActivationCodePlaintext activationModule; + } +} \ No newline at end of file diff --git a/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/service/ActivateService.java b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/service/ActivateService.java new file mode 100644 index 0000000..4c9e462 --- /dev/null +++ b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/service/ActivateService.java @@ -0,0 +1,40 @@ +package com.njcn.gather.tool.active.service; + +import com.njcn.gather.tool.active.vo.ActivationCodePlaintext; + +public interface ActivateService { + + + /** + * 生成设备申请码 + * + * @return + */ + String generateApplicationCode(); + + /** + * 验证激活码 + * + * @param activationCode + * @return + */ + ActivationCodePlaintext verifyActivationCode(String activationCode); + + /** + * 读取授权文件 + * + * @return + */ + ActivationCodePlaintext readLicenseFile(); + + /** + * 生成设备激活码 + * + * @param applicationCode 申请码 + * @param activationCodePlaintext + * @return + */ + String generateActivationCode(String applicationCode, ActivationCodePlaintext activationCodePlaintext); + + +} diff --git a/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/service/impl/ActivateServiceImpl.java b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/service/impl/ActivateServiceImpl.java new file mode 100644 index 0000000..d9d3384 --- /dev/null +++ b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/service/impl/ActivateServiceImpl.java @@ -0,0 +1,164 @@ +package com.njcn.gather.tool.active.service.impl; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.common.utils.RSAUtil; +import com.njcn.gather.tool.active.config.ActivateProperties; +import com.njcn.gather.tool.active.service.ActivateService; +import com.njcn.gather.tool.active.vo.ActivationCodePlaintext; +import com.njcn.gather.tool.active.vo.ActivationModule; +import com.njcn.gather.tool.active.vo.ApplicationCodePlaintext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; + +@EnableConfigurationProperties(ActivateProperties.class) +@RequiredArgsConstructor +@Slf4j +@Service +public class ActivateServiceImpl implements ActivateService { + + private final ActivateProperties props; + + @Override + public String generateApplicationCode() { + // 获取当前设备MAC地址 + String macAddress = NetUtil.getLocalMacAddress(); + log.debug("当前设备MAC地址:{}", macAddress); + ApplicationCodePlaintext data = new ApplicationCodePlaintext(); + data.setMacAddress(macAddress); + String plaintext = JSONUtil.toJsonStr(data); + // RSA 加密 + try { + return RSAUtil.encrypt(plaintext, RSAUtil.stringToPublicKey(props.getPublicKey())); + } catch (Exception e) { + log.error("申请码加密失败", e); + throw new BusinessException(CommonResponseEnum.FAIL, "申请码生成失败"); + } + } + + @Override + public ActivationCodePlaintext verifyActivationCode(String activationCode) { + String plaintext; + try { + plaintext = RSAUtil.decrypt(activationCode, RSAUtil.stringToPrivateKey(props.getPrivateKey())); + } catch (Exception e) { + log.error("授权码解密失败", e); + throw new BusinessException(CommonResponseEnum.FAIL, "无效的激活码"); + } + log.info("新授权码解密:{}", JSONUtil.toJsonStr(plaintext)); + ActivationCodePlaintext activationCodePlaintext = JSONUtil.toBean(plaintext, ActivationCodePlaintext.class); + String macAddress = NetUtil.getLocalMacAddress(); + if (!StrUtil.equals(activationCodePlaintext.getMacAddress(), macAddress)) { + log.error("mac地址不匹配"); + throw new BusinessException(CommonResponseEnum.FAIL, "无效的激活码"); + } + return addOrUpdateLicenseFile(activationCodePlaintext, activationCode); + } + + @Override + public ActivationCodePlaintext readLicenseFile() { + String licenseFilePath = props.getLicenseFilePath(); + log.info("读取授权文件,{}", licenseFilePath); + if (FileUtil.exist(licenseFilePath)) { + String content = FileUtil.readUtf8String(licenseFilePath); + String plaintext; + try { + plaintext = RSAUtil.decrypt(content, RSAUtil.stringToPrivateKey(props.getPrivateKey())); + } catch (Exception e) { + log.error("授权文件内容解密失败", e); + throw new BusinessException(CommonResponseEnum.FAIL, "许可信息读取失败"); + } + return JSONUtil.toBean(plaintext, ActivationCodePlaintext.class); + } + return null; + } + + @Override + public String generateActivationCode(String applicationCode, ActivationCodePlaintext activationCodePlaintext) { + // RSA 解密 + String plaintext; + try { + plaintext = RSAUtil.decrypt(applicationCode, RSAUtil.stringToPrivateKey(props.getPrivateKey())); + } catch (Exception e) { + log.error("申请码解密失败", e); + throw new BusinessException(CommonResponseEnum.FAIL, "无效的申请码"); + } + ApplicationCodePlaintext applicationCodePlaintext = JSONUtil.toBean(plaintext, ApplicationCodePlaintext.class); + if (applicationCodePlaintext == null) { + log.error("申请码内容为空"); + throw new BusinessException(CommonResponseEnum.FAIL, "无效的申请码"); + } + String macAddress = applicationCodePlaintext.getMacAddress(); + if (StrUtil.isBlank(macAddress)) { + log.error("mac地址为空"); + throw new BusinessException(CommonResponseEnum.FAIL, "无效的申请码"); + } + // 激活码明文 + activationCodePlaintext.setMacAddress(applicationCodePlaintext.getMacAddress()); + // RSA 加密 + String jsonStr = JSONUtil.toJsonStr(activationCodePlaintext); + log.info("生成激活码明文:{}", jsonStr); + try { + return RSAUtil.encrypt(jsonStr, RSAUtil.stringToPublicKey(props.getPublicKey())); + } catch (Exception e) { + log.error("生成激活码失败", e); + throw new BusinessException(CommonResponseEnum.FAIL, "生成激活码失败"); + } + } + + /** + * 添加或更新授权文件 + * + * @param newActivationCodePlaintext 新授权码明文 + * @param activationCode 授权码 + * @return 最新授权码明文 + */ + private ActivationCodePlaintext addOrUpdateLicenseFile(ActivationCodePlaintext newActivationCodePlaintext, String activationCode) { + + log.info("新授权码明文:{}", JSONUtil.toJsonStr(newActivationCodePlaintext)); + ActivationModule newContrast = newActivationCodePlaintext.getContrast(); + ActivationModule newDigital = newActivationCodePlaintext.getDigital(); + ActivationModule newSimulate = newActivationCodePlaintext.getSimulate(); + + String licenseFilePath = props.getLicenseFilePath(); + log.info("授权文件路径:{}", licenseFilePath); + ActivationCodePlaintext oldActivationCodePlaintext = this.readLicenseFile(); + if (oldActivationCodePlaintext == null) { + // 如果文件不存在,创建新文件 + FileUtil.touch(licenseFilePath); + // 写入授权文件 + FileUtil.writeUtf8String(activationCode, licenseFilePath); + return newActivationCodePlaintext; + } else { + log.info("旧授权码明文:{}", JSONUtil.toJsonStr(oldActivationCodePlaintext)); + oldActivationCodePlaintext.setMacAddress(newActivationCodePlaintext.getMacAddress()); + if (newContrast != null && newContrast.isPermanently()) { + oldActivationCodePlaintext.setContrast(newContrast); + } + if (newDigital != null && newDigital.isPermanently()) { + oldActivationCodePlaintext.setDigital(newDigital); + } + if (newSimulate != null && newSimulate.isPermanently()) { + oldActivationCodePlaintext.setSimulate(newSimulate); + } + log.info("最新授权码明文:{}", JSONUtil.toJsonStr(oldActivationCodePlaintext)); + String updateContent; + // 重新加密 + try { + updateContent = RSAUtil.encrypt(JSONUtil.toJsonStr(oldActivationCodePlaintext), RSAUtil.stringToPublicKey(props.getPublicKey())); + } catch (Exception e) { + log.error("授权文件内容加密失败", e); + throw new BusinessException(CommonResponseEnum.FAIL, "激活失败,请联系管理员"); + } + FileUtil.writeUtf8String(updateContent, licenseFilePath); + return oldActivationCodePlaintext; + } + } + +} diff --git a/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ActivationCodePlaintext.java b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ActivationCodePlaintext.java new file mode 100644 index 0000000..2864806 --- /dev/null +++ b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ActivationCodePlaintext.java @@ -0,0 +1,33 @@ +package com.njcn.gather.tool.active.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ActivationCodePlaintext extends ApplicationCodePlaintext { + + /** + * 模拟式模块 + */ + private ActivationModule simulate; + + /** + * 数字式模块 + */ + private ActivationModule digital; + + /** + * 比对式模块 + */ + private ActivationModule contrast; + + + public void init() { + simulate = new ActivationModule(); + digital = new ActivationModule(); + contrast = new ActivationModule(); + } + + +} diff --git a/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ActivationModule.java b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ActivationModule.java new file mode 100644 index 0000000..1b6e2cf --- /dev/null +++ b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ActivationModule.java @@ -0,0 +1,18 @@ +package com.njcn.gather.tool.active.vo; + +import lombok.Data; + +@Data +public class ActivationModule { + + /** + * 是否永久授权 + */ + private int permanently; + + public boolean isPermanently() { + return permanently == 1; + } + + +} \ No newline at end of file diff --git a/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ApplicationCodePlaintext.java b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ApplicationCodePlaintext.java new file mode 100644 index 0000000..40b2678 --- /dev/null +++ b/tools/activate-tool/src/main/java/com/njcn/gather/tool/active/vo/ApplicationCodePlaintext.java @@ -0,0 +1,16 @@ +package com.njcn.gather.tool.active.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel("申请码明文") +@Data +public class ApplicationCodePlaintext { + /** + * mac地址 + */ + @ApiModelProperty(value = "mac地址", hidden = true) + private String macAddress; + +} diff --git a/tools/pom.xml b/tools/pom.xml new file mode 100644 index 0000000..06a07e1 --- /dev/null +++ b/tools/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + com.njcn.gather + CN_Tool + 1.0.0 + + + tools + pom + tools + Retained utility aggregator for platform capabilities. + + + + activate-tool + + + diff --git a/user/Readme.md b/user/Readme.md new file mode 100644 index 0000000..64d9f9e --- /dev/null +++ b/user/Readme.md @@ -0,0 +1,10 @@ +#### 简介 + 用户模块主要包含以下功能: +* 用户管理 +* 角色管理 +* 资源管理 +* 部门管理 +* 职位管理(非必须) +* 菜单资源管理 +* 认证管理 + \ No newline at end of file diff --git a/user/pom.xml b/user/pom.xml new file mode 100644 index 0000000..c17d299 --- /dev/null +++ b/user/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + com.njcn.gather + CN_Tool + 1.0.0 + + + com.njcn.gather + user + + + + com.njcn + njcn-common + 0.0.1 + + + + com.njcn + mybatis-plus + 0.0.1 + + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + + com.alibaba + fastjson + 1.2.83 + + + + + + diff --git a/user/src/main/java/com/njcn/gather/user/pojo/constant/FunctionConst.java b/user/src/main/java/com/njcn/gather/user/pojo/constant/FunctionConst.java new file mode 100644 index 0000000..f8311f7 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/pojo/constant/FunctionConst.java @@ -0,0 +1,33 @@ +package com.njcn.gather.user.pojo.constant; + +/** + * @author caozehui + * @data 2024/11/12 + */ +public interface FunctionConst { + /** + * 资源类型:0-菜单 + */ + int TYPE_MENU =0; + + /** + * 资源类型:1-按钮 + */ + int TYPE_BUTTON =1; + + /** + * 资源类型:2-公共资源 + */ + int TYPE_PUBLIC =2; + + /** + * 资源类型:3-服务间调用资源 + */ + int TYPE_SERVICE_INVOKE_FUNCTION =3; + + /** + * 顶级父节点ID + */ + String FATHER_PID = "0"; + +} diff --git a/user/src/main/java/com/njcn/gather/user/pojo/constant/RoleConst.java b/user/src/main/java/com/njcn/gather/user/pojo/constant/RoleConst.java new file mode 100644 index 0000000..29c567e --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/pojo/constant/RoleConst.java @@ -0,0 +1,28 @@ +package com.njcn.gather.user.pojo.constant; + +/** + * @author caozehui + * @data 2024/11/11 + */ +public interface RoleConst { + /** + * 角色类型:0-超级管理员 + */ + int TYPE_SUPER_ADMINISTRATOR = 0; + + /** + * 角色类型:1-管理员 + */ + int TYPE_ADMINISTRATOR = 1; + + /** + * 角色类型:2-用户 + */ + int TYPE_USER = 2; + + /** + * 角色类型:3-APP角色 + */ + int TYPE_APP = 3; + +} diff --git a/user/src/main/java/com/njcn/gather/user/pojo/constant/UserConst.java b/user/src/main/java/com/njcn/gather/user/pojo/constant/UserConst.java new file mode 100644 index 0000000..48baa69 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/pojo/constant/UserConst.java @@ -0,0 +1,16 @@ +package com.njcn.gather.user.pojo.constant; + +/** + * @author caozehui + * @data 2024/11/11 + */ +public interface UserConst { + Integer STATE_DELETE = 0; + Integer STATE_ENABLE = 1; + Integer STATE_LOCKED = 2; + Integer STATE_WAITING_FOR_APPROVAL = 3; + Integer STATE_SLEEPING = 4; + Integer STATE_PASSWORD_EXPIRED = 5; + + String SUPER_ADMIN = "root"; +} diff --git a/user/src/main/java/com/njcn/gather/user/pojo/constant/UserValidMessage.java b/user/src/main/java/com/njcn/gather/user/pojo/constant/UserValidMessage.java new file mode 100644 index 0000000..a4c593b --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/pojo/constant/UserValidMessage.java @@ -0,0 +1,49 @@ +package com.njcn.gather.user.pojo.constant; + +/** + * @author caozehui + * @data 2024/11/8 + */ +public interface UserValidMessage { + + String ID_NOT_BLANK = "id不能为空,请检查id参数"; + + String ID_FORMAT_ERROR = "id格式错误,请检查id参数"; + + String DEPT_ID_FORMAT_ERROR = "部门id格式错误,请检查deptId参数"; + + String NAME_NOT_BLANK = "名称不能为空,请检查name参数"; + + String NAME_FORMAT_ERROR = "名称格式错误,请检查name参数"; + + String CODE_NOT_BLANK = "编码不能为空,请检查code参数"; + + String LOGIN_NAME_NOT_BLANK = "登录名不能为空,请检查loginName参数"; + + String LOGIN_NAME_FORMAT_ERROR = "登录名格式错误,需以字母开头,长度为3-16位的字母或数字"; + + String PASSWORD_NOT_BLANK = "密码不能为空,请检查password参数"; + + String PASSWORD_FORMAT_ERROR = "密码格式错误,需要包含特殊字符字母数字8-16位"; + + String PHONE_FORMAT_ERROR = "电话号码格式错误,请检查phone参数"; + + String EMAIL_FORMAT_ERROR = "邮箱格式错误,请检查email参数"; + + String OLD_PASSWORD_NOT_BLANK = "旧密码不能为空,请检查oldPassword参数"; + + String NEW_PASSWORD_NOT_BLANK = "新密码不能为空,请检查newPassword参数"; + + String PID_NOT_BLANK = "父节点id不能为空,请检查pid参数"; + + String SORT_NOT_NULL = "排序不能为空,请检查sort参数"; + + String TYPE_NOT_BLANK = "类型不能为空,请检查type参数"; + + String PARAM_FORMAT_ERROR = "参数值非法"; + + String LOGIN_FAILED = "登录失败,用户名或密码错误"; + + String FUNCTION_NAME_FORMAT_ERROR = "菜单名称格式错误,只能包含字母、数字、中文、下划线、中划线、空格,长度为1-32个字符"; + String FUNCTION_CODE_FORMAT_ERROR = "菜单编码格式错误,只能包含字母、数字、下划线、中划线、空格,长度为1-32个字符"; +} diff --git a/user/src/main/java/com/njcn/gather/user/pojo/enums/UserResponseEnum.java b/user/src/main/java/com/njcn/gather/user/pojo/enums/UserResponseEnum.java new file mode 100644 index 0000000..3a5c747 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/pojo/enums/UserResponseEnum.java @@ -0,0 +1,37 @@ +package com.njcn.gather.user.pojo.enums; + +import lombok.Getter; + +/** + * @author caozehui + * @data 2024/11/9 + */ +@Getter +public enum UserResponseEnum { + LOGIN_NAME_REPEAT("A010001", "登录名重复,请检查loginName参数"), + REGISTER_PHONE_FAIL("A010002", "该号码已被注册"), + USER_NAME_REPEAT("A010003", "用户名重复,请检查name参数"), + REGISTER_EMAIL_FAIL("A010004", "该邮箱已被注册"), + NAME_OR_CODE_REPEAT("A010005", "名称或编码已存在"), + EXISTS_SAME_MENU_CHILDREN("A010006", "该层级下已存在相同名称或相同编码或相同路径或相同组件地址的菜单"), + EXISTS_CHILDREN_NOT_UPDATE("A010008", "该菜单下存在子节点,无法将菜单修改为按钮"), + EXISTS_CHILDREN_NOT_DELETE("A010007", "该节点下存在子节点,无法删除"), + SUPER_ADMINSTRATOR_ROLE_CANNOT_UPDATE("A010009", "禁止修改超级管理员角色"), + SUPER_ADMINSTRATOR_ROLE_CANNOT_DELETE("A010009", "禁止删除超级管理员角色"), + SUPER_ADMIN_CANNOT_DELETE("A010010", "禁止删除超级管理员用户"), + COMPONENT_NOT_BLANK("A010011", "组件地址不能为空"), + FUNCTION_PATH_FORMAT_ERROR("A010012", "菜单路由地址格式错误,只能包含字母、数字、下划线、中划线、空格、斜线、反斜线,长度为1-32个字符"), + FUNCTION_COMPONENT_FORMAT_ERROR("A010013","菜单组件地址格式错误,只能包含字母、数字、下划线、中划线、空格、斜线、反斜线,长度为1-32个字符" ), + SUPER_ADMIN_REPEAT("A010013","超级管理员已存在,请勿重复添加" ), + RSA_DECRYT_ERROR("A010014","RSA解密失败" ), + PASSWORD_SAME("A010015", "新密码不能与旧密码相同"), + OLD_PASSWORD_ERROR("A010016", "旧密码错误"), ; + + private String code; + private String message; + + UserResponseEnum(String code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/controller/AuthController.java b/user/src/main/java/com/njcn/gather/user/user/controller/AuthController.java new file mode 100644 index 0000000..0a2a9fd --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/controller/AuthController.java @@ -0,0 +1,158 @@ +package com.njcn.gather.user.user.controller; + +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.njcn.common.bean.CustomCacheUtil; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.constant.SecurityConstants; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.JwtUtil; +import com.njcn.common.utils.LogUtil; +import com.njcn.common.utils.RSAUtil; +import com.njcn.gather.user.pojo.constant.UserValidMessage; +import com.njcn.gather.user.pojo.enums.UserResponseEnum; +import com.njcn.gather.user.user.pojo.param.SysUserParam; +import com.njcn.gather.user.user.pojo.po.SysUser; +import com.njcn.gather.user.user.pojo.po.Token; +import com.njcn.gather.user.user.service.ISysUserService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import com.njcn.web.utils.RequestUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.security.KeyPair; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + + +@Slf4j +@RestController +@Api(tags = "登录-注销") +@RequestMapping("/admin") +@RequiredArgsConstructor +public class AuthController extends BaseController { + + private static final String LOGIN_SESSION_KEY_PREFIX = "login_session:"; + + private final ISysUserService sysUserService; + private final CustomCacheUtil customCacheUtil; + private KeyPair keyPair; + + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.AUTHENTICATE) + @PostMapping("/login") + @ApiOperation("登录") + public HttpResult login(@RequestBody SysUserParam.LoginParam param, HttpServletRequest request) { + String methodDescribe = getMethodDescribe("login"); + LogUtil.njcnDebug(log, "{},登录参数为:{}", methodDescribe, param); + byte[] decode = Base64.getDecoder().decode(param.getUsername()); + String username = new String(decode); + String password = null; + + try { + password = RSAUtil.decrypt(param.getPassword(), keyPair.getPrivate()); + } catch (Exception e) { + throw new BusinessException(UserResponseEnum.RSA_DECRYT_ERROR); + } + // 因不确定是否能登陆成功先将登陆名保存到request,一遍记录谁执行了登录操作 + request.setAttribute(SecurityConstants.AUTHENTICATE_USERNAME, username); + SysUser user = sysUserService.getUserByLoginNameAndPassword(username, password); + if (ObjectUtil.isNull(user)) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, null, UserValidMessage.LOGIN_FAILED); + } else { + String accessToken = JwtUtil.getAccessToken(user.getId(), user.getLoginName()); + String refreshToken = JwtUtil.getRefreshToken(accessToken); + Token token = new Token(); + token.setAccessToken(accessToken); + token.setRefreshToken(refreshToken); + + Map map = new HashMap<>(); + map.put("name", user.getName()); + map.put("id", user.getId()); + map.put("loginName", user.getLoginName()); + + token.setUserInfo(map); + + customCacheUtil.putWithExpireTime(accessToken, JSON.toJSONString(user), DateUnit.DAY.getMillis() * Integer.MAX_VALUE); + customCacheUtil.putWithExpireTime(buildLoginSessionKey(user.getId()), user.getId(), DateUnit.DAY.getMillis() * Integer.MAX_VALUE); + sysUserService.updateLoginTime(user.getId()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, token, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_SERIOUS, operateType = OperateType.LOGOUT) + @ApiOperation("注销登录") + @PostMapping("/logout") + public HttpResult logout() { + String methodDescribe = getMethodDescribe("logout"); + LogUtil.njcnDebug(log, "{},注销登录", methodDescribe); + String accessToken = RequestUtil.getAccessToken(); + if (StrUtil.isNotBlank(accessToken)) { + Map tokenInfo = JwtUtil.parseToken(accessToken); + String userId = (String) tokenInfo.get(SecurityConstants.USER_ID); + customCacheUtil.remove(accessToken); + if (StrUtil.isNotBlank(userId)) { + customCacheUtil.remove(buildLoginSessionKey(userId)); + } + + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, null, methodDescribe); + } + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, null, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @ApiOperation("刷新accessToken") + @GetMapping("/refreshToken") + public HttpResult refreshToken(HttpServletRequest request) { + String methodDescribe = getMethodDescribe("refreshToken"); + LogUtil.njcnDebug(log, "{},刷新token", methodDescribe); + String accessToken = RequestUtil.getAccessToken(); + + Token token = new Token(); + if (StrUtil.isNotBlank(accessToken)) { + Map map = JwtUtil.parseToken(accessToken); + String userId = (String) map.get(SecurityConstants.USER_ID); + SysUser user = sysUserService.getById(userId); + String accessTokenNew = JwtUtil.getAccessToken(userId, user.getLoginName()); + request.setAttribute(SecurityConstants.AUTHENTICATE_USERNAME, user.getLoginName()); +// String refreshTokenNew = JwtUtil.getRefreshToken(accessTokenNew); + + token.setAccessToken(accessTokenNew); + token.setRefreshToken(accessToken); + + customCacheUtil.putWithExpireTime(accessTokenNew, JSON.toJSONString(user), DateUnit.DAY.getMillis() * Integer.MAX_VALUE); + customCacheUtil.putWithExpireTime(buildLoginSessionKey(userId), userId, DateUnit.DAY.getMillis() * Integer.MAX_VALUE); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, token, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, null, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @ApiOperation("获取RSA公钥") + @GetMapping("/getPublicKey") + public HttpResult publicKey(@RequestParam("username") String username, HttpServletRequest request) throws Exception { + String methodDescribe = getMethodDescribe("publicKey"); + LogUtil.njcnDebug(log, "{},获取RSA公钥", methodDescribe); + // 因不确定是否能登陆成功先将登陆名保存到request,一遍记录谁执行了登录操作 + request.setAttribute(SecurityConstants.AUTHENTICATE_USERNAME, username); + keyPair = RSAUtil.generateKeyPair(); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, RSAUtil.publicKeyToString(keyPair.getPublic()), methodDescribe); + } + + public static String buildLoginSessionKey(String userId) { + return LOGIN_SESSION_KEY_PREFIX + userId; + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/controller/SysFunctionController.java b/user/src/main/java/com/njcn/gather/user/user/controller/SysFunctionController.java new file mode 100644 index 0000000..2b8f303 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/controller/SysFunctionController.java @@ -0,0 +1,168 @@ +package com.njcn.gather.user.user.controller; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.constant.SecurityConstants; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.JwtUtil; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.user.user.pojo.param.SysFunctionParam; +import com.njcn.gather.user.user.pojo.param.SysRoleParam; +import com.njcn.gather.user.user.pojo.po.SysFunction; +import com.njcn.gather.user.user.pojo.vo.MenuVO; +import com.njcn.gather.user.user.service.ISysFunctionService; +import com.njcn.gather.user.user.service.ISysRoleFunctionService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + + +/** + * @author caozehui + * @date 2024-11-15 + */ +@Slf4j +@Api(tags = "菜单(资源)管理") +@RestController +@RequestMapping("/sysFunction") +@RequiredArgsConstructor +public class SysFunctionController extends BaseController { + private final ISysFunctionService sysFunctionService; + private final ISysRoleFunctionService sysRoleFunctionService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getTree") + @ApiOperation("按照名称模糊查询菜单树") + @ApiImplicitParam(name = "keyword", value = "查询参数", required = true) + public HttpResult> getFunctionTreeByKeyword(@RequestParam @Validated String keyword) { + String methodDescribe = getMethodDescribe("getFunctionTreeByKeyword"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, keyword); + List result = sysFunctionService.getFunctionTreeByKeyword(keyword); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/functionTreeNoButton") + @ApiOperation("菜单树-不包括按钮") + public HttpResult> getFunctionTreeNoButton() { + String methodDescribe = getMethodDescribe("getFunctionTreeNoButton"); + List list = sysFunctionService.getFunctionTree(false); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, list, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) + @PostMapping("/add") + @ApiOperation("新增菜单") + @ApiImplicitParam(name = "functionParam", value = "菜单数据", required = true) + public HttpResult add(@RequestBody @Validated SysFunctionParam functionParam) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},菜单数据为:{}", methodDescribe, functionParam); + boolean result = sysFunctionService.addFunction(functionParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改菜单") + @ApiImplicitParam(name = "functionParam", value = "菜单数据", required = true) + public HttpResult update(@RequestBody @Validated SysFunctionParam.UpdateParam functionParam) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{},更新的菜单信息为:{}", methodDescribe, functionParam); + boolean result = sysFunctionService.updateFunction(functionParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DELETE) + @PostMapping("/delete") + @ApiOperation("删除菜单") + @ApiImplicitParam(name = "id", value = "菜单id", required = true) + public HttpResult delete(@RequestParam String id) { + String methodDescribe = getMethodDescribe("delete"); + LogUtil.njcnDebug(log, "{},删除的菜单id为:{}", methodDescribe, id); + boolean result = sysFunctionService.deleteFunction(id); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getMenu") + @ApiOperation("获取菜单") + public HttpResult> getMenu(HttpServletRequest request) { + String methodDescribe = getMethodDescribe("getMenu"); + String tokenStr = request.getHeader(SecurityConstants.AUTHORIZATION_KEY); + if (StrUtil.isNotBlank(tokenStr)) { + tokenStr = tokenStr.replace(SecurityConstants.AUTHORIZATION_PREFIX, Strings.EMPTY); + String userId = (String) (JwtUtil.parseToken(tokenStr).get("userId")); + List list = sysFunctionService.getMenuByUserId(userId); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, list, methodDescribe); + } + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, null, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getButton") + @ApiOperation("获取按钮") + public HttpResult>> getButton(HttpServletRequest request) { + String methodDescribe = getMethodDescribe("getButton"); + String tokenStr = request.getHeader(SecurityConstants.AUTHORIZATION_KEY); + if (StrUtil.isNotBlank(tokenStr)) { + tokenStr = tokenStr.replace(SecurityConstants.AUTHORIZATION_PREFIX, Strings.EMPTY); + String userId = (String) JwtUtil.parseToken(tokenStr).get("userId"); + Map> map = sysFunctionService.getButtonByUserId(userId); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, map, methodDescribe); + } + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, null, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @PostMapping("/getFunctionsByRoleId") + @ApiOperation("获取角色id绑定的菜单(资源)") + @ApiImplicitParam(name = "id", value = "角色id", required = true) + public HttpResult> getFunctionsByRoleId(@RequestParam @Validated String id) { + String methodDescribe = getMethodDescribe("getFunctionsByRoleId"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, id); + List sysFunctions = sysRoleFunctionService.listFunctionByRoleId(id); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, sysFunctions, methodDescribe); + } + + @OperateInfo(operateType = OperateType.UPDATE, info = LogEnum.SYSTEM_MEDIUM) + @PostMapping("/assignFunctionByRoleId") + @ApiOperation("角色分配菜单") + @ApiImplicitParam(name = "param", value = "角色信息", required = true) + public HttpResult assignFunctionByRoleId(@RequestBody @Validated SysRoleParam.RoleBindFunction param) { + String methodDescribe = getMethodDescribe("assignFunctionByRoleId"); + LogUtil.njcnDebug(log, "{},传入的角色id和资源id集合为:{}", methodDescribe, param); + boolean result = sysRoleFunctionService.updateRoleFunction(param.getRoleId(), param.getFunctionIds()); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/controller/SysRoleController.java b/user/src/main/java/com/njcn/gather/user/user/controller/SysRoleController.java new file mode 100644 index 0000000..cf7cfe2 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/controller/SysRoleController.java @@ -0,0 +1,104 @@ +package com.njcn.gather.user.user.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.user.user.pojo.param.SysRoleParam; +import com.njcn.gather.user.user.pojo.po.SysRole; +import com.njcn.gather.user.user.service.ISysRoleService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +/** + * @author caozehui + * @date 2024-11-11 + */ +@Slf4j +@Api(tags = "角色管理") +@RestController +@RequestMapping("/sysRole") +@RequiredArgsConstructor +public class SysRoleController extends BaseController { + private final ISysRoleService sysRoleService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @PostMapping("/list") + @ApiOperation("分页查询角色") + @ApiImplicitParam(name = "queryParam", value = "查询参数", required = true) + public HttpResult> list(@RequestBody @Validated SysRoleParam.QueryParam queryParam) { + String methodDescribe = getMethodDescribe("list"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, queryParam); + Page result = sysRoleService.listRole(queryParam); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) + @PostMapping("/add") + @ApiOperation("新增角色信息") + @ApiImplicitParam(name = "sysRoleParam", value = "角色信息", required = true) + public HttpResult add(@RequestBody @Validated SysRoleParam sysRoleParam) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},角色信息数据为:{}", methodDescribe, sysRoleParam); + boolean result = sysRoleService.addRole(sysRoleParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改角色信息") + @ApiImplicitParam(name = "updateParam", value = "角色信息", required = true) + public HttpResult update(@RequestBody @Validated SysRoleParam.UpdateParam updateParam) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{},角色信息数据为:{}", methodDescribe, updateParam); + boolean result = sysRoleService.updateRole(updateParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DELETE) + @PostMapping("/delete") + @ApiOperation("删除角色信息") + @ApiImplicitParam(name = "ids", value = "角色id集合", required = true) + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + LogUtil.njcnDebug(log, "{},角色信息数据为:{}", methodDescribe, ids); + boolean result = sysRoleService.deleteRole(ids); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/simpleList") + @ApiOperation("查询所有角色作为下拉框") + public HttpResult> simpleList() { + String methodDescribe = getMethodDescribe("simpleList"); + List result = sysRoleService.simpleList(); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/controller/SysUserController.java b/user/src/main/java/com/njcn/gather/user/user/controller/SysUserController.java new file mode 100644 index 0000000..54cddc7 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/controller/SysUserController.java @@ -0,0 +1,134 @@ +package com.njcn.gather.user.user.controller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.user.user.pojo.param.SysUserParam; +import com.njcn.gather.user.user.pojo.po.SysRole; +import com.njcn.gather.user.user.pojo.po.SysUser; +import com.njcn.gather.user.user.service.ISysUserRoleService; +import com.njcn.gather.user.user.service.ISysUserService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + + +/** + * @author caozehui + * @since 2024-11-08 + */ +@Slf4j +@Api(tags = "用户管理") +@RestController +@RequestMapping("/sysUser") +@RequiredArgsConstructor +public class SysUserController extends BaseController { + + private final ISysUserService sysUserService; + private final ISysUserRoleService sysUserRoleService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @PostMapping("/list") + @ApiOperation("分页查询用户列表") + @ApiImplicitParam(name = "queryParam", value = "查询参数", required = true) + public HttpResult> list(@RequestBody @Validated SysUserParam.SysUserQueryParam queryParam) { + String methodDescribe = getMethodDescribe("list"); + LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, queryParam); + Page result = sysUserService.listUser(queryParam); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) + @PostMapping("/add") + @ApiOperation("新增用户") + @ApiImplicitParam(name = "addUserParam", value = "新增用户", required = true) + public HttpResult add(@RequestBody @Validated SysUserParam.SysUserAddParam addUserParam) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},用户数据为:{}", methodDescribe, addUserParam); + boolean result = sysUserService.addUser(addUserParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/update") + @ApiOperation("修改用户") + @ApiImplicitParam(name = "updateUserParam", value = "修改用户", required = true) + public HttpResult update(@RequestBody @Validated SysUserParam.SysUserUpdateParam updateUserParam) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{},用户数据为:{}", methodDescribe, updateUserParam); + boolean result = sysUserService.updateUser(updateUserParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.DELETE) + @PostMapping("/delete") + @ApiOperation("批量删除用户") + @ApiImplicitParam(name = "ids", value = "用户id", required = true) + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + LogUtil.njcnDebug(log, "{},用户id为:{}", methodDescribe, String.join(StrUtil.COMMA, ids)); + boolean result = sysUserService.deleteUser(ids); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) + @PostMapping("/updatePassword") + @ApiOperation("修改密码") + @ApiImplicitParam(name = "param", value = "修改密码参数", required = true) + public HttpResult updatePassword(@RequestBody @Validated SysUserParam.SysUserUpdatePasswordParam param) { + String methodDescribe = getMethodDescribe("updatePassword"); + LogUtil.njcnDebug(log, "{},用户id:{},用户旧密码:{},新密码:{}", methodDescribe, param.getId(), param.getOldPassword(), param.getNewPassword()); + boolean result = sysUserService.updatePassword(param); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } else { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/getAll") + @ApiOperation("获取所有用户") + public HttpResult> getAll() { + String methodDescribe = getMethodDescribe("getAll"); + LogUtil.njcnDebug(log, "{},查询所有用户", methodDescribe); + List result = sysUserService.lambdaQuery().eq(SysUser::getState, DataStateEnum.ENABLE.getCode()).list(); + result.forEach(user -> { + user.setPassword(null); + List sysRoles = sysUserRoleService.listRoleByUserId(user.getId()); + user.setRoleIds(sysRoles.stream().map(SysRole::getId).collect(Collectors.toList())); + user.setRoleCodes(sysRoles.stream().map(SysRole::getCode).collect(Collectors.toList())); + user.setRoleNames(sysRoles.stream().map(SysRole::getName).collect(Collectors.toList())); + }); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java b/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java new file mode 100644 index 0000000..870afdc --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java @@ -0,0 +1,99 @@ +package com.njcn.gather.user.user.filter; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.njcn.common.bean.CustomCacheUtil; +import com.njcn.common.pojo.constant.SecurityConstants; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.JwtUtil; +import com.njcn.gather.user.user.controller.AuthController; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @author caozehui + * @data 2024/11/18 + */ +@Slf4j +@Component +public class AuthGlobalFilter implements Filter, Ordered { + // Key cleanup point: report generation is a removed business capability and + // must no longer bypass global authentication. + private static final List IGNORE_URI = Arrays.asList( + "/doc.html", + "/v3/api-docs", + "/admin/login", + "/admin/getPublicKey" + ); + + @Resource + private CustomCacheUtil customCacheUtil; + + @Override + public int getOrder() { + return 0; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + res.setCharacterEncoding("UTF-8"); + res.setContentType("application/json; charset=utf-8"); + + String requestURI = req.getRequestURI(); + if (IGNORE_URI.contains(requestURI) || requestURI.startsWith("/webjars") || requestURI.startsWith("/swagger-resources")) { + filterChain.doFilter(req, res); + } else { + String accessTokenStr = req.getHeader(SecurityConstants.AUTHORIZATION_KEY); + String isRefreshToken = req.getHeader(SecurityConstants.IS_REFRESH_TOKEN); + if (StrUtil.isBlank(accessTokenStr) || !accessTokenStr.startsWith(SecurityConstants.AUTHORIZATION_PREFIX)) { + HttpResult httpResult = new HttpResult<>(CommonResponseEnum.PARSE_TOKEN_ERROR.getCode(), CommonResponseEnum.PARSE_TOKEN_ERROR.getMessage()); + res.getWriter().write(JSON.toJSONString(httpResult)); //前端重定向到登录页面 + return; + } + String accessToken = accessTokenStr.substring(SecurityConstants.AUTHORIZATION_PREFIX.length()); + try { + if (StrUtil.isBlank(accessToken) || !JwtUtil.verifyToken(accessToken)) { + HttpResult httpResult = new HttpResult<>(CommonResponseEnum.PARSE_TOKEN_ERROR.getCode(), CommonResponseEnum.PARSE_TOKEN_ERROR.getMessage()); + res.getWriter().write(JSON.toJSONString(httpResult)); + } else if (JwtUtil.isExpired(accessToken)) { + if ("true".equals(isRefreshToken)) { + HttpResult httpResult = new HttpResult<>(CommonResponseEnum.PARSE_TOKEN_ERROR.getCode(), CommonResponseEnum.PARSE_TOKEN_ERROR.getMessage()); + res.getWriter().write(JSON.toJSONString(httpResult)); + } else { + HttpResult httpResult = new HttpResult<>(CommonResponseEnum.TOKEN_EXPIRE_JWT.getCode(), CommonResponseEnum.TOKEN_EXPIRE_JWT.getMessage()); + res.getWriter().write(JSON.toJSONString(httpResult)); + } + } else { + Map tokenInfo = JwtUtil.parseToken(accessToken); + String userId = (String) tokenInfo.get(SecurityConstants.USER_ID); + String loginSession = StrUtil.isBlank(userId) ? null : customCacheUtil.get(AuthController.buildLoginSessionKey(userId), false); + if (StrUtil.isBlank(loginSession)) { + HttpResult httpResult = new HttpResult<>(CommonResponseEnum.PARSE_TOKEN_ERROR.getCode(), CommonResponseEnum.PARSE_TOKEN_ERROR.getMessage()); + res.getWriter().write(JSON.toJSONString(httpResult)); + } else { + filterChain.doFilter(req, res); + } + } + } catch (Exception e) { + HttpResult httpResult = new HttpResult<>(CommonResponseEnum.PARSE_TOKEN_ERROR.getCode(), CommonResponseEnum.PARSE_TOKEN_ERROR.getMessage()); + res.getWriter().write(JSON.toJSONString(httpResult)); + } + } + } +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/SysFunctionMapper.java b/user/src/main/java/com/njcn/gather/user/user/mapper/SysFunctionMapper.java new file mode 100644 index 0000000..34c578d --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/SysFunctionMapper.java @@ -0,0 +1,29 @@ +package com.njcn.gather.user.user.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.user.user.pojo.po.SysFunction; +import com.njcn.gather.user.user.pojo.vo.MenuVO; + +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-12 + */ +public interface SysFunctionMapper extends MPJBaseMapper { + + /** + * 根据用户id获取菜单列表 + * @param userId 用户id + * @return 菜单列表 + */ + List getMenuByUserId(String userId); + + /* + * 根据用户id获取按钮列表 + * @param userId 用户id + * @return 按钮列表 + */ + List getButtonByUserId(String userId); +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/SysRoleFunctionMapper.java b/user/src/main/java/com/njcn/gather/user/user/mapper/SysRoleFunctionMapper.java new file mode 100644 index 0000000..07111bd --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/SysRoleFunctionMapper.java @@ -0,0 +1,24 @@ +package com.njcn.gather.user.user.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.user.user.pojo.po.SysFunction; +import com.njcn.gather.user.user.pojo.po.SysRoleFunction; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-12 + */ +public interface SysRoleFunctionMapper extends MPJBaseMapper { + + /** + * 根据角色id获取角色拥有的菜单(资源)列表 + * + * @param roleId 角色id + * @return 角色拥有的菜单(资源)列表 + */ + List getFunctionListByRoleId(@Param("roleId") String roleId); +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/SysRoleMapper.java b/user/src/main/java/com/njcn/gather/user/user/mapper/SysRoleMapper.java new file mode 100644 index 0000000..3a4462f --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/SysRoleMapper.java @@ -0,0 +1,13 @@ +package com.njcn.gather.user.user.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.user.user.pojo.po.SysRole; + +/** + * @author caozehui + * @date 2024-11-11 + */ +public interface SysRoleMapper extends MPJBaseMapper { + +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/SysUserMapper.java b/user/src/main/java/com/njcn/gather/user/user/mapper/SysUserMapper.java new file mode 100644 index 0000000..8b4bc15 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/SysUserMapper.java @@ -0,0 +1,15 @@ +package com.njcn.gather.user.user.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.user.user.pojo.param.SysUserParam; +import com.njcn.gather.user.user.pojo.po.SysUser; + +/** + * @author caozehui + * @since 2024-11-08 + */ +public interface SysUserMapper extends MPJBaseMapper { + +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/SysUserRoleMapper.java b/user/src/main/java/com/njcn/gather/user/user/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..411c500 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/SysUserRoleMapper.java @@ -0,0 +1,22 @@ +package com.njcn.gather.user.user.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.user.user.pojo.po.SysRole; +import com.njcn.gather.user.user.pojo.po.SysUserRole; + +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-12 + */ +public interface SysUserRoleMapper extends MPJBaseMapper { + /** + * 根据用户id获取角色详情 + * + * @param userId 用户id + * @return 角色结果集 + */ + List getRoleListByUserId(String userId); +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysFunctionMapper.xml b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysFunctionMapper.xml new file mode 100644 index 0000000..d944cb5 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysFunctionMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysRoleFunctionMapper.xml b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysRoleFunctionMapper.xml new file mode 100644 index 0000000..43dd6b7 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysRoleFunctionMapper.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysRoleMapper.xml b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysRoleMapper.xml new file mode 100644 index 0000000..e6f64ef --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysRoleMapper.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysUserMapper.xml b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysUserMapper.xml new file mode 100644 index 0000000..40b1aa4 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysUserMapper.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysUserRoleMapper.xml b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysUserRoleMapper.xml new file mode 100644 index 0000000..3cb25f6 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/mapper/mapping/SysUserRoleMapper.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysFunctionParam.java b/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysFunctionParam.java new file mode 100644 index 0000000..24dbc55 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysFunctionParam.java @@ -0,0 +1,75 @@ +package com.njcn.gather.user.user.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.user.pojo.constant.UserValidMessage; +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; +import javax.validation.constraints.Pattern; + +/** + * @author caozehui + * @data 2024/11/12 + */ +@Data +public class SysFunctionParam { + @ApiModelProperty("父节点") + @NotBlank(message = UserValidMessage.PID_NOT_BLANK) + private String pid; + + @ApiModelProperty("名称") + @NotBlank(message = UserValidMessage.NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.FUNCTION_NAME_REGEX, message = UserValidMessage.FUNCTION_NAME_FORMAT_ERROR) + private String name; + + @ApiModelProperty("编码") + @NotBlank(message = UserValidMessage.CODE_NOT_BLANK) + @Pattern(regexp = PatternRegex.FUNCTION_CODE_REGEX, message = UserValidMessage.FUNCTION_CODE_FORMAT_ERROR) + private String code; + + @ApiModelProperty("路径") + private String path; + + @ApiModelProperty("组件地址") + private String component; + + @ApiModelProperty("图标") + private String icon; + + @ApiModelProperty("排序") + @NotNull(message = UserValidMessage.SORT_NOT_NULL) + @Range(min = 0, max = 999, message = UserValidMessage.PARAM_FORMAT_ERROR) + private Integer sort; + + @ApiModelProperty("资源类型") + @NotNull(message = UserValidMessage.TYPE_NOT_BLANK) + @Range(min = 0, max = 3, message = UserValidMessage.PARAM_FORMAT_ERROR) + private Integer type; + + @ApiModelProperty("描述") + private String remark; + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("名称") + @Pattern(regexp = PatternRegex.FUNCTION_NAME_REGEX, message = UserValidMessage.FUNCTION_NAME_FORMAT_ERROR) + private String name; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class UpdateParam extends SysFunctionParam { + + @ApiModelProperty("id") + @NotBlank(message = UserValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = UserValidMessage.ID_FORMAT_ERROR) + private String id; + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysRoleParam.java b/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysRoleParam.java new file mode 100644 index 0000000..16a4579 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysRoleParam.java @@ -0,0 +1,83 @@ +package com.njcn.gather.user.user.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.user.pojo.constant.UserValidMessage; +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.util.List; + +/** + * @author caozehui + * @data 2024/11/11 + */ +@Data +public class SysRoleParam { + + + @ApiModelProperty("名称") + @NotBlank(message = UserValidMessage.NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.DEPT_NAME_REGEX, message = UserValidMessage.NAME_FORMAT_ERROR) + private String name; + + @ApiModelProperty("编码") + @NotNull(message = UserValidMessage.CODE_NOT_BLANK) + private String code; + + /** + * 角色类型 0:超级管理员;1:管理员;2:普通用户 + */ + @ApiModelProperty("类型") + @Range(min = 0, max = 2, message = UserValidMessage.PARAM_FORMAT_ERROR) + private Integer type; + + @ApiModelProperty("描述") + private String remark; + + /** + * 更新操作实体 + */ + @Data + @EqualsAndHashCode(callSuper = true) + public static class UpdateParam extends SysRoleParam { + + @ApiModelProperty("id") + @NotBlank(message = UserValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = UserValidMessage.ID_FORMAT_ERROR) + private String id; + + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("编码") + private String code; + + @ApiModelProperty("类型") + private Integer type; + } + + /** + * 角色绑定菜单(资源)参数 + */ + @Data + public static class RoleBindFunction { + @ApiModelProperty("角色id") + @NotBlank(message = UserValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = UserValidMessage.ID_FORMAT_ERROR) + private String roleId; + + @ApiModelProperty("菜单ids") + private List functionIds; + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysUserParam.java b/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysUserParam.java new file mode 100644 index 0000000..1f796d4 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/param/SysUserParam.java @@ -0,0 +1,104 @@ +package com.njcn.gather.user.user.pojo.param; + +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.gather.user.pojo.constant.UserValidMessage; +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.util.List; + +/** + * @author caozehui + * @data 2024/11/8 + */ +@Data +public class SysUserParam { + + @ApiModelProperty("用户名(别名)") + @NotBlank(message = UserValidMessage.NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.USERNAME_REGEX, message = UserValidMessage.NAME_FORMAT_ERROR) + private String name; + + @ApiModelProperty("部门Id") + private String deptId; + + @ApiModelProperty("电话号码") + @Pattern(regexp = PatternRegex.PHONE_REGEX_OR_NULL, message = UserValidMessage.PHONE_FORMAT_ERROR) + private String phone; + + @ApiModelProperty("邮箱") + @Pattern(regexp = PatternRegex.EMAIL_REGEX_OR_NULL, message = UserValidMessage.EMAIL_FORMAT_ERROR) + private String email; + + @ApiModelProperty("角色ids") + private List roleIds; + + @Data + @EqualsAndHashCode(callSuper = true) + public static class SysUserAddParam extends SysUserParam { + + @ApiModelProperty("登录名") + @NotBlank(message = UserValidMessage.LOGIN_NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.LOGIN_NAME_REGEX, message = UserValidMessage.LOGIN_NAME_FORMAT_ERROR) + private String loginName; + + @ApiModelProperty("密码") + @NotBlank(message = UserValidMessage.PASSWORD_NOT_BLANK) + @Pattern(regexp = PatternRegex.PASSWORD_REGEX, message = UserValidMessage.PASSWORD_FORMAT_ERROR) + private String password; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class SysUserUpdateParam extends SysUserParam { + + @ApiModelProperty("用户表Id") + @NotBlank(message = UserValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = UserValidMessage.ID_FORMAT_ERROR) + private String id; + + } + + @Data + public static class SysUserUpdatePasswordParam { + @ApiModelProperty("用户Id") + @NotBlank(message = UserValidMessage.ID_NOT_BLANK) + @Pattern(regexp = PatternRegex.SYSTEM_ID, message = UserValidMessage.ID_FORMAT_ERROR) + private String id; + + @ApiModelProperty("旧密码") + @NotBlank(message = UserValidMessage.OLD_PASSWORD_NOT_BLANK) + @Pattern(regexp = PatternRegex.PASSWORD_REGEX, message = UserValidMessage.PASSWORD_FORMAT_ERROR) + private String oldPassword; + + @ApiModelProperty("新密码") + @NotBlank(message = UserValidMessage.NEW_PASSWORD_NOT_BLANK) + @Pattern(regexp = PatternRegex.PASSWORD_REGEX, message = UserValidMessage.PASSWORD_FORMAT_ERROR) + private String newPassword; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class SysUserQueryParam extends BaseParam { + @ApiModelProperty("用户名(别名)") + private String name; + + } + + @Data + public static class LoginParam { + @ApiModelProperty("登录名") + @NotBlank(message = UserValidMessage.LOGIN_NAME_NOT_BLANK) + @Pattern(regexp = PatternRegex.LOGIN_NAME_REGEX, message = UserValidMessage.LOGIN_NAME_FORMAT_ERROR) + private String username; + + @ApiModelProperty("密码") + @NotBlank(message = UserValidMessage.PASSWORD_NOT_BLANK) + @Pattern(regexp = PatternRegex.PASSWORD_REGEX, message = UserValidMessage.PASSWORD_FORMAT_ERROR) + private String password; + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/MenuVO.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/MenuVO.java new file mode 100644 index 0000000..5f17760 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/MenuVO.java @@ -0,0 +1,39 @@ +package com.njcn.gather.user.user.pojo.po; + +import lombok.Data; + +import java.util.List; + +@Data +public class MenuVO { + + /** + * 路由菜单访问路径 + */ + private String path; + + /** + * 路由 name (对应页面组件 name, 可用作 KeepAlive 缓存标识 && 按钮权限筛选) + */ + private String name; + + /** + * 视图文件路径 + */ + private String component; + + /** + * 路由重定向地址 + */ + private String redirect; + + /** + * 路由菜单元信息 + */ + private MetaVO meta; + + /** + * 子集路由菜单信息 + */ + private List children; +} diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/MetaVO.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/MetaVO.java new file mode 100644 index 0000000..35f7113 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/MetaVO.java @@ -0,0 +1,49 @@ +package com.njcn.gather.user.user.pojo.po; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class MetaVO { + + /** + * 菜单和面包屑对应的图标 + */ + private String icon; + + /** + * 路由标题 (用作 document.title || 菜单的名称) + */ + private String title; + + /** + * 路由外链时填写的访问地址 + */ + @JsonProperty("isLink") + private String isLink; + + /** + * 是否在菜单中隐藏 (通常列表详情页需要隐藏) + */ + @JsonProperty("isHide") + private boolean isHide; + + /** + * 菜单是否全屏 (示例:数据大屏页面) + */ + @JsonProperty("isFull") + private boolean isFull; + + /** + * 菜单是否固定在标签页中 (首页通常是固定项) + */ + @JsonProperty("isAffix") + private boolean isAffix; + + /** + * 当前路由是否缓存 + */ + @JsonProperty("isKeepAlive") + private boolean isKeepAlive; + +} diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysFunction.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysFunction.java new file mode 100644 index 0000000..149ecf4 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysFunction.java @@ -0,0 +1,88 @@ +package com.njcn.gather.user.user.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_function") +public class SysFunction extends BaseEntity implements Serializable { + private static final long serialVersionUID = -30909841321495323L; + + /** + * 资源表Id + */ + private String id; + + /** + * 节点(0为根节点) + */ + private String pid; + + /** + * 上层所有节点 + */ + private String pids; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 路径 + */ + private String path; + + /** + * 组件地址 + */ + private String component; + + /** + * 图标(没有图标默认为:“Null”) + */ + private String icon; + + /** + * 排序 + */ + private Integer sort; + + /** + * 资源类型:0-菜单、1-按钮、2-公共资源、3-服务间调用资源 + */ + private Integer type; + + /** + * 权限资源描述 + */ + private String remark; + + /** + * 权限资源状态:0-删除 1-正常 + */ + private Integer state; + + /** + * 子节点 + */ + @TableField(exist = false) + private List children; +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysRole.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysRole.java new file mode 100644 index 0000000..bf8838e --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysRole.java @@ -0,0 +1,50 @@ +package com.njcn.gather.user.user.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * @author caozehui + * @date 2024-11-11 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_role") +public class SysRole extends BaseEntity implements Serializable { + private static final long serialVersionUID = 183697621480953314L; + + /** + * 角色表Id + */ + private String id; + + /** + * 角色名称 + */ + private String name; + + /** + * 编码,有需要用做匹配时候用(关联字典表id) + */ + private String code; + + /** + * 类型:0-超级管理员;1-管理员角色;2-普通角色,默认普通角色 + */ + private Integer type; + + /** + * 描述 + */ + private String remark; + + /** + * 状态:0-删除;1-正常;默认正常 + */ + private Integer state; +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysRoleFunction.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysRoleFunction.java new file mode 100644 index 0000000..9f67a2c --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysRoleFunction.java @@ -0,0 +1,29 @@ +package com.njcn.gather.user.user.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * @author caozehui + * @date 2024-11-15 + */ +@Data +@TableName("sys_role_function") +public class SysRoleFunction implements Serializable { + private static final long serialVersionUID = -32044506851166587L; + /** + * 角色表Id + */ + private String roleId; + + /** + * 资源表Id + */ + private String functionId; + +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysUser.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysUser.java new file mode 100644 index 0000000..c6ef03e --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysUser.java @@ -0,0 +1,98 @@ +package com.njcn.gather.user.user.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author caozehui + * @since 2024-11-08 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_user") +public class SysUser extends BaseEntity implements Serializable { + + private static final long serialVersionUID = -54771740356521149L; + + /** + * 用户表Id + */ + private String id; + + /** + * 用户名(别名) + */ + private String name; + + /** + * 登录名 + */ + private String loginName; + + /** + * 密码 + */ + private String password; + + /** + * 部门Id + */ + private String deptId; + + /** + * 电话号码 + */ + private String phone; + + /** + * 邮箱 + */ + private String email; + + /** + * 最后一次登录时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime loginTime; + + /** + * 密码错误次数 + */ + private Integer loginErrorTimes; + + /** + * 用户密码错误锁定时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime lockTime; + + /** + * 用户状态 0-删除;1-正常;2-锁定;3-待审核;4-休眠;5-密码过期 + */ + private Integer state; + + @TableField(exist = false) + private List roleIds; + @TableField(exist = false) + private List roleCodes; + + @TableField(exist = false) + private List roleNames; +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysUserRole.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysUserRole.java new file mode 100644 index 0000000..76c9a23 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/SysUserRole.java @@ -0,0 +1,29 @@ +package com.njcn.gather.user.user.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * @author caozehui + * @date 2024-11-12 + */ +@Data +@TableName("sys_user_role") +public class SysUserRole implements Serializable { + private static final long serialVersionUID = 725290952766199948L; + /** + * 用户Id + */ + private String userId; + + /** + * 角色Id + */ + private String roleId; + +} + diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/po/Token.java b/user/src/main/java/com/njcn/gather/user/user/pojo/po/Token.java new file mode 100644 index 0000000..9b1ca89 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/po/Token.java @@ -0,0 +1,16 @@ +package com.njcn.gather.user.user.pojo.po; + +import lombok.Data; + +import java.util.Map; + +@Data +public class Token { + + private String accessToken; + + private String refreshToken; + + private Map userInfo; + +} diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/vo/MenuVO.java b/user/src/main/java/com/njcn/gather/user/user/pojo/vo/MenuVO.java new file mode 100644 index 0000000..b42c421 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/vo/MenuVO.java @@ -0,0 +1,12 @@ +package com.njcn.gather.user.user.pojo.vo; + +import com.njcn.gather.user.user.pojo.po.SysFunction; +import lombok.Data; + +@Data +public class MenuVO extends SysFunction { + + private String redirect; + + private MetaVO meta; +} diff --git a/user/src/main/java/com/njcn/gather/user/user/pojo/vo/MetaVO.java b/user/src/main/java/com/njcn/gather/user/user/pojo/vo/MetaVO.java new file mode 100644 index 0000000..f27d20b --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/pojo/vo/MetaVO.java @@ -0,0 +1,49 @@ +package com.njcn.gather.user.user.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class MetaVO { + + /** + * 菜单和面包屑对应的图标 + */ + private String icon; + + /** + * 路由标题 (用作 document.title || 菜单的名称) + */ + private String title; + + /** + * 路由外链时填写的访问地址 + */ + @JsonProperty("isLink") + private String isLink; + + /** + * 是否在菜单中隐藏 (通常列表详情页需要隐藏) + */ + @JsonProperty("isHide") + private boolean isHide; + + /** + * 菜单是否全屏 (示例:数据大屏页面) + */ + @JsonProperty("isFull") + private boolean isFull; + + /** + * 菜单是否固定在标签页中 (首页通常是固定项) + */ + @JsonProperty("isAffix") + private boolean isAffix; + + /** + * 当前路由是否缓存 + */ + @JsonProperty("isKeepAlive") + private boolean isKeepAlive; + +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/ISysFunctionService.java b/user/src/main/java/com/njcn/gather/user/user/service/ISysFunctionService.java new file mode 100644 index 0000000..701f888 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/ISysFunctionService.java @@ -0,0 +1,70 @@ +package com.njcn.gather.user.user.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.user.user.pojo.param.SysFunctionParam; +import com.njcn.gather.user.user.pojo.po.SysFunction; +import com.njcn.gather.user.user.pojo.vo.MenuVO; + +import java.util.List; +import java.util.Map; + +/** + * @author caozehui + * @date 2024-11-12 + */ +public interface ISysFunctionService extends IService { + + /** + * 根据关键字模糊查询菜单(资源)树 + * + * @param keyword 关键字 + * @return 菜单(资源)树 + */ + List getFunctionTreeByKeyword(String keyword); + + /** + * 添加菜单(资源) + * + * @param functionParam 资源参数 + * @return 是否添加成功 + */ + boolean addFunction(SysFunctionParam functionParam); + + /** + * 修改菜单(资源) + * + * @param functionParam 资源参数 + * @return 是否更新成功 + */ + boolean updateFunction(SysFunctionParam.UpdateParam functionParam); + + /** + * 删除菜单(资源) + * + * @param id 资源id + */ + boolean deleteFunction(String id); + + /** + * 获取树形结构的菜单(资源 + * + * @param isContainButton 是否包含按钮 + * @return 树形结构的资源 + */ + List getFunctionTree(boolean isContainButton); + + /** + * 根据用户id获取菜单 + * + * @return 路由菜单 + */ + List getMenuByUserId(String userId); + + /** + * 根据用户id获取按钮 + * @param userId 用户id + * @return 按钮 + */ + Map> getButtonByUserId(String userId); + +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/ISysRoleFunctionService.java b/user/src/main/java/com/njcn/gather/user/user/service/ISysRoleFunctionService.java new file mode 100644 index 0000000..4ff39cd --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/ISysRoleFunctionService.java @@ -0,0 +1,47 @@ +package com.njcn.gather.user.user.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.user.user.pojo.po.SysFunction; +import com.njcn.gather.user.user.pojo.po.SysRoleFunction; + +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-12 + */ +public interface ISysRoleFunctionService extends IService { + + /** + * 获取角色id绑定的菜单(资源) + * + * @param roleId 角色id + * @return 菜单(资源)列表 + */ + List listFunctionByRoleId(String roleId); + + /** + * 更新角色菜单(资源)关联数据 + * + * @param roleId 角色id + * @param functionIds 菜单(资源)ids + * @return 成功返回true,失败返回false + */ + boolean updateRoleFunction(String roleId, List functionIds); + + /** + * 根据角色ids删除角色资源关联数据 + * + * @param roleIds + * @return 成功返回true,失败返回false + */ + boolean deleteRoleFunctionByRoleIds(List roleIds); + + /** + * 根据菜单(资源)ids删除角色资源关联数据 + * + * @param functionIds 菜单(资源)ids + * @return 成功返回true,失败返回false + */ + boolean deleteRoleFunctionByFunctionIds(List functionIds); +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/ISysRoleService.java b/user/src/main/java/com/njcn/gather/user/user/service/ISysRoleService.java new file mode 100644 index 0000000..821b252 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/ISysRoleService.java @@ -0,0 +1,53 @@ +package com.njcn.gather.user.user.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.user.user.pojo.param.SysRoleParam; +import com.njcn.gather.user.user.pojo.po.SysRole; + +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-11 + */ +public interface ISysRoleService extends IService { + + /** + * 分页查询角色列表 + * + * @param queryParam 查询参数 + */ + Page listRole(SysRoleParam.QueryParam queryParam); + + /** + * 新增角色 + * + * @param sysRoleParam 角色参数 + * @return 是否成功 + */ + boolean addRole(SysRoleParam sysRoleParam); + + /** + * 更新角色 + * + * @param updateParam 更新参数 + * @return 是否成功 + */ + boolean updateRole(SysRoleParam.UpdateParam updateParam); + + /** + * 删除角色 + * + * @param ids 角色id列表 + * @return 是否成功 + */ + boolean deleteRole(List ids); + + /** + * 查询所有角色作为下拉框 + * + * @return 角色列表 + */ + List simpleList(); +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/ISysUserRoleService.java b/user/src/main/java/com/njcn/gather/user/user/service/ISysUserRoleService.java new file mode 100644 index 0000000..6a9c64a --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/ISysUserRoleService.java @@ -0,0 +1,56 @@ +package com.njcn.gather.user.user.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.user.user.pojo.po.SysRole; +import com.njcn.gather.user.user.pojo.po.SysUserRole; + +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-12 + */ +public interface ISysUserRoleService extends IService { + + /** + * 根据用户id获取角色 + * + * @param userId 用户id + * @return 角色信息 + */ + List listRoleByUserId(String userId); + + /** + * 新增用户角色关联数据 + * + * @param userId 用户id + * @param roleIds 角色id + * @return 成功返回true,失败返回false + */ + boolean addUserRole(String userId, List roleIds); + + /** + * 修改用户角色关联数据 + * + * @param userId 用户id + * @param roleIds 角色id + * @return 成功返回true,失败返回false + */ + boolean updateUserRole(String userId, List roleIds); + + /** + * 根据用户id删除用户角色关联数据 + * + * @param userIds 用户ids + * @return 成功返回true,失败返回false + */ + boolean deleteUserRoleByUserIds(List userIds); + + /** + * 根据角色id删除用户角色关联数据 + * + * @param roleIds 角色ids + * @return 成功返回true,失败返回false + */ + boolean deleteUserRoleByRoleIds(List roleIds); +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/ISysUserService.java b/user/src/main/java/com/njcn/gather/user/user/service/ISysUserService.java new file mode 100644 index 0000000..e473f0b --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/ISysUserService.java @@ -0,0 +1,106 @@ +package com.njcn.gather.user.user.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.user.user.pojo.param.SysUserParam; +import com.njcn.gather.user.user.pojo.po.SysUser; + +import java.util.List; + +/** + * @author caozehui + * @since 2024-11-08 + */ +public interface ISysUserService extends IService { + + /** + * 分页查询用户列表 + * + * @param queryParam 分页查询参数 + * @return 分页查询结果 + */ + Page listUser(SysUserParam.SysUserQueryParam queryParam); + + /** + * 根据登录名查询用户 + * + * @param loginName + * @return 用户对象,如果没有查询到则返回null + */ + SysUser getUserByLoginName(String loginName); + + /** + * 根据手机号查询用户 + * + * @param phone 手机号 + * @param isExcludeSelf 是否排除自己 + * @param id 排除自己时需要传入自己的ID + * @return 用户对象,如果没有查询到则返回null + */ + SysUser getUserByPhone(String phone, boolean isExcludeSelf, String id); + + /** + * 根据用户名(别名)查询用户 + * + * @param name 用户名(别名) + * @param isExcludeSelf 是否排除自己 + * @param id 排除自己时需要传入自己的ID + * @return 用户对象,如果没有查询到则返回null + */ + SysUser getUserByName(String name, boolean isExcludeSelf, String id); + + /** + * 根据邮箱查询用户 + * @param email 邮箱 + * @param isExcludeSelf 是否排除自己 + * @param id 排除自己时需要传入自己的ID + * @return 用户对象,如果没有查询到则返回null + */ + SysUser getUserByEmail(String email, boolean isExcludeSelf, String id); + + /** + * 新增用户 + * + * @param addUserParam 新增用户参数 + * @return 结果,true表示新增成功,false表示新增失败 + */ + boolean addUser(SysUserParam.SysUserAddParam addUserParam); + + /** + * 更新用户 + * + * @param updateUserParam 更新用户参数 + * @return 结果,true表示更新成功,false表示更新失败 + */ + boolean updateUser(SysUserParam.SysUserUpdateParam updateUserParam); + + /** + * 修改密码 + * @return 结果,true表示修改成功,false表示修改失败 + */ + boolean updatePassword(SysUserParam.SysUserUpdatePasswordParam param); + + /** + * 批量删除用户 + * + * @param ids 用户ID列表 + * @return 结果,true表示删除成功,false表示删除失败 + */ + boolean deleteUser(List ids); + + /** + * 根据登录名和密码查询用户 + * + * @param loginName 登录名 + * @param password 密码 + * @return 用户对象,如果没有查询到则返回null + */ + SysUser getUserByLoginNameAndPassword(String loginName, String password); + + /** + * 更新用户登录时间为当前时间 + * + * @param userId + */ + boolean updateLoginTime(String userId); +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/impl/SysFunctionServiceImpl.java b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysFunctionServiceImpl.java new file mode 100644 index 0000000..dd89b9a --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysFunctionServiceImpl.java @@ -0,0 +1,234 @@ +package com.njcn.gather.user.user.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.constant.PatternRegex; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.user.pojo.constant.FunctionConst; +import com.njcn.gather.user.pojo.enums.UserResponseEnum; +import com.njcn.gather.user.user.mapper.SysFunctionMapper; +import com.njcn.gather.user.user.pojo.param.SysFunctionParam; +import com.njcn.gather.user.user.pojo.po.SysFunction; +import com.njcn.gather.user.user.pojo.vo.MenuVO; +import com.njcn.gather.user.user.pojo.vo.MetaVO; +import com.njcn.gather.user.user.service.ISysFunctionService; +import com.njcn.gather.user.user.service.ISysRoleFunctionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * @author caozehui + * @date 2024-11-12 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysFunctionServiceImpl extends ServiceImpl implements ISysFunctionService { + + private final ISysRoleFunctionService sysRoleFunctionService; + + @Override + public List getFunctionTreeByKeyword(String keyword) { + List functionTree = this.getFunctionTree(true); + filterTreeByName(functionTree, keyword); + return functionTree; + } + + @Override + @Transactional + public boolean addFunction(SysFunctionParam functionParam) { + functionParam.setName(functionParam.getName().trim()); + functionParam.setPath(functionParam.getPath().trim()); + functionParam.setComponent(functionParam.getComponent().trim()); + checkFunctionParam(functionParam, false); + SysFunction function = new SysFunction(); + BeanUtil.copyProperties(functionParam, function); + function.setState(DataStateEnum.ENABLE.getCode()); + if (Objects.equals(functionParam.getPid(), FunctionConst.FATHER_PID)) { + function.setPids(FunctionConst.FATHER_PID); + } else { + SysFunction fatherFunction = this.lambdaQuery().eq(SysFunction::getId, functionParam.getPid()).one(); + if (Objects.equals(fatherFunction.getPid(), FunctionConst.FATHER_PID)) { + function.setPids(functionParam.getPid()); + } else { + String pidS = fatherFunction.getPids(); + function.setPids(pidS + "," + functionParam.getPid()); + } + } + return this.save(function); + } + + @Override + @Transactional + public boolean updateFunction(SysFunctionParam.UpdateParam param) { + param.setName(param.getName().trim()); + boolean result = false; + param.setPath(param.getPath().trim()); + param.setComponent(param.getComponent().trim()); + checkFunctionParam(param, true); + SysFunction oldFunction = this.lambdaQuery().eq(SysFunction::getId, param.getId()).eq(SysFunction::getState, DataStateEnum.ENABLE.getCode()).one(); + List childrenList = this.lambdaQuery().eq(SysFunction::getPid, param.getId()).eq(SysFunction::getState, DataStateEnum.ENABLE.getCode()).list(); + if (oldFunction.getType().equals(FunctionConst.TYPE_MENU) && param.getType().equals(FunctionConst.TYPE_BUTTON) && !CollectionUtils.isEmpty(childrenList)) { + throw new BusinessException(UserResponseEnum.EXISTS_CHILDREN_NOT_UPDATE); + } else { + SysFunction function = new SysFunction(); + BeanUtil.copyProperties(param, function); + result = this.updateById(function); + } + return result; + } + + @Override + @Transactional + public boolean deleteFunction(String id) { + boolean result1 = false; + sysRoleFunctionService.deleteRoleFunctionByFunctionIds(Collections.singletonList(id)); + List childrenList = this.lambdaQuery().eq(SysFunction::getState, DataStateEnum.ENABLE.getCode()).eq(SysFunction::getPid, id).list(); + if (CollectionUtils.isEmpty(childrenList)) { + result1 = this.lambdaUpdate().set(SysFunction::getState, DataStateEnum.DELETED.getCode()).in(SysFunction::getId, id).update(); + } else { + throw new BusinessException(UserResponseEnum.EXISTS_CHILDREN_NOT_DELETE); + } + return result1; + } + + @Override + public List getFunctionTree(boolean isContainButton) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysFunction::getState, DataStateEnum.ENABLE.getCode()); + if (isContainButton) { + wrapper.in(SysFunction::getType, FunctionConst.TYPE_MENU, FunctionConst.TYPE_BUTTON); + } else { + wrapper.in(SysFunction::getType, FunctionConst.TYPE_MENU); + } + List allFunctions = this.list(wrapper); + return allFunctions.stream().filter(fun -> Objects.equals(FunctionConst.FATHER_PID, fun.getPid())).peek(funS -> funS.setChildren(getChildrenList(funS, allFunctions))).sorted(Comparator.comparingInt(SysFunction::getSort)).collect(Collectors.toList()); + } + + @Override + public List getMenuByUserId(String userId) { + List menu = this.baseMapper.getMenuByUserId(userId); + menu.stream().forEach(m -> { + MetaVO meta = new MetaVO(); + meta.setIcon(m.getIcon()); + meta.setTitle(m.getName()); + meta.setIsLink(""); + meta.setHide(false); + meta.setFull(false); + meta.setAffix(false); + if("home".equals(m.getCode())){ + meta.setAffix(true); + } + meta.setKeepAlive(true); + m.setMeta(meta); + m.setName(m.getCode()); + }); + return menu.stream().filter(fun -> Objects.equals(FunctionConst.FATHER_PID, fun.getPid())).peek(funS -> { + List childrenList = getChildrenList(funS, menu); + if (ObjectUtil.isNull(childrenList) || childrenList.size() == 0) { + funS.setRedirect(null); + } else { + funS.setRedirect(funS.getComponent()); + funS.setComponent(null); + } + funS.setChildren(childrenList); + }).sorted(Comparator.comparingInt(MenuVO::getSort)).collect(Collectors.toList()); + } + + @Override + public Map> getButtonByUserId(String userId) { + List sysFunctions = this.baseMapper.getButtonByUserId(userId); + + Map> buttonMap = new HashMap<>(); + sysFunctions.stream().collect(Collectors.groupingBy(SysFunction::getPid)).forEach((k, v) -> { + SysFunction fatherFunction = this.getById(k); + if (ObjectUtil.isNotNull(fatherFunction)) { + buttonMap.put(fatherFunction.getCode(), v.stream().map(SysFunction::getCode).collect(Collectors.toList())); + } + }); + return buttonMap; + } + + private List getChildrenList(T currMenu, List categories) { + return categories.stream().filter(o -> Objects.equals(o.getPid(), currMenu.getId())).peek(o -> o.setChildren(getChildrenList(o, categories))).sorted(Comparator.comparingInt(SysFunction::getSort)).collect(Collectors.toList()); + } + + /** + * 校验参数, + * 1.检查是否存在相同名称的菜单 + * 名称 && 路径做唯一判断 + */ + private void checkFunctionParam(SysFunctionParam functionParam, boolean isExcludeSelf) { + if (functionParam.getType().equals(FunctionConst.TYPE_MENU)) { + if (StrUtil.isBlank(functionParam.getComponent())) { + throw new BusinessException(UserResponseEnum.COMPONENT_NOT_BLANK); + } + if (StrUtil.isBlank(functionParam.getPath()) || !Pattern.matches(PatternRegex.FUNCTION_PATH_REGEX, functionParam.getPath())) { + throw new BusinessException(UserResponseEnum.FUNCTION_PATH_FORMAT_ERROR); + } + if(StrUtil.isBlank(functionParam.getComponent()) || !Pattern.matches(PatternRegex.FUNCTION_COMPONENT_REGEX, functionParam.getComponent())){ + throw new BusinessException(UserResponseEnum.FUNCTION_COMPONENT_FORMAT_ERROR); + } + } + LambdaQueryWrapper functionLambdaQueryWrapper = new LambdaQueryWrapper<>(); + // 同一个pid下,名称、编码、路径、组件地址不能重复 + functionLambdaQueryWrapper + .eq(SysFunction::getPid, functionParam.getPid()) + .eq(SysFunction::getState, DataStateEnum.ENABLE.getCode()) + .and(obj -> obj.eq(SysFunction::getName, functionParam.getName()).or() + .eq(SysFunction::getCode, functionParam.getCode()).or() + .eq(StrUtil.isNotBlank(functionParam.getPath()), SysFunction::getPath, functionParam.getPath()).or() + .eq(StrUtil.isNotBlank(functionParam.getComponent()), SysFunction::getComponent, functionParam.getComponent()) + ); + //更新的时候,需排除当前记录 + if (isExcludeSelf) { + if (functionParam instanceof SysFunctionParam.UpdateParam) { + functionLambdaQueryWrapper.ne(SysFunction::getId, ((SysFunctionParam.UpdateParam) functionParam).getId()); + } + } + int countByAccount = this.count(functionLambdaQueryWrapper); + //大于等于1个则表示重复 + if (countByAccount >= 1) { + throw new BusinessException(UserResponseEnum.EXISTS_SAME_MENU_CHILDREN); + } + } + + private List filterTreeByName(List tree, String keyword) { + if (CollectionUtils.isEmpty(tree) || !StrUtil.isNotBlank(keyword)) { + return tree; + } + filter(tree, keyword); + return tree; + } + + private void filter(List list, String keyword) { + for (int i = list.size() - 1; i >= 0; i--) { + SysFunction function = list.get(i); + List children = function.getChildren(); + if (!function.getName().contains(keyword)) { + if (!CollectionUtils.isEmpty(children)) { + filter(children, keyword); + } + if (CollectionUtils.isEmpty(function.getChildren())) { + list.remove(i); + } + } +// else { +// if (!CollectionUtils.isEmpty(children)) { +// filter(children, keyword); +// } +// } + } + } +} \ No newline at end of file diff --git a/user/src/main/java/com/njcn/gather/user/user/service/impl/SysRoleFunctionServiceImpl.java b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysRoleFunctionServiceImpl.java new file mode 100644 index 0000000..6d41aa8 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysRoleFunctionServiceImpl.java @@ -0,0 +1,68 @@ +package com.njcn.gather.user.user.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.gather.user.user.mapper.SysRoleFunctionMapper; +import com.njcn.gather.user.user.pojo.po.SysFunction; +import com.njcn.gather.user.user.pojo.po.SysRoleFunction; +import com.njcn.gather.user.user.service.ISysRoleFunctionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-12 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysRoleFunctionServiceImpl extends ServiceImpl implements ISysRoleFunctionService { + + @Override + public List listFunctionByRoleId(String roleId) { + return this.baseMapper.getFunctionListByRoleId(roleId); + } + + @Override + @Transactional + public boolean updateRoleFunction(String roleId, List functionIds) { + //删除原有关系 + this.deleteRoleFunctionByRoleIds(Collections.singletonList(roleId)); + //新增关系 + List roleFunctions = new ArrayList<>(); + functionIds.forEach(functionId -> { + SysRoleFunction roleFunction = new SysRoleFunction(); + roleFunction.setRoleId(roleId); + roleFunction.setFunctionId(functionId); + roleFunctions.add(roleFunction); + }); + if (CollectionUtil.isEmpty(roleFunctions)) { + return true; + } else { + return this.saveBatch(roleFunctions); + } + } + + @Override + @Transactional + public boolean deleteRoleFunctionByRoleIds(List roleIds) { + LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>(); + lambdaQuery.in(SysRoleFunction::getRoleId, roleIds); + return this.remove(lambdaQuery); + } + + @Override + @Transactional + public boolean deleteRoleFunctionByFunctionIds(List functionIds) { + LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>(); + lambdaQuery.in(SysRoleFunction::getFunctionId, functionIds); + return this.remove(lambdaQuery); + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/impl/SysRoleServiceImpl.java b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..caf7500 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,128 @@ +package com.njcn.gather.user.user.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.user.pojo.constant.RoleConst; +import com.njcn.gather.user.pojo.enums.UserResponseEnum; +import com.njcn.gather.user.user.mapper.SysRoleMapper; +import com.njcn.gather.user.user.pojo.param.SysRoleParam; +import com.njcn.gather.user.user.pojo.po.SysRole; +import com.njcn.gather.user.user.service.ISysRoleFunctionService; +import com.njcn.gather.user.user.service.ISysRoleService; +import com.njcn.gather.user.user.service.ISysUserRoleService; +import com.njcn.web.factory.PageFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysRoleServiceImpl extends ServiceImpl implements ISysRoleService { + + private final ISysUserRoleService sysUserRoleService; + private final ISysRoleFunctionService sysRoleFunctionService; + + @Override + public Page listRole(SysRoleParam.QueryParam queryParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(queryParam)) { + queryWrapper.like(StrUtil.isNotBlank(queryParam.getName()), "sys_role.name", queryParam.getName()).eq(StrUtil.isNotBlank(queryParam.getCode()), "sys_role.code", queryParam.getCode()).eq(ObjectUtil.isNotNull(queryParam.getType()), "sys_role.type", queryParam.getType()); + } +// if (queryParam.getType().equals(0)) { +// queryWrapper.in("sys_role.type", queryParam.getType(), 1); +// } else if (queryParam.getType().equals(1)) { +// queryWrapper.eq("sys_role.type", 2); +// } + queryWrapper.eq("sys_role.state", DataStateEnum.ENABLE.getCode()).orderByDesc("sys_role.Update_Time"); + return this.baseMapper.selectPage(new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)), queryWrapper); + } + + @Override + @Transactional + public boolean addRole(SysRoleParam sysRoleParam) { + sysRoleParam.setName(sysRoleParam.getName().trim()); + checkRepeat(sysRoleParam, false); + SysRole role = new SysRole(); + BeanUtil.copyProperties(sysRoleParam, role); + //默认为正常状态 + role.setState(DataStateEnum.ENABLE.getCode()); + return this.save(role); + } + + @Override + @Transactional + public boolean updateRole(SysRoleParam.UpdateParam updateParam) { + updateParam.setName(updateParam.getName().trim()); + checkRepeat(updateParam, true); + //不能修改超级管理员角色 + Integer count = this.lambdaQuery() + .in(SysRole::getType, RoleConst.TYPE_SUPER_ADMINISTRATOR) + .eq(SysRole::getId, updateParam.getId()).eq(SysRole::getState, DataStateEnum.ENABLE.getCode()).count(); + if (count > 0) { + throw new BusinessException(UserResponseEnum.SUPER_ADMINSTRATOR_ROLE_CANNOT_UPDATE); + } + SysRole role = new SysRole(); + BeanUtil.copyProperties(updateParam, role); + return this.updateById(role); + } + + @Override + @Transactional + public boolean deleteRole(List ids) { + //不能删除超级管理员角色 + Integer count = this.lambdaQuery() + .in(SysRole::getType, RoleConst.TYPE_SUPER_ADMINISTRATOR) + .in(SysRole::getId, ids).eq(SysRole::getState, DataStateEnum.ENABLE.getCode()).count(); + if (count > 0) { + throw new BusinessException(UserResponseEnum.SUPER_ADMINSTRATOR_ROLE_CANNOT_DELETE); + } + // 删除角色和用户的绑定 + sysUserRoleService.deleteUserRoleByRoleIds(ids); + //删除角色和资源的绑定 + sysRoleFunctionService.deleteRoleFunctionByRoleIds(ids); + return this.lambdaUpdate().set(SysRole::getState, DataStateEnum.DELETED.getCode()).in(SysRole::getId, ids).update(); + } + + @Override + public List simpleList() { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.select(SysRole::getId, SysRole::getName).eq(SysRole::getState, DataStateEnum.ENABLE.getCode()); + return this.baseMapper.selectList(lambdaQueryWrapper); + } + + /** + * 校验参数,检查是否存在相同名称或编码的角色 + */ + private void checkRepeat(SysRoleParam roleParam, boolean isExcludeSelf) { + LambdaQueryWrapper roleLambdaQueryWrapper = new LambdaQueryWrapper<>(); + roleLambdaQueryWrapper + .eq(SysRole::getState, DataStateEnum.ENABLE.getCode()) + .and(w -> w.eq(SysRole::getName, roleParam.getName()).or().eq(SysRole::getCode, roleParam.getCode())); + //更新的时候,需排除当前记录 + if (isExcludeSelf) { + if (roleParam instanceof SysRoleParam.UpdateParam) { + roleLambdaQueryWrapper.ne(SysRole::getId, ((SysRoleParam.UpdateParam) roleParam).getId()); + } + } + int countByAccount = this.count(roleLambdaQueryWrapper); + //大于等于1个则表示重复 + if (countByAccount >= 1) { + throw new BusinessException(UserResponseEnum.NAME_OR_CODE_REPEAT); + } + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/impl/SysUserRoleServiceImpl.java b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysUserRoleServiceImpl.java new file mode 100644 index 0000000..0c99393 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysUserRoleServiceImpl.java @@ -0,0 +1,79 @@ +package com.njcn.gather.user.user.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.gather.user.user.mapper.SysUserRoleMapper; +import com.njcn.gather.user.user.pojo.po.SysRole; +import com.njcn.gather.user.user.pojo.po.SysUserRole; +import com.njcn.gather.user.user.service.ISysUserRoleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author caozehui + * @date 2024-11-12 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysUserRoleServiceImpl extends ServiceImpl implements ISysUserRoleService { + + @Override + public List listRoleByUserId(String userId) { + return this.baseMapper.getRoleListByUserId(userId); + } + + @Override + @Transactional + public boolean addUserRole(String userId, List roleIds) { + List userRoles = new ArrayList<>(); + if (!CollectionUtil.isEmpty(roleIds)) { + roleIds.forEach(id -> { + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(userId); + userRole.setRoleId(id); + userRoles.add(userRole); + }); + } + return this.saveBatch(userRoles); + } + + @Override + @Transactional + public boolean updateUserRole(String userId, List roleIds) { + //删除原有关系 + this.deleteUserRoleByUserIds(Collections.singletonList(userId)); + //新增关系 + List userROles = new ArrayList<>(); + roleIds.forEach(role -> { + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(userId); + userRole.setRoleId(role); + userROles.add(userRole); + }); + return this.saveBatch(userROles); + } + + @Override + @Transactional + public boolean deleteUserRoleByUserIds(List userIds) { + LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>(); + lambdaQuery.in(SysUserRole::getUserId, userIds); + return this.remove(lambdaQuery); + } + + @Override + @Transactional + public boolean deleteUserRoleByRoleIds(List roleIds) { + LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>(); + lambdaQuery.in(SysUserRole::getRoleId, roleIds); + return this.remove(lambdaQuery); + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/service/impl/SysUserServiceImpl.java b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..e7a6da4 --- /dev/null +++ b/user/src/main/java/com/njcn/gather/user/user/service/impl/SysUserServiceImpl.java @@ -0,0 +1,213 @@ +package com.njcn.gather.user.user.service.impl; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.common.utils.sm.Sm4Utils; +import com.njcn.db.mybatisplus.constant.DbConstant; +import com.njcn.gather.user.pojo.constant.RoleConst; +import com.njcn.gather.user.pojo.constant.UserConst; +import com.njcn.gather.user.pojo.enums.UserResponseEnum; +import com.njcn.gather.user.user.mapper.SysUserMapper; +import com.njcn.gather.user.user.pojo.param.SysUserParam; +import com.njcn.gather.user.user.pojo.po.SysRole; +import com.njcn.gather.user.user.pojo.po.SysUser; +import com.njcn.gather.user.user.service.ISysUserRoleService; +import com.njcn.gather.user.user.service.ISysUserService; +import com.njcn.web.factory.PageFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @author caozehui + * @date 2024-11-08 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysUserServiceImpl extends ServiceImpl implements ISysUserService { + + private final ISysUserRoleService sysUserRoleService; + + @Override + public Page listUser(SysUserParam.SysUserQueryParam queryParam) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (ObjectUtil.isNotNull(queryParam)) { + queryWrapper.like(StrUtil.isNotBlank(queryParam.getName()), "sys_user.name", queryParam.getName()) + .between(ObjectUtil.isAllNotEmpty(queryParam.getSearchBeginTime(), queryParam.getSearchEndTime()), "sys_user.Login_Time", queryParam.getSearchBeginTime(), queryParam.getSearchEndTime()); + //排序 + if (ObjectUtil.isAllNotEmpty(queryParam.getSortBy(), queryParam.getOrderBy())) { + queryWrapper.orderBy(true, queryParam.getOrderBy().equals(DbConstant.ASC), StrUtil.toUnderlineCase(queryParam.getSortBy())); + } else { + queryWrapper.orderByDesc("sys_user.update_time"); + } + } else { + queryWrapper.orderByDesc("sys_user.update_time"); + } + queryWrapper.ne("sys_user.state", UserConst.STATE_DELETE); + Page page = this.baseMapper.selectPage(new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)), queryWrapper); + page.getRecords().forEach(sysUser -> { + List sysRoles = sysUserRoleService.listRoleByUserId(sysUser.getId()); + sysUser.setRoleIds(sysRoles.stream().map(SysRole::getId).collect(Collectors.toList())); + sysUser.setRoleNames(sysRoles.stream().map(SysRole::getName).collect(Collectors.toList())); + }); + return page; + } + + @Override + public SysUser getUserByLoginName(String loginName) { + return this.lambdaQuery().ne(SysUser::getState, UserConst.STATE_DELETE).eq(SysUser::getLoginName, loginName).one(); + } + + @Override + public SysUser getUserByPhone(String phone, boolean isExcludeSelf, String id) { + LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>(); + lambdaQuery.eq(SysUser::getPhone, phone).ne(SysUser::getState, UserConst.STATE_DELETE); + if (isExcludeSelf) { + lambdaQuery.ne(SysUser::getId, id); + } + return this.baseMapper.selectOne(lambdaQuery); + } + + @Override + public SysUser getUserByName(String name, boolean isExcludeSelf, String id) { + LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>(); + lambdaQuery.eq(SysUser::getName, name).ne(SysUser::getState, UserConst.STATE_DELETE); + if (isExcludeSelf) { + lambdaQuery.ne(SysUser::getId, id); + } + return this.baseMapper.selectOne(lambdaQuery); + } + + @Override + public SysUser getUserByEmail(String email, boolean isExcludeSelf, String id) { + LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>(); + lambdaQuery.eq(SysUser::getEmail, email).ne(SysUser::getState, UserConst.STATE_DELETE); + if (isExcludeSelf) { + lambdaQuery.ne(SysUser::getId, id); + } + return this.baseMapper.selectOne(lambdaQuery); + } + + @Override + @Transactional + public boolean addUser(SysUserParam.SysUserAddParam addUserParam) { + addUserParam.setName(addUserParam.getName().trim()); + addUserParam.setLoginName(addUserParam.getLoginName().trim()); + if (UserConst.SUPER_ADMIN.equals(addUserParam.getLoginName())) { + throw new BusinessException(UserResponseEnum.SUPER_ADMIN_REPEAT); + } + if (!Objects.isNull(getUserByLoginName(addUserParam.getLoginName()))) { + throw new BusinessException(UserResponseEnum.LOGIN_NAME_REPEAT); + } + checkRepeat(addUserParam, false, null); + SysUser sysUser = new SysUser(); + BeanUtils.copyProperties(addUserParam, sysUser); + String secretkey = Sm4Utils.globalSecretKey; + Sm4Utils sm4 = new Sm4Utils(secretkey); + sysUser.setPassword(sm4.encryptData_ECB(sysUser.getPassword())); + sysUser.setLoginTime(LocalDateTimeUtil.now()); + sysUser.setLoginErrorTimes(0); + sysUser.setState(UserConst.STATE_ENABLE); + boolean result = this.save(sysUser); + sysUserRoleService.addUserRole(sysUser.getId(), addUserParam.getRoleIds()); + return result; + } + + @Override + @Transactional + public boolean updateUser(SysUserParam.SysUserUpdateParam updateUserParam) { + updateUserParam.setName(updateUserParam.getName().trim()); + checkRepeat(updateUserParam, true, updateUserParam.getId()); + SysUser sysUser = new SysUser(); + BeanUtils.copyProperties(updateUserParam, sysUser); + sysUserRoleService.updateUserRole(sysUser.getId(), updateUserParam.getRoleIds()); + return this.updateById(sysUser); + } + + @Override + @Transactional + public boolean updatePassword(SysUserParam.SysUserUpdatePasswordParam param) { + if (param.getOldPassword().equals(param.getNewPassword())) { + throw new BusinessException(UserResponseEnum.PASSWORD_SAME); + } + SysUser user = lambdaQuery().ne(SysUser::getState, UserConst.STATE_DELETE).eq(SysUser::getId, param.getId()).one(); + if (ObjectUtil.isNotNull(user)) { + String secretkey = Sm4Utils.globalSecretKey; + Sm4Utils sm4 = new Sm4Utils(secretkey); + if (sm4.encryptData_ECB(param.getOldPassword()).equals(user.getPassword())) { + user.setPassword(sm4.encryptData_ECB(param.getNewPassword())); + return this.updateById(user); + }else { + throw new BusinessException(UserResponseEnum.OLD_PASSWORD_ERROR); + } + } + return false; + } + + @Override + @Transactional + public boolean deleteUser(List ids) { + for (String id : ids) { + List sysRoles = sysUserRoleService.listRoleByUserId(id); + for (SysRole sysRole : sysRoles) { + if (sysRole.getType().equals(RoleConst.TYPE_SUPER_ADMINISTRATOR)) { + throw new BusinessException(UserResponseEnum.SUPER_ADMIN_CANNOT_DELETE); // 超级管理员角色不能删除 + } + } + } + // 删除用户角色关联数据 + sysUserRoleService.deleteUserRoleByUserIds(ids); + return this.lambdaUpdate() + .set(SysUser::getState, UserConst.STATE_DELETE) + .in(SysUser::getId, ids) + .update(); + } + + @Override + public SysUser getUserByLoginNameAndPassword(String loginName, String password) { + String secretkey = Sm4Utils.globalSecretKey; + Sm4Utils sm4 = new Sm4Utils(secretkey); + return this.lambdaQuery().ne(SysUser::getState, UserConst.STATE_DELETE) + .eq(SysUser::getLoginName, loginName) + .eq(SysUser::getPassword, sm4.encryptData_ECB(password)).one(); + } + + @Override + public boolean updateLoginTime(String userId) { + return this.lambdaUpdate().eq(SysUser::getId, userId).set(SysUser::getLoginTime, LocalDateTimeUtil.now()).update(); + } + + /** + * 校验重复 + * + * @param sysUserParam 检查对象 + * @param isExcludeSelf 是否排除自己 + * @param id 排除自己id + */ + private void checkRepeat(SysUserParam sysUserParam, boolean isExcludeSelf, String id) { + if (!Objects.isNull(getUserByName(sysUserParam.getName(), isExcludeSelf, id))) { + throw new BusinessException(UserResponseEnum.USER_NAME_REPEAT); + } + if (StringUtils.isNotBlank(sysUserParam.getPhone()) && !Objects.isNull(getUserByPhone(sysUserParam.getPhone(), isExcludeSelf, id))) { + throw new BusinessException(UserResponseEnum.REGISTER_PHONE_FAIL); + } + if (StringUtils.isNotBlank(sysUserParam.getEmail()) && !Objects.isNull(getUserByEmail(sysUserParam.getEmail(), isExcludeSelf, id))) { + throw new BusinessException(UserResponseEnum.REGISTER_EMAIL_FAIL); + } + } +}