2 Commits 4e3aa772f9 ... 8c93624f82

Tác giả SHA1 Thông báo Ngày
  Nobita 8c93624f82 feat: Initialize item 11 tháng trước cách đây
  Nobita 362bcdfb44 feat: Initialize item 11 tháng trước cách đây
100 tập tin đã thay đổi với 10241 bổ sung40 xóa
  1. 33 1
      himeeter-admin/pom.xml
  2. 14 0
      himeeter-admin/src/main/java/com/lij/himeeter/HimeeterApplication.java
  3. 27 0
      himeeter-admin/src/main/java/com/lij/himeeter/HimeeterServletInitializer.java
  4. 0 13
      himeeter-admin/src/main/java/com/lij/himeeter/admin/HimeeterAdminApplication.java
  5. 105 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/common/CaptchaCtrl.java
  6. 153 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/common/CommonCtrl.java
  7. 115 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/CacheCtrl.java
  8. 35 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/ServerCtrl.java
  9. 82 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/SysLogininforCtrl.java
  10. 69 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/SysOperlogCtrl.java
  11. 79 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/SysUserOnlineCtrl.java
  12. 126 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysConfigCtrl.java
  13. 121 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysDeptCtrl.java
  14. 116 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysDictDataCtrl.java
  15. 124 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysDictTypeCtrl.java
  16. 39 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysIndexCtrl.java
  17. 103 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysLoginCtrl.java
  18. 128 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysMenuCtrl.java
  19. 89 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysNoticeCtrl.java
  20. 119 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysPostCtrl.java
  21. 132 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysProfileCtrl.java
  22. 45 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysRegisterCtrl.java
  23. 242 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysRoleCtrl.java
  24. 234 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysUserCtrl.java
  25. 154 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/controller/tool/TestCtrl.java
  26. 72 0
      himeeter-admin/src/main/java/com/lij/himeeter/web/core/config/SwaggerConfig.java
  27. 1 0
      himeeter-admin/src/main/resources/META-INF/spring-devtools.properties
  28. 61 0
      himeeter-admin/src/main/resources/application-druid.yml
  29. 0 1
      himeeter-admin/src/main/resources/application.properties
  30. 138 0
      himeeter-admin/src/main/resources/application.yml
  31. 10 0
      himeeter-admin/src/main/resources/banner.txt
  32. 38 0
      himeeter-admin/src/main/resources/i18n/messages.properties
  33. 93 0
      himeeter-admin/src/main/resources/logback.xml
  34. 20 0
      himeeter-admin/src/main/resources/mybatis/mybatis-config.xml
  35. 66 0
      himeeter-common/src/main/java/com/lij/himeeter/common/config/serializer/SensitiveJsonSerializer.java
  36. 196 0
      himeeter-common/src/main/java/com/lij/himeeter/common/constant/GenConstants.java
  37. 66 0
      himeeter-common/src/main/java/com/lij/himeeter/common/constant/ScheduleConstants.java
  38. 193 0
      himeeter-common/src/main/java/com/lij/himeeter/common/core/controller/BaseController.java
  39. 102 0
      himeeter-common/src/main/java/com/lij/himeeter/common/core/page/PageDomain.java
  40. 91 0
      himeeter-common/src/main/java/com/lij/himeeter/common/core/page/TableDataInfo.java
  41. 62 0
      himeeter-common/src/main/java/com/lij/himeeter/common/core/page/TableSupport.java
  42. 1 1
      himeeter-common/src/main/java/com/lij/himeeter/common/core/redis/RedisCache.java
  43. 1 1
      himeeter-common/src/main/java/com/lij/himeeter/common/core/text/CharsetKit.java
  44. 2 4
      himeeter-common/src/main/java/com/lij/himeeter/common/enums/DesensitizedType.java
  45. 42 0
      himeeter-common/src/main/java/com/lij/himeeter/common/enums/HttpMethod.java
  46. 38 0
      himeeter-common/src/main/java/com/lij/himeeter/common/enums/UserStatus.java
  47. 23 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/DemoModeException.java
  48. 60 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/GlobalException.java
  49. 31 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/UtilException.java
  50. 94 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/base/BaseException.java
  51. 26 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileException.java
  52. 24 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileNameLengthLimitExceededException.java
  53. 24 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileSizeLimitExceededException.java
  54. 62 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileUploadException.java
  55. 77 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/InvalidExtensionException.java
  56. 39 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/job/TaskException.java
  57. 24 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/BlackListException.java
  58. 23 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/CaptchaException.java
  59. 23 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/CaptchaExpireException.java
  60. 26 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserException.java
  61. 23 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserNotExistsException.java
  62. 24 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserPasswordNotMatchException.java
  63. 24 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserPasswordRetryLimitExceedException.java
  64. 49 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/RepeatableFilter.java
  65. 77 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/RepeatedlyRequestWrapper.java
  66. 72 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/XssFilter.java
  67. 120 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/XssHttpServletRequestWrapper.java
  68. 122 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/Arith.java
  69. 171 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/DateUtils.java
  70. 214 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/DictUtils.java
  71. 45 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/ExceptionUtil.java
  72. 35 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/MessageUtils.java
  73. 42 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/PageUtils.java
  74. 114 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/bean/BeanUtils.java
  75. 32 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/bean/BeanValidators.java
  76. 74 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/FileTypeUtils.java
  77. 241 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/FileUploadUtils.java
  78. 253 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/FileUtils.java
  79. 89 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/ImageUtils.java
  80. 66 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/MimeTypeUtils.java
  81. 150 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/html/EscapeUtil.java
  82. 507 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/html/HTMLFilter.java
  83. 36 1
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/http/HttpHelper.java
  84. 7 14
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/ip/AddressUtils.java
  85. 1530 2
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/poi/ExcelUtil.java
  86. 334 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/reflect/ReflectUtils.java
  87. 263 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/sign/Base64.java
  88. 65 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/sign/Md5Utils.java
  89. 2 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/spring/SpringUtils.java
  90. 71 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/sql/SqlUtil.java
  91. 60 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/IdUtils.java
  92. 98 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/Seq.java
  93. 450 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/UUID.java
  94. 1 1
      himeeter-framework/src/main/java/com/lij/himeeter/framework/aspectj/LogAspect.java
  95. 71 1
      himeeter-framework/src/main/java/com/lij/himeeter/framework/aspectj/RateLimiterAspect.java
  96. 38 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ApplicationConfig.java
  97. 92 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/CaptchaConfig.java
  98. 122 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/DruidConfig.java
  99. 56 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/FastJson2JsonRedisSerializer.java
  100. 68 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/FilterConfig.java

+ 33 - 1
himeeter-admin/pom.xml

@@ -7,13 +7,15 @@
         <artifactId>himeeter</artifactId>
         <version>1.0.0</version>
     </parent>
+    <packaging>jar</packaging>
 
     <artifactId>himeeter-admin</artifactId>
     <description>
-        himeeter-admin
+        web服务入口
     </description>
 
     <dependencies>
+
         <!-- spring-boot-devtools -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -51,4 +53,34 @@
             <artifactId>himeeter-generator</artifactId>
         </dependency>
     </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.5.15</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
 </project>

+ 14 - 0
himeeter-admin/src/main/java/com/lij/himeeter/HimeeterApplication.java

@@ -0,0 +1,14 @@
+package com.lij.himeeter;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class HimeeterApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(HimeeterApplication.class, args);
+        System.out.println("\n(♥◠‿◠)ノ゙  Himeeter启动成功   ლ(´ڡ`ლ)゙  \n");
+    }
+
+}

+ 27 - 0
himeeter-admin/src/main/java/com/lij/himeeter/HimeeterServletInitializer.java

@@ -0,0 +1,27 @@
+package com.lij.himeeter;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  HimeeterServletInitializer
+ * 创建时间 : 2025年01月06日 04:07
+ *
+ * 文件描述 : web容器中进行部署
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class HimeeterServletInitializer extends SpringBootServletInitializer {
+
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(HimeeterApplication.class);
+    }
+}

+ 0 - 13
himeeter-admin/src/main/java/com/lij/himeeter/admin/HimeeterAdminApplication.java

@@ -1,13 +0,0 @@
-package com.lij.himeeter.admin;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class HimeeterAdminApplication {
-
-    public static void main(String[] args) {
-        SpringApplication.run(HimeeterAdminApplication.class, args);
-    }
-
-}

+ 105 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/common/CaptchaCtrl.java

@@ -0,0 +1,105 @@
+package com.lij.himeeter.web.controller.common;
+
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.constant.CacheConstants;
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.redis.RedisCache;
+import com.lij.himeeter.common.utils.sign.Base64;
+import com.lij.himeeter.common.utils.uuid.IdUtils;
+import com.lij.himeeter.system.service.ISysConfigService;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.FastByteArrayOutputStream;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.google.code.kaptcha.Producer;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  CaptchaController
+ * 创建时间 : 2025年01月06日 04:10
+ *
+ * 文件描述 : 验证码操作处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+public class CaptchaCtrl {
+
+    @Resource(name = "captchaProducer")
+    private Producer captchaProducer;
+
+    @Resource(name = "captchaProducerMath")
+    private Producer captchaProducerMath;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ISysConfigService configService;
+    /**
+     * 生成验证码
+     */
+    @GetMapping("/captchaImage")
+    public AjaxResult getCode(HttpServletResponse response) throws IOException
+    {
+        AjaxResult ajax = AjaxResult.success();
+        boolean captchaEnabled = configService.selectCaptchaEnabled();
+        ajax.put("captchaEnabled", captchaEnabled);
+        if (!captchaEnabled)
+        {
+            return ajax;
+        }
+
+        // 保存验证码信息
+        String uuid = IdUtils.simpleUUID();
+        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
+
+        String capStr = null, code = null;
+        BufferedImage image = null;
+
+        // 生成验证码
+        String captchaType = HimeeterConfig.getCaptchaType();
+        if ("math".equals(captchaType))
+        {
+            String capText = captchaProducerMath.createText();
+            capStr = capText.substring(0, capText.lastIndexOf("@"));
+            code = capText.substring(capText.lastIndexOf("@") + 1);
+            image = captchaProducerMath.createImage(capStr);
+        }
+        else if ("char".equals(captchaType))
+        {
+            capStr = code = captchaProducer.createText();
+            image = captchaProducer.createImage(capStr);
+        }
+
+        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
+        // 转换流信息写出
+        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
+        try
+        {
+            ImageIO.write(image, "jpg", os);
+        }
+        catch (IOException e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+
+        ajax.put("uuid", uuid);
+        ajax.put("img", Base64.encode(os.toByteArray()));
+        return ajax;
+    }
+}

+ 153 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/common/CommonCtrl.java

@@ -0,0 +1,153 @@
+package com.lij.himeeter.web.controller.common;
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.file.FileUploadUtils;
+import com.lij.himeeter.common.utils.file.FileUtils;
+import com.lij.himeeter.framework.config.ServerConfig;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  CommonCtrl
+ * 创建时间 : 2025年01月06日 04:26
+ *
+ * 文件描述 : 通用请求处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/common")
+public class CommonCtrl {
+
+    private static final Logger log = LoggerFactory.getLogger(CommonCtrl.class);
+
+    @Autowired
+    private ServerConfig serverConfig;
+
+    private static final String FILE_DELIMETER = ",";
+
+    /**
+     * 通用下载请求
+     *
+     * @param fileName 文件名称
+     * @param delete   是否删除
+     */
+    @GetMapping("/download")
+    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) {
+        try {
+            if (!FileUtils.checkAllowDownload(fileName)) {
+                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
+            }
+            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
+            String filePath = HimeeterConfig.getDownloadPath() + fileName;
+
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, realFileName);
+            FileUtils.writeBytes(filePath, response.getOutputStream());
+            if (delete) {
+                FileUtils.deleteFile(filePath);
+            }
+        } catch (Exception e) {
+            log.error("下载文件失败", e);
+        }
+    }
+
+    /**
+     * 通用上传请求(单个)
+     */
+    @PostMapping("/upload")
+    public AjaxResult uploadFile(MultipartFile file) throws Exception {
+        try {
+            // 上传文件路径
+            String filePath = HimeeterConfig.getUploadPath();
+            // 上传并返回新文件名称
+            String fileName = FileUploadUtils.upload(filePath, file);
+            String url = serverConfig.getUrl() + fileName;
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("url", url);
+            ajax.put("fileName", fileName);
+            ajax.put("newFileName", FileUtils.getName(fileName));
+            ajax.put("originalFilename", file.getOriginalFilename());
+            return ajax;
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 通用上传请求(多个)
+     */
+    @PostMapping("/uploads")
+    public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception {
+        try {
+            // 上传文件路径
+            String filePath = HimeeterConfig.getUploadPath();
+            List<String> urls = new ArrayList<String>();
+            List<String> fileNames = new ArrayList<String>();
+            List<String> newFileNames = new ArrayList<String>();
+            List<String> originalFilenames = new ArrayList<String>();
+            for (MultipartFile file : files) {
+                // 上传并返回新文件名称
+                String fileName = FileUploadUtils.upload(filePath, file);
+                String url = serverConfig.getUrl() + fileName;
+                urls.add(url);
+                fileNames.add(fileName);
+                newFileNames.add(FileUtils.getName(fileName));
+                originalFilenames.add(file.getOriginalFilename());
+            }
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
+            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
+            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
+            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
+            return ajax;
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 本地资源通用下载
+     */
+    @GetMapping("/download/resource")
+    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
+            throws Exception {
+        try {
+            if (!FileUtils.checkAllowDownload(resource)) {
+                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
+            }
+            // 本地资源路径
+            String localPath = HimeeterConfig.getProfile();
+            // 数据库资源地址
+            String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
+            // 下载名称
+            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, downloadName);
+            FileUtils.writeBytes(downloadPath, response.getOutputStream());
+        } catch (Exception e) {
+            log.error("下载文件失败", e);
+        }
+    }
+}

+ 115 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/CacheCtrl.java

@@ -0,0 +1,115 @@
+package com.lij.himeeter.web.controller.monitor;
+
+import com.lij.himeeter.common.constant.CacheConstants;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.domain.SysCache;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  CacheCtrl
+ * 创建时间 : 2025年01月06日 04:29
+ *
+ * 文件描述 : 缓存监控
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/monitor/cache")
+public class CacheCtrl {
+
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    private final static List<SysCache> caches = new ArrayList<SysCache>();
+
+    {
+        caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
+        caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
+        caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
+        caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
+        caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
+        caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
+        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
+    }
+
+    @SuppressWarnings("deprecation")
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping()
+    public AjaxResult getInfo() throws Exception {
+        Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
+        Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
+        Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
+
+        Map<String, Object> result = new HashMap<>(3);
+        result.put("info", info);
+        result.put("dbSize", dbSize);
+
+        List<Map<String, String>> pieList = new ArrayList<>();
+        commandStats.stringPropertyNames().forEach(key -> {
+            Map<String, String> data = new HashMap<>(2);
+            String property = commandStats.getProperty(key);
+            data.put("name", StringUtils.removeStart(key, "cmdstat_"));
+            data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
+            pieList.add(data);
+        });
+        result.put("commandStats", pieList);
+        return AjaxResult.success(result);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getNames")
+    public AjaxResult cache() {
+        return AjaxResult.success(caches);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getKeys/{cacheName}")
+    public AjaxResult getCacheKeys(@PathVariable String cacheName) {
+        Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+        return AjaxResult.success(new TreeSet<>(cacheKeys));
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getValue/{cacheName}/{cacheKey}")
+    public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) {
+        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
+        SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
+        return AjaxResult.success(sysCache);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheName/{cacheName}")
+    public AjaxResult clearCacheName(@PathVariable String cacheName) {
+        Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+        redisTemplate.delete(cacheKeys);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheKey/{cacheKey}")
+    public AjaxResult clearCacheKey(@PathVariable String cacheKey) {
+        redisTemplate.delete(cacheKey);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheAll")
+    public AjaxResult clearCacheAll() {
+        Collection<String> cacheKeys = redisTemplate.keys("*");
+        redisTemplate.delete(cacheKeys);
+        return AjaxResult.success();
+    }
+}

+ 35 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/ServerCtrl.java

@@ -0,0 +1,35 @@
+package com.lij.himeeter.web.controller.monitor;
+
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.framework.web.domain.Server;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ServerCtrl
+ * 创建时间 : 2025年01月06日 04:32
+ *
+ * 文件描述 : 服务器监控
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/monitor/server")
+public class ServerCtrl {
+
+    @PreAuthorize("@ss.hasPermi('monitor:server:list')")
+    @GetMapping()
+    public AjaxResult getInfo() throws Exception {
+        Server server = new Server();
+        server.copyTo();
+        return AjaxResult.success(server);
+    }
+}

+ 82 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/SysLogininforCtrl.java

@@ -0,0 +1,82 @@
+package com.lij.himeeter.web.controller.monitor;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.framework.web.service.SysPasswordService;
+import com.lij.himeeter.system.domain.SysLogininfor;
+import com.lij.himeeter.system.service.ISysLogininforService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysLogininforCtrl
+ * 创建时间 : 2025年01月06日 04:32
+ *
+ * 文件描述 : 系统访问记录
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/monitor/logininfor")
+public class SysLogininforCtrl extends BaseController {
+
+    @Autowired
+    private ISysLogininforService logininforService;
+
+    @Autowired
+    private SysPasswordService passwordService;
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysLogininfor logininfor) {
+        startPage();
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+        return getDataTable(list);
+    }
+
+    @Log(title = "登录日志", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysLogininfor logininfor) {
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+        ExcelUtil<SysLogininfor> util = new ExcelUtil<>(SysLogininfor.class);
+        util.exportExcel(response, list, "登录日志");
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+    @Log(title = "登录日志", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{infoIds}")
+    public AjaxResult remove(@PathVariable Long[] infoIds) {
+        return toAjax(logininforService.deleteLogininforByIds(infoIds));
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+    @Log(title = "登录日志", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/clean")
+    public AjaxResult clean() {
+        logininforService.cleanLogininfor();
+        return success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
+    @Log(title = "账户解锁", businessType = BusinessType.OTHER)
+    @GetMapping("/unlock/{userName}")
+    public AjaxResult unlock(@PathVariable("userName") String userName) {
+        passwordService.clearLoginRecordCache(userName);
+        return success();
+    }
+}

+ 69 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/SysOperlogCtrl.java

@@ -0,0 +1,69 @@
+package com.lij.himeeter.web.controller.monitor;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.system.domain.SysOperLog;
+import com.lij.himeeter.system.service.ISysOperLogService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysOperlogCtrl
+ * 创建时间 : 2025年01月06日 04:38
+ *
+ * 文件描述 : 操作日志记录
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/monitor/operlog")
+public class SysOperlogCtrl extends BaseController {
+    @Autowired
+    private ISysOperLogService operLogService;
+
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysOperLog operLog) {
+        startPage();
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+        return getDataTable(list);
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysOperLog operLog) {
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+        ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
+        util.exportExcel(response, list, "操作日志");
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.DELETE)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+    @DeleteMapping("/{operIds}")
+    public AjaxResult remove(@PathVariable Long[] operIds) {
+        return toAjax(operLogService.deleteOperLogByIds(operIds));
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.CLEAN)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+    @DeleteMapping("/clean")
+    public AjaxResult clean() {
+        operLogService.cleanOperLog();
+        return success();
+    }
+}

+ 79 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/monitor/SysUserOnlineCtrl.java

@@ -0,0 +1,79 @@
+package com.lij.himeeter.web.controller.monitor;
+
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.constant.CacheConstants;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.core.redis.RedisCache;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.domain.SysUserOnline;
+import com.lij.himeeter.system.service.ISysUserOnlineService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysUserOnlineCtrl
+ * 创建时间 : 2025年01月06日 04:38
+ *
+ * 文件描述 : 在线用户监控
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/monitor/online")
+public class SysUserOnlineCtrl extends BaseController {
+    @Autowired
+    private ISysUserOnlineService userOnlineService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @PreAuthorize("@ss.hasPermi('monitor:online:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(String ipaddr, String userName) {
+        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
+        List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
+        for (String key : keys) {
+            LoginUser user = redisCache.getCacheObject(key);
+            if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) {
+                userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
+            } else if (StringUtils.isNotEmpty(ipaddr)) {
+                userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
+            } else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) {
+                userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
+            } else {
+                userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
+            }
+        }
+        Collections.reverse(userOnlineList);
+        userOnlineList.removeAll(Collections.singleton(null));
+        return getDataTable(userOnlineList);
+    }
+
+    /**
+     * 强退用户
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
+    @Log(title = "在线用户", businessType = BusinessType.FORCE)
+    @DeleteMapping("/{tokenId}")
+    public AjaxResult forceLogout(@PathVariable String tokenId) {
+        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
+        return success();
+    }
+}

+ 126 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysConfigCtrl.java

@@ -0,0 +1,126 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.system.domain.SysConfig;
+import com.lij.himeeter.system.service.ISysConfigService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysConfigCtrl
+ * 创建时间 : 2025年01月06日 04:43
+ *
+ * 文件描述 : 参数配置 信息操作处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/config")
+public class SysConfigCtrl extends BaseController {
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 获取参数配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysConfig config) {
+        startPage();
+        List<SysConfig> list = configService.selectConfigList(config);
+        return getDataTable(list);
+    }
+
+    @Log(title = "参数管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:config:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysConfig config) {
+        List<SysConfig> list = configService.selectConfigList(config);
+        ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
+        util.exportExcel(response, list, "参数数据");
+    }
+
+    /**
+     * 根据参数编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:query')")
+    @GetMapping(value = "/{configId}")
+    public AjaxResult getInfo(@PathVariable Long configId) {
+        return success(configService.selectConfigById(configId));
+    }
+
+    /**
+     * 根据参数键名查询参数值
+     */
+    @GetMapping(value = "/configKey/{configKey}")
+    public AjaxResult getConfigKey(@PathVariable String configKey) {
+        return success(configService.selectConfigByKey(configKey));
+    }
+
+    /**
+     * 新增参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:add')")
+    @Log(title = "参数管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysConfig config) {
+        if (!configService.checkConfigKeyUnique(config)) {
+            return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
+        }
+        config.setCreateBy(getUsername());
+        return toAjax(configService.insertConfig(config));
+    }
+
+    /**
+     * 修改参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:edit')")
+    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysConfig config) {
+        if (!configService.checkConfigKeyUnique(config)) {
+            return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
+        }
+        config.setUpdateBy(getUsername());
+        return toAjax(configService.updateConfig(config));
+    }
+
+    /**
+     * 删除参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
+    @Log(title = "参数管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{configIds}")
+    public AjaxResult remove(@PathVariable Long[] configIds) {
+        configService.deleteConfigByIds(configIds);
+        return success();
+    }
+
+    /**
+     * 刷新参数缓存
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
+    @Log(title = "参数管理", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/refreshCache")
+    public AjaxResult refreshCache() {
+        configService.resetConfigCache();
+        return success();
+    }
+}

+ 121 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysDeptCtrl.java

@@ -0,0 +1,121 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.constant.UserConstants;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysDept;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.service.ISysDeptService;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysDeptCtrl
+ * 创建时间 : 2025年01月06日 04:44
+ *
+ * 文件描述 : 部门信息
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/dept")
+public class SysDeptCtrl extends BaseController {
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    /**
+     * 获取部门列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list")
+    public AjaxResult list(SysDept dept) {
+        List<SysDept> depts = deptService.selectDeptList(dept);
+        return success(depts);
+    }
+
+    /**
+     * 查询部门列表(排除节点)
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list/exclude/{deptId}")
+    public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) {
+        List<SysDept> depts = deptService.selectDeptList(new SysDept());
+        depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
+        return success(depts);
+    }
+
+    /**
+     * 根据部门编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:query')")
+    @GetMapping(value = "/{deptId}")
+    public AjaxResult getInfo(@PathVariable Long deptId) {
+        deptService.checkDeptDataScope(deptId);
+        return success(deptService.selectDeptById(deptId));
+    }
+
+    /**
+     * 新增部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:add')")
+    @Log(title = "部门管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDept dept) {
+        if (!deptService.checkDeptNameUnique(dept)) {
+            return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        }
+        dept.setCreateBy(getUsername());
+        return toAjax(deptService.insertDept(dept));
+    }
+
+    /**
+     * 修改部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
+    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDept dept) {
+        Long deptId = dept.getDeptId();
+        deptService.checkDeptDataScope(deptId);
+        if (!deptService.checkDeptNameUnique(dept)) {
+            return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        } else if (dept.getParentId().equals(deptId)) {
+            return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
+        } else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) {
+            return error("该部门包含未停用的子部门!");
+        }
+        dept.setUpdateBy(getUsername());
+        return toAjax(deptService.updateDept(dept));
+    }
+
+    /**
+     * 删除部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:remove')")
+    @Log(title = "部门管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{deptId}")
+    public AjaxResult remove(@PathVariable Long deptId) {
+        if (deptService.hasChildByDeptId(deptId)) {
+            return warn("存在下级部门,不允许删除");
+        }
+        if (deptService.checkDeptExistUser(deptId)) {
+            return warn("部门存在用户,不允许删除");
+        }
+        deptService.checkDeptDataScope(deptId);
+        return toAjax(deptService.deleteDeptById(deptId));
+    }
+}

+ 116 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysDictDataCtrl.java

@@ -0,0 +1,116 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysDictData;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.system.service.ISysDictDataService;
+import com.lij.himeeter.system.service.ISysDictTypeService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysDictDataCtrl
+ * 创建时间 : 2025年01月06日 04:45
+ *
+ * 文件描述 : 数据字典信息
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/dict/data")
+public class SysDictDataCtrl extends BaseController {
+
+    @Autowired
+    private ISysDictDataService dictDataService;
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysDictData dictData) {
+        startPage();
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+        return getDataTable(list);
+    }
+
+    @Log(title = "字典数据", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysDictData dictData) {
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+        ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
+        util.exportExcel(response, list, "字典数据");
+    }
+
+    /**
+     * 查询字典数据详细
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
+    @GetMapping(value = "/{dictCode}")
+    public AjaxResult getInfo(@PathVariable Long dictCode) {
+        return success(dictDataService.selectDictDataById(dictCode));
+    }
+
+    /**
+     * 根据字典类型查询字典数据信息
+     */
+    @GetMapping(value = "/type/{dictType}")
+    public AjaxResult dictType(@PathVariable String dictType) {
+        List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
+        if (StringUtils.isNull(data)) {
+            data = new ArrayList<SysDictData>();
+        }
+        return success(data);
+    }
+
+    /**
+     * 新增字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
+    @Log(title = "字典数据", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDictData dict) {
+        dict.setCreateBy(getUsername());
+        return toAjax(dictDataService.insertDictData(dict));
+    }
+
+    /**
+     * 修改保存字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+    @Log(title = "字典数据", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDictData dict) {
+        dict.setUpdateBy(getUsername());
+        return toAjax(dictDataService.updateDictData(dict));
+    }
+
+    /**
+     * 删除字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{dictCodes}")
+    public AjaxResult remove(@PathVariable Long[] dictCodes) {
+        dictDataService.deleteDictDataByIds(dictCodes);
+        return success();
+    }
+}

+ 124 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysDictTypeCtrl.java

@@ -0,0 +1,124 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysDictType;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.system.service.ISysDictTypeService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysDictTypeCtrl
+ * 创建时间 : 2025年01月06日 04:46
+ *
+ * 文件描述 : 数据字典信息
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/dict/type")
+public class SysDictTypeCtrl extends BaseController {
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysDictType dictType) {
+        startPage();
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+        return getDataTable(list);
+    }
+
+    @Log(title = "字典类型", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysDictType dictType) {
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+        ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
+        util.exportExcel(response, list, "字典类型");
+    }
+
+    /**
+     * 查询字典类型详细
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
+    @GetMapping(value = "/{dictId}")
+    public AjaxResult getInfo(@PathVariable Long dictId) {
+        return success(dictTypeService.selectDictTypeById(dictId));
+    }
+
+    /**
+     * 新增字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
+    @Log(title = "字典类型", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDictType dict) {
+        if (!dictTypeService.checkDictTypeUnique(dict)) {
+            return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
+        }
+        dict.setCreateBy(getUsername());
+        return toAjax(dictTypeService.insertDictType(dict));
+    }
+
+    /**
+     * 修改字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+    @Log(title = "字典类型", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDictType dict) {
+        if (!dictTypeService.checkDictTypeUnique(dict)) {
+            return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
+        }
+        dict.setUpdateBy(getUsername());
+        return toAjax(dictTypeService.updateDictType(dict));
+    }
+
+    /**
+     * 删除字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{dictIds}")
+    public AjaxResult remove(@PathVariable Long[] dictIds) {
+        dictTypeService.deleteDictTypeByIds(dictIds);
+        return success();
+    }
+
+    /**
+     * 刷新字典缓存
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/refreshCache")
+    public AjaxResult refreshCache() {
+        dictTypeService.resetDictCache();
+        return success();
+    }
+
+    /**
+     * 获取字典选择框列表
+     */
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect() {
+        List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
+        return success(dictTypes);
+    }
+}

+ 39 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysIndexCtrl.java

@@ -0,0 +1,39 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysIndexCtrl
+ * 创建时间 : 2025年01月06日 04:46
+ *
+ * 文件描述 : 无
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+public class SysIndexCtrl {
+
+    /**
+     * 系统基础配置
+     */
+    @Autowired
+    private HimeeterConfig himeeterConfig;
+
+    /**
+     * 访问首页,提示语
+     */
+    @RequestMapping("/")
+    public String index() {
+        return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", himeeterConfig.getName(), himeeterConfig.getVersion());
+    }
+}

+ 103 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysLoginCtrl.java

@@ -0,0 +1,103 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysMenu;
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+import com.lij.himeeter.common.core.domain.model.LoginBody;
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.framework.web.service.SysLoginService;
+import com.lij.himeeter.framework.web.service.SysPermissionService;
+import com.lij.himeeter.framework.web.service.TokenService;
+import com.lij.himeeter.system.service.ISysMenuService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysLoginCtrl
+ * 创建时间 : 2025年01月06日 04:48
+ *
+ * 文件描述 : 登录验证
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+public class SysLoginCtrl {
+
+    @Autowired
+    private SysLoginService loginService;
+
+    @Autowired
+    private ISysMenuService menuService;
+
+    @Autowired
+    private SysPermissionService permissionService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 登录方法
+     *
+     * @param loginBody 登录信息
+     * @return 结果
+     */
+    @PostMapping("/login")
+    public AjaxResult login(@RequestBody LoginBody loginBody) {
+        AjaxResult ajax = AjaxResult.success();
+        // 生成令牌
+        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
+                loginBody.getUuid());
+        ajax.put(Constants.TOKEN, token);
+        return ajax;
+    }
+
+    /**
+     * 获取用户信息
+     *
+     * @return 用户信息
+     */
+    @GetMapping("getInfo")
+    public AjaxResult getInfo() {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        SysUser user = loginUser.getUser();
+        // 角色集合
+        Set<String> roles = permissionService.getRolePermission(user);
+        // 权限集合
+        Set<String> permissions = permissionService.getMenuPermission(user);
+        if (!loginUser.getPermissions().equals(permissions)) {
+            loginUser.setPermissions(permissions);
+            tokenService.refreshToken(loginUser);
+        }
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("user", user);
+        ajax.put("roles", roles);
+        ajax.put("permissions", permissions);
+        return ajax;
+    }
+
+    /**
+     * 获取路由信息
+     *
+     * @return 路由信息
+     */
+    @GetMapping("getRouters")
+    public AjaxResult getRouters() {
+        Long userId = SecurityUtils.getUserId();
+        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
+        return AjaxResult.success(menuService.buildMenus(menus));
+    }
+}

+ 128 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysMenuCtrl.java

@@ -0,0 +1,128 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.constant.UserConstants;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysMenu;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.service.ISysMenuService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysMenuCtrl
+ * 创建时间 : 2025年01月06日 04:49
+ *
+ * 文件描述 : 菜单信息
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/menu")
+public class SysMenuCtrl extends BaseController {
+
+    @Autowired
+    private ISysMenuService menuService;
+
+    /**
+     * 获取菜单列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:list')")
+    @GetMapping("/list")
+    public AjaxResult list(SysMenu menu) {
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+        return success(menus);
+    }
+
+    /**
+     * 根据菜单编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:query')")
+    @GetMapping(value = "/{menuId}")
+    public AjaxResult getInfo(@PathVariable Long menuId) {
+        return success(menuService.selectMenuById(menuId));
+    }
+
+    /**
+     * 获取菜单下拉树列表
+     */
+    @GetMapping("/treeselect")
+    public AjaxResult treeselect(SysMenu menu) {
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+        return success(menuService.buildMenuTreeSelect(menus));
+    }
+
+    /**
+     * 加载对应角色菜单列表树
+     */
+    @GetMapping(value = "/roleMenuTreeselect/{roleId}")
+    public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) {
+        List<SysMenu> menus = menuService.selectMenuList(getUserId());
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
+        ajax.put("menus", menuService.buildMenuTreeSelect(menus));
+        return ajax;
+    }
+
+    /**
+     * 新增菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:add')")
+    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysMenu menu) {
+        if (!menuService.checkMenuNameUnique(menu)) {
+            return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) {
+            return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        menu.setCreateBy(getUsername());
+        return toAjax(menuService.insertMenu(menu));
+    }
+
+    /**
+     * 修改菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysMenu menu) {
+        if (!menuService.checkMenuNameUnique(menu)) {
+            return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) {
+            return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        } else if (menu.getMenuId().equals(menu.getParentId())) {
+            return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+        }
+        menu.setUpdateBy(getUsername());
+        return toAjax(menuService.updateMenu(menu));
+    }
+
+    /**
+     * 删除菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
+    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{menuId}")
+    public AjaxResult remove(@PathVariable("menuId") Long menuId) {
+        if (menuService.hasChildByMenuId(menuId)) {
+            return warn("存在子菜单,不允许删除");
+        }
+        if (menuService.checkMenuExistRole(menuId)) {
+            return warn("菜单已分配,不允许删除");
+        }
+        return toAjax(menuService.deleteMenuById(menuId));
+    }
+}

+ 89 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysNoticeCtrl.java

@@ -0,0 +1,89 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.system.domain.SysNotice;
+import com.lij.himeeter.system.service.ISysNoticeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysNoticeCtrl
+ * 创建时间 : 2025年01月06日 04:49
+ *
+ * 文件描述 : 公告 信息操作处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/notice")
+public class SysNoticeCtrl extends BaseController {
+
+    @Autowired
+    private ISysNoticeService noticeService;
+
+    /**
+     * 获取通知公告列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysNotice notice) {
+        startPage();
+        List<SysNotice> list = noticeService.selectNoticeList(notice);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据通知公告编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:query')")
+    @GetMapping(value = "/{noticeId}")
+    public AjaxResult getInfo(@PathVariable Long noticeId) {
+        return success(noticeService.selectNoticeById(noticeId));
+    }
+
+    /**
+     * 新增通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:add')")
+    @Log(title = "通知公告", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysNotice notice) {
+        notice.setCreateBy(getUsername());
+        return toAjax(noticeService.insertNotice(notice));
+    }
+
+    /**
+     * 修改通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:edit')")
+    @Log(title = "通知公告", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysNotice notice) {
+        notice.setUpdateBy(getUsername());
+        return toAjax(noticeService.updateNotice(notice));
+    }
+
+    /**
+     * 删除通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:remove')")
+    @Log(title = "通知公告", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{noticeIds}")
+    public AjaxResult remove(@PathVariable Long[] noticeIds) {
+        return toAjax(noticeService.deleteNoticeByIds(noticeIds));
+    }
+}

+ 119 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysPostCtrl.java

@@ -0,0 +1,119 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.system.domain.SysPost;
+import com.lij.himeeter.system.service.ISysPostService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysPostCtrl
+ * 创建时间 : 2025年01月06日 04:50
+ *
+ * 文件描述 : 岗位信息操作处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/post")
+public class SysPostCtrl extends BaseController {
+
+    @Autowired
+    private ISysPostService postService;
+
+    /**
+     * 获取岗位列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysPost post) {
+        startPage();
+        List<SysPost> list = postService.selectPostList(post);
+        return getDataTable(list);
+    }
+
+    @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:post:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysPost post) {
+        List<SysPost> list = postService.selectPostList(post);
+        ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
+        util.exportExcel(response, list, "岗位数据");
+    }
+
+    /**
+     * 根据岗位编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:query')")
+    @GetMapping(value = "/{postId}")
+    public AjaxResult getInfo(@PathVariable Long postId) {
+        return success(postService.selectPostById(postId));
+    }
+
+    /**
+     * 新增岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:add')")
+    @Log(title = "岗位管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysPost post) {
+        if (!postService.checkPostNameUnique(post)) {
+            return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+        } else if (!postService.checkPostCodeUnique(post)) {
+            return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+        }
+        post.setCreateBy(getUsername());
+        return toAjax(postService.insertPost(post));
+    }
+
+    /**
+     * 修改岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:edit')")
+    @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysPost post) {
+        if (!postService.checkPostNameUnique(post)) {
+            return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+        } else if (!postService.checkPostCodeUnique(post)) {
+            return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+        }
+        post.setUpdateBy(getUsername());
+        return toAjax(postService.updatePost(post));
+    }
+
+    /**
+     * 删除岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:remove')")
+    @Log(title = "岗位管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{postIds}")
+    public AjaxResult remove(@PathVariable Long[] postIds) {
+        return toAjax(postService.deletePostByIds(postIds));
+    }
+
+    /**
+     * 获取岗位选择框列表
+     */
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect() {
+        List<SysPost> posts = postService.selectPostAll();
+        return success(posts);
+    }
+}

+ 132 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysProfileCtrl.java

@@ -0,0 +1,132 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.file.FileUploadUtils;
+import com.lij.himeeter.common.utils.file.MimeTypeUtils;
+import com.lij.himeeter.framework.web.service.TokenService;
+import com.lij.himeeter.system.service.ISysUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Map;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysProfileCtrl
+ * 创建时间 : 2025年01月06日 04:51
+ *
+ * 文件描述 : 个人信息 业务处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/user/profile")
+public class SysProfileCtrl extends BaseController {
+
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 个人信息
+     */
+    @GetMapping
+    public AjaxResult profile() {
+        LoginUser loginUser = getLoginUser();
+        SysUser user = loginUser.getUser();
+        AjaxResult ajax = AjaxResult.success(user);
+        ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
+        ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
+        return ajax;
+    }
+
+    /**
+     * 修改用户
+     */
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult updateProfile(@RequestBody SysUser user) {
+        LoginUser loginUser = getLoginUser();
+        SysUser currentUser = loginUser.getUser();
+        currentUser.setNickName(user.getNickName());
+        currentUser.setEmail(user.getEmail());
+        currentUser.setPhonenumber(user.getPhonenumber());
+        currentUser.setSex(user.getSex());
+        if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) {
+            return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
+        }
+        if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) {
+            return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
+        }
+        if (userService.updateUserProfile(currentUser) > 0) {
+            // 更新缓存用户信息
+            tokenService.setLoginUser(loginUser);
+            return success();
+        }
+        return error("修改个人信息异常,请联系管理员");
+    }
+
+    /**
+     * 重置密码
+     */
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+    @PutMapping("/updatePwd")
+    public AjaxResult updatePwd(@RequestBody Map<String, String> params) {
+        String oldPassword = params.get("oldPassword");
+        String newPassword = params.get("newPassword");
+        LoginUser loginUser = getLoginUser();
+        String userName = loginUser.getUsername();
+        String password = loginUser.getPassword();
+        if (!SecurityUtils.matchesPassword(oldPassword, password)) {
+            return error("修改密码失败,旧密码错误");
+        }
+        if (SecurityUtils.matchesPassword(newPassword, password)) {
+            return error("新密码不能与旧密码相同");
+        }
+        newPassword = SecurityUtils.encryptPassword(newPassword);
+        if (userService.resetUserPwd(userName, newPassword) > 0) {
+            // 更新缓存用户密码
+            loginUser.getUser().setPassword(newPassword);
+            tokenService.setLoginUser(loginUser);
+            return success();
+        }
+        return error("修改密码异常,请联系管理员");
+    }
+
+    /**
+     * 头像上传
+     */
+    @Log(title = "用户头像", businessType = BusinessType.UPDATE)
+    @PostMapping("/avatar")
+    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception {
+        if (!file.isEmpty()) {
+            LoginUser loginUser = getLoginUser();
+            String avatar = FileUploadUtils.upload(HimeeterConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
+            if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) {
+                AjaxResult ajax = AjaxResult.success();
+                ajax.put("imgUrl", avatar);
+                // 更新缓存用户头像
+                loginUser.getUser().setAvatar(avatar);
+                tokenService.setLoginUser(loginUser);
+                return ajax;
+            }
+        }
+        return error("上传图片异常,请联系管理员");
+    }
+}

+ 45 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysRegisterCtrl.java

@@ -0,0 +1,45 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.model.RegisterBody;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.framework.web.service.SysRegisterService;
+import com.lij.himeeter.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysRegisterCtrl
+ * 创建时间 : 2025年01月06日 04:52
+ *
+ * 文件描述 : 注册验证
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+public class SysRegisterCtrl extends BaseController {
+
+    @Autowired
+    private SysRegisterService registerService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @PostMapping("/register")
+    public AjaxResult register(@RequestBody RegisterBody user) {
+        if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) {
+            return error("当前系统没有开启注册功能!");
+        }
+        String msg = registerService.register(user);
+        return StringUtils.isEmpty(msg) ? success() : error(msg);
+    }
+}

+ 242 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysRoleCtrl.java

@@ -0,0 +1,242 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysDept;
+import com.lij.himeeter.common.core.domain.entity.SysRole;
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.framework.web.service.SysPermissionService;
+import com.lij.himeeter.framework.web.service.TokenService;
+import com.lij.himeeter.system.domain.SysUserRole;
+import com.lij.himeeter.system.service.ISysDeptService;
+import com.lij.himeeter.system.service.ISysRoleService;
+import com.lij.himeeter.system.service.ISysUserService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysRoleCtrl
+ * 创建时间 : 2025年01月06日 04:53
+ *
+ * 文件描述 : 角色信息
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/role")
+public class SysRoleCtrl extends BaseController {
+
+    @Autowired
+    private ISysRoleService roleService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private SysPermissionService permissionService;
+
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysRole role) {
+        startPage();
+        List<SysRole> list = roleService.selectRoleList(role);
+        return getDataTable(list);
+    }
+
+    @Log(title = "角色管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:role:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysRole role) {
+        List<SysRole> list = roleService.selectRoleList(role);
+        ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
+        util.exportExcel(response, list, "角色数据");
+    }
+
+    /**
+     * 根据角色编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping(value = "/{roleId}")
+    public AjaxResult getInfo(@PathVariable Long roleId) {
+        roleService.checkRoleDataScope(roleId);
+        return success(roleService.selectRoleById(roleId));
+    }
+
+    /**
+     * 新增角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:add')")
+    @Log(title = "角色管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysRole role) {
+        if (!roleService.checkRoleNameUnique(role)) {
+            return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
+        } else if (!roleService.checkRoleKeyUnique(role)) {
+            return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
+        }
+        role.setCreateBy(getUsername());
+        return toAjax(roleService.insertRole(role));
+
+    }
+
+    /**
+     * 修改保存角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysRole role) {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        if (!roleService.checkRoleNameUnique(role)) {
+            return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
+        } else if (!roleService.checkRoleKeyUnique(role)) {
+            return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
+        }
+        role.setUpdateBy(getUsername());
+
+        if (roleService.updateRole(role) > 0) {
+            // 更新缓存用户权限
+            LoginUser loginUser = getLoginUser();
+            if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) {
+                loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
+                loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
+                tokenService.setLoginUser(loginUser);
+            }
+            return success();
+        }
+        return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
+    }
+
+    /**
+     * 修改保存数据权限
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/dataScope")
+    public AjaxResult dataScope(@RequestBody SysRole role) {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        return toAjax(roleService.authDataScope(role));
+    }
+
+    /**
+     * 状态修改
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody SysRole role) {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        role.setUpdateBy(getUsername());
+        return toAjax(roleService.updateRoleStatus(role));
+    }
+
+    /**
+     * 删除角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:remove')")
+    @Log(title = "角色管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{roleIds}")
+    public AjaxResult remove(@PathVariable Long[] roleIds) {
+        return toAjax(roleService.deleteRoleByIds(roleIds));
+    }
+
+    /**
+     * 获取角色选择框列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect() {
+        return success(roleService.selectRoleAll());
+    }
+
+    /**
+     * 查询已分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/allocatedList")
+    public TableDataInfo allocatedList(SysUser user) {
+        startPage();
+        List<SysUser> list = userService.selectAllocatedList(user);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询未分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/unallocatedList")
+    public TableDataInfo unallocatedList(SysUser user) {
+        startPage();
+        List<SysUser> list = userService.selectUnallocatedList(user);
+        return getDataTable(list);
+    }
+
+    /**
+     * 取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancel")
+    public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) {
+        return toAjax(roleService.deleteAuthUser(userRole));
+    }
+
+    /**
+     * 批量取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancelAll")
+    public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) {
+        return toAjax(roleService.deleteAuthUsers(roleId, userIds));
+    }
+
+    /**
+     * 批量选择用户授权
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/selectAll")
+    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) {
+        roleService.checkRoleDataScope(roleId);
+        return toAjax(roleService.insertAuthUsers(roleId, userIds));
+    }
+
+    /**
+     * 获取对应角色部门树列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping(value = "/deptTree/{roleId}")
+    public AjaxResult deptTree(@PathVariable("roleId") Long roleId) {
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
+        ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
+        return ajax;
+    }
+}

+ 234 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/system/SysUserCtrl.java

@@ -0,0 +1,234 @@
+package com.lij.himeeter.web.controller.system;
+
+import com.lij.himeeter.common.annotation.Log;
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.entity.SysDept;
+import com.lij.himeeter.common.core.domain.entity.SysRole;
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.enums.BusinessType;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.poi.ExcelUtil;
+import com.lij.himeeter.system.service.ISysDeptService;
+import com.lij.himeeter.system.service.ISysPostService;
+import com.lij.himeeter.system.service.ISysRoleService;
+import com.lij.himeeter.system.service.ISysUserService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysUserCtrl
+ * 创建时间 : 2025年01月06日 04:54
+ *
+ * 文件描述 : 用户信息
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@RestController
+@RequestMapping("/system/user")
+public class SysUserCtrl extends BaseController {
+
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private ISysRoleService roleService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @Autowired
+    private ISysPostService postService;
+
+    /**
+     * 获取用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysUser user) {
+        startPage();
+        List<SysUser> list = userService.selectUserList(user);
+        return getDataTable(list);
+    }
+
+    @Log(title = "用户管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:user:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysUser user) {
+        List<SysUser> list = userService.selectUserList(user);
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        util.exportExcel(response, list, "用户数据");
+    }
+
+    @Log(title = "用户管理", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('system:user:import')")
+    @PostMapping("/importData")
+    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        List<SysUser> userList = util.importExcel(file.getInputStream());
+        String operName = getUsername();
+        String message = userService.importUser(userList, updateSupport, operName);
+        return success(message);
+    }
+
+    @PostMapping("/importTemplate")
+    public void importTemplate(HttpServletResponse response) {
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        util.importTemplateExcel(response, "用户数据");
+    }
+
+    /**
+     * 根据用户编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping(value = {"/", "/{userId}"})
+    public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) {
+        AjaxResult ajax = AjaxResult.success();
+        if (StringUtils.isNotNull(userId)) {
+            userService.checkUserDataScope(userId);
+            SysUser sysUser = userService.selectUserById(userId);
+            ajax.put(AjaxResult.DATA_TAG, sysUser);
+            ajax.put("postIds", postService.selectPostListByUserId(userId));
+            ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
+        }
+        List<SysRole> roles = roleService.selectRoleAll();
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+        ajax.put("posts", postService.selectPostAll());
+        return ajax;
+    }
+
+    /**
+     * 新增用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:add')")
+    @Log(title = "用户管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysUser user) {
+        deptService.checkDeptDataScope(user.getDeptId());
+        roleService.checkRoleDataScope(user.getRoleIds());
+        if (!userService.checkUserNameUnique(user)) {
+            return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
+        } else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
+            return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
+        } else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) {
+            return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+        }
+        user.setCreateBy(getUsername());
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+        return toAjax(userService.insertUser(user));
+    }
+
+    /**
+     * 修改用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysUser user) {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        deptService.checkDeptDataScope(user.getDeptId());
+        roleService.checkRoleDataScope(user.getRoleIds());
+        if (!userService.checkUserNameUnique(user)) {
+            return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
+        } else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
+            return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
+        } else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) {
+            return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+        }
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.updateUser(user));
+    }
+
+    /**
+     * 删除用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:remove')")
+    @Log(title = "用户管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{userIds}")
+    public AjaxResult remove(@PathVariable Long[] userIds) {
+        if (ArrayUtils.contains(userIds, getUserId())) {
+            return error("当前用户不能删除");
+        }
+        return toAjax(userService.deleteUserByIds(userIds));
+    }
+
+    /**
+     * 重置密码
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/resetPwd")
+    public AjaxResult resetPwd(@RequestBody SysUser user) {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.resetPwd(user));
+    }
+
+    /**
+     * 状态修改
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody SysUser user) {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.updateUserStatus(user));
+    }
+
+    /**
+     * 根据用户编号获取授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping("/authRole/{userId}")
+    public AjaxResult authRole(@PathVariable("userId") Long userId) {
+        AjaxResult ajax = AjaxResult.success();
+        SysUser user = userService.selectUserById(userId);
+        List<SysRole> roles = roleService.selectRolesByUserId(userId);
+        ajax.put("user", user);
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+        return ajax;
+    }
+
+    /**
+     * 用户授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authRole")
+    public AjaxResult insertAuthRole(Long userId, Long[] roleIds) {
+        userService.checkUserDataScope(userId);
+        roleService.checkRoleDataScope(roleIds);
+        userService.insertUserAuth(userId, roleIds);
+        return success();
+    }
+
+    /**
+     * 获取部门树列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/deptTree")
+    public AjaxResult deptTree(SysDept dept) {
+        return success(deptService.selectDeptTreeList(dept));
+    }
+}

+ 154 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/controller/tool/TestCtrl.java

@@ -0,0 +1,154 @@
+package com.lij.himeeter.web.controller.tool;
+
+import com.lij.himeeter.common.core.controller.BaseController;
+import com.lij.himeeter.common.core.domain.R;
+import com.lij.himeeter.common.utils.StringUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  TestCtrl
+ * 创建时间 : 2025年01月06日 04:41
+ *
+ * 文件描述 : swagger 用户测试方法
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Tag(name = "用户信息管理")
+@RestController
+@RequestMapping("/test/user")
+public class TestCtrl extends BaseController {
+
+    private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
+
+    {
+        users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
+        users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
+    }
+
+    @Operation(summary = "获取用户列表")
+    @GetMapping("/list")
+    public R<List<UserEntity>> userList() {
+        List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
+        return R.ok(userList);
+    }
+
+    @Operation(summary = "获取用户详细")
+    @GetMapping("/{userId}")
+    public R<UserEntity> getUser(@PathVariable(name = "userId")
+                                 Integer userId) {
+        if (!users.isEmpty() && users.containsKey(userId)) {
+            return R.ok(users.get(userId));
+        } else {
+            return R.fail("用户不存在");
+        }
+    }
+
+    @Operation(summary = "新增用户")
+    @PostMapping("/save")
+    public R<String> save(UserEntity user) {
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) {
+            return R.fail("用户ID不能为空");
+        }
+        users.put(user.getUserId(), user);
+        return R.ok();
+    }
+
+    @Operation(summary = "更新用户")
+    @PutMapping("/update")
+    public R<String> update(@RequestBody
+                            UserEntity user) {
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) {
+            return R.fail("用户ID不能为空");
+        }
+        if (users.isEmpty() || !users.containsKey(user.getUserId())) {
+            return R.fail("用户不存在");
+        }
+        users.remove(user.getUserId());
+        users.put(user.getUserId(), user);
+        return R.ok();
+    }
+
+    @Operation(summary = "删除用户信息")
+    @DeleteMapping("/{userId}")
+    public R<String> delete(@PathVariable(name = "userId")
+                            Integer userId) {
+        if (!users.isEmpty() && users.containsKey(userId)) {
+            users.remove(userId);
+            return R.ok();
+        } else {
+            return R.fail("用户不存在");
+        }
+    }
+}
+
+@Schema(description = "用户实体")
+class UserEntity {
+    @Schema(title = "用户ID")
+    private Integer userId;
+
+    @Schema(title = "用户名称")
+    private String username;
+
+    @Schema(title = "用户密码")
+    private String password;
+
+    @Schema(title = "用户手机")
+    private String mobile;
+
+    public UserEntity() {
+
+    }
+
+    public UserEntity(Integer userId, String username, String password, String mobile) {
+        this.userId = userId;
+        this.username = username;
+        this.password = password;
+        this.mobile = mobile;
+    }
+
+    public Integer getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Integer userId) {
+        this.userId = userId;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getMobile() {
+        return mobile;
+    }
+
+    public void setMobile(String mobile) {
+        this.mobile = mobile;
+    }
+}

+ 72 - 0
himeeter-admin/src/main/java/com/lij/himeeter/web/core/config/SwaggerConfig.java

@@ -0,0 +1,72 @@
+package com.lij.himeeter.web.core.config;
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SwaggerConfig
+ * 创建时间 : 2025年01月06日 04:08
+ *
+ * 文件描述 : Swagger2的接口配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class SwaggerConfig {
+
+    /**
+     * 系统基础配置
+     */
+    @Autowired
+    private HimeeterConfig himeeterConfig;
+
+    /**
+     * 自定义的 OpenAPI 对象
+     */
+    @Bean
+    public OpenAPI customOpenApi() {
+        return new OpenAPI().components(new Components()
+                        // 设置认证的请求头
+                        .addSecuritySchemes("apikey", securityScheme()))
+                .addSecurityItem(new SecurityRequirement().addList("apikey"))
+                .info(getApiInfo());
+    }
+
+    @Bean
+    public SecurityScheme securityScheme() {
+        return new SecurityScheme()
+                .type(SecurityScheme.Type.APIKEY)
+                .name("Authorization")
+                .in(SecurityScheme.In.HEADER)
+                .scheme("Bearer");
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    public Info getApiInfo() {
+        return new Info()
+                // 设置标题
+                .title("标题:Himeeter管理系统_接口文档")
+                // 描述
+                .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
+                // 作者信息
+                .contact(new Contact().name(himeeterConfig.getName()))
+                // 版本
+                .version("版本号:" + himeeterConfig.getVersion());
+    }
+}

+ 1 - 0
himeeter-admin/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson2.*.jar

+ 61 - 0
himeeter-admin/src/main/resources/application-druid.yml

@@ -0,0 +1,61 @@
+# 数据源配置
+spring:
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://192.168.31.129:3306/d_dandelion?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: root
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url: 
+                username: 
+                password: 
+            # 初始连接数
+            initialSize: 5
+            # 最小连接池数量
+            minIdle: 10
+            # 最大连接池数量
+            maxActive: 20
+            # 配置获取连接等待超时的时间
+            maxWait: 60000
+            # 配置连接超时时间
+            connectTimeout: 30000
+            # 配置网络超时时间
+            socketTimeout: 60000
+            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+            timeBetweenEvictionRunsMillis: 60000
+            # 配置一个连接在池中最小生存的时间,单位是毫秒
+            minEvictableIdleTimeMillis: 300000
+            # 配置一个连接在池中最大生存的时间,单位是毫秒
+            maxEvictableIdleTimeMillis: 900000
+            # 配置检测连接是否有效
+            validationQuery: SELECT 1 FROM DUAL
+            testWhileIdle: true
+            testOnBorrow: false
+            testOnReturn: false
+            webStatFilter: 
+                enabled: true
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username: himeeter
+                login-password: 123456
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true

+ 0 - 1
himeeter-admin/src/main/resources/application.properties

@@ -1 +0,0 @@
-spring.application.name=himeeter-admin

+ 138 - 0
himeeter-admin/src/main/resources/application.yml

@@ -0,0 +1,138 @@
+# \u9879\u76EE\u76F8\u5173\u914D\u7F6E
+himeeter:
+  # \u540D\u79F0
+  name: Himeeter
+  # \u7248\u672C
+  version: 1.0.0
+  # \u7248\u6743\u5E74\u4EFD
+  copyrightYear: 2024
+  # \u6587\u4EF6\u8DEF\u5F84 \u793A\u4F8B\uFF08 Windows\u914D\u7F6ED:/himeeter/uploadPath\uFF0CLinux\u914D\u7F6E /home/himeeter/uploadPath\uFF09
+  profile: D:/himeeter/uploadPath
+  # \u83B7\u53D6ip\u5730\u5740\u5F00\u5173
+  addressEnabled: true
+  # \u9A8C\u8BC1\u7801\u7C7B\u578B math \u6570\u5B57\u8BA1\u7B97 char \u5B57\u7B26\u9A8C\u8BC1
+  captchaType: math
+
+# \u5F00\u53D1\u73AF\u5883\u914D\u7F6E
+server:
+  # \u670D\u52A1\u5668\u7684HTTP\u7AEF\u53E3\uFF0C\u9ED8\u8BA4\u4E3A8080
+  port: 8080
+  servlet:
+    # \u5E94\u7528\u7684\u8BBF\u95EE\u8DEF\u5F84
+    context-path: /
+  tomcat:
+    # tomcat\u7684URI\u7F16\u7801
+    uri-encoding: UTF-8
+    # \u8FDE\u63A5\u6570\u6EE1\u540E\u7684\u6392\u961F\u6570\uFF0C\u9ED8\u8BA4\u4E3A100
+    accept-count: 1000
+    threads:
+      # tomcat\u6700\u5927\u7EBF\u7A0B\u6570\uFF0C\u9ED8\u8BA4\u4E3A200
+      max: 800
+      # Tomcat\u542F\u52A8\u521D\u59CB\u5316\u7684\u7EBF\u7A0B\u6570\uFF0C\u9ED8\u8BA4\u503C10
+      min-spare: 100
+
+# \u65E5\u5FD7\u914D\u7F6E
+logging:
+  level:
+    com.lij.himeeter: debug
+    org.springframework: warn
+
+
+# \u7528\u6237\u914D\u7F6E
+user:
+  password:
+    # \u5BC6\u7801\u6700\u5927\u9519\u8BEF\u6B21\u6570
+    maxRetryCount: 5
+    # \u5BC6\u7801\u9501\u5B9A\u65F6\u95F4\uFF08\u9ED8\u8BA410\u5206\u949F\uFF09
+    lockTime: 10
+
+# Spring\u914D\u7F6E
+spring:
+  # \u8D44\u6E90\u4FE1\u606F
+  messages:
+    # \u56FD\u9645\u5316\u8D44\u6E90\u6587\u4EF6\u8DEF\u5F84
+    basename: i18n/messages
+  profiles:
+    active: druid
+  # \u6587\u4EF6\u4E0A\u4F20
+  servlet:
+    multipart:
+      # \u5355\u4E2A\u6587\u4EF6\u5927\u5C0F
+      max-file-size: 10MB
+      # \u8BBE\u7F6E\u603B\u4E0A\u4F20\u7684\u6587\u4EF6\u5927\u5C0F
+      max-request-size: 20MB
+  # \u670D\u52A1\u6A21\u5757
+  devtools:
+    restart:
+      # \u70ED\u90E8\u7F72\u5F00\u5173
+      enabled: true
+  data:
+    # redis \u914D\u7F6E
+    redis:
+      # \u5730\u5740
+      host: 192.168.31.129
+      # \u7AEF\u53E3\uFF0C\u9ED8\u8BA4\u4E3A6379
+      port: 6379
+      # \u6570\u636E\u5E93\u7D22\u5F15
+      database: 0
+      # \u5BC6\u7801
+      password: redis
+      # \u8FDE\u63A5\u8D85\u65F6\u65F6\u95F4
+      timeout: 10s
+      lettuce:
+        pool:
+          # \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5C0F\u7A7A\u95F2\u8FDE\u63A5
+          min-idle: 0
+          # \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5927\u7A7A\u95F2\u8FDE\u63A5
+          max-idle: 8
+          # \u8FDE\u63A5\u6C60\u7684\u6700\u5927\u6570\u636E\u5E93\u8FDE\u63A5\u6570
+          max-active: 8
+          # #\u8FDE\u63A5\u6C60\u6700\u5927\u963B\u585E\u7B49\u5F85\u65F6\u95F4\uFF08\u4F7F\u7528\u8D1F\u503C\u8868\u793A\u6CA1\u6709\u9650\u5236\uFF09
+          max-wait: -1ms
+
+# token\u914D\u7F6E
+token:
+  # \u4EE4\u724C\u81EA\u5B9A\u4E49\u6807\u8BC6
+  header: Authorization
+  # \u4EE4\u724C\u5BC6\u94A5
+  secret: abcdefghijklmnopqrstuvwxyz
+  # \u4EE4\u724C\u6709\u6548\u671F\uFF08\u9ED8\u8BA430\u5206\u949F\uFF09
+  expireTime: 30
+
+# MyBatis\u914D\u7F6E
+mybatis:
+  # \u641C\u7D22\u6307\u5B9A\u5305\u522B\u540D
+  typeAliasesPackage: com.lij.himeeter.**.domain
+  # \u914D\u7F6Emapper\u7684\u626B\u63CF\uFF0C\u627E\u5230\u6240\u6709\u7684mapper.xml\u6620\u5C04\u6587\u4EF6
+  mapperLocations: classpath*:mapper/**/*Mapper.xml
+  # \u52A0\u8F7D\u5168\u5C40\u7684\u914D\u7F6E\u6587\u4EF6
+  configLocation: classpath:mybatis/mybatis-config.xml
+
+# PageHelper\u5206\u9875\u63D2\u4EF6
+pagehelper:
+  helperDialect: mysql
+  supportMethodsArguments: true
+  params: count=countSql
+
+# Springdoc\u914D\u7F6E
+springdoc:
+  api-docs:
+    path: /v3/api-docs
+  swagger-ui:
+    enabled: true
+    path: /swagger-ui.html
+    tags-sorter: alpha
+  group-configs:
+    - group: 'default'
+      display-name: '\u6D4B\u8BD5\u6A21\u5757'
+      paths-to-match: '/**'
+      packages-to-scan: com.lij.himeeter.web.controller.tool
+
+# \u9632\u6B62XSS\u653B\u51FB
+xss:
+  # \u8FC7\u6EE4\u5F00\u5173
+  enabled: true
+  # \u6392\u9664\u94FE\u63A5\uFF08\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF09
+  excludes: /system/notice
+  # \u5339\u914D\u94FE\u63A5
+  urlPatterns: /system/*,/monitor/*,/tool/*

+ 10 - 0
himeeter-admin/src/main/resources/banner.txt

@@ -0,0 +1,10 @@
+
+██╗  ██╗██╗███╗   ███╗███████╗███████╗████████╗███████╗██████╗
+██║  ██║██║████╗ ████║██╔════╝██╔════╝╚══██╔══╝██╔════╝██╔══██╗
+███████║██║██╔████╔██║█████╗  █████╗     ██║   █████╗  ██████╔╝
+██╔══██║██║██║╚██╔╝██║██╔══╝  ██╔══╝     ██║   ██╔══╝  ██╔══██╗
+██║  ██║██║██║ ╚═╝ ██║███████╗███████╗   ██║   ███████╗██║  ██║
+╚═╝  ╚═╝╚═╝╚═╝     ╚═╝╚══════╝╚══════╝   ╚═╝   ╚══════╝╚═╝  ╚═╝
+
+Application Version: ${himeeter.version}
+Spring Boot Version: ${spring-boot.version}

+ 38 - 0
himeeter-admin/src/main/resources/i18n/messages.properties

@@ -0,0 +1,38 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+login.blocked=很遗憾,访问IP已被列入系统黑名单
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 93 - 0
himeeter-admin/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/home/himeeter/logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+	
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+	
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+	
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+	
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.himeeter" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+	
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+	
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration> 

+ 20 - 0
himeeter-admin/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+    <!-- 全局参数 -->
+    <settings>
+        <!-- 使全局的映射器启用或禁用缓存 -->
+        <setting name="cacheEnabled"             value="true"   />
+        <!-- 允许JDBC 支持自动生成主键 -->
+        <setting name="useGeneratedKeys"         value="true"   />
+        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
+        <setting name="defaultExecutorType"      value="SIMPLE" />
+		<!-- 指定 MyBatis 所用日志的具体实现 -->
+        <setting name="logImpl"                  value="SLF4J"  />
+        <!-- 使用驼峰命名法转换字段 -->
+		<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
+	</settings>
+	
+</configuration>

+ 66 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/config/serializer/SensitiveJsonSerializer.java

@@ -0,0 +1,66 @@
+package com.lij.himeeter.common.config.serializer;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import com.lij.himeeter.common.annotation.Sensitive;
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.enums.DesensitizedType;
+import com.lij.himeeter.common.utils.SecurityUtils;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SensitiveJsonSerializer
+ * 创建时间 : 2025年01月02日 15:32
+ *
+ * 文件描述 : 数据脱敏序列化过滤
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
+    private DesensitizedType desensitizedType;
+
+    @Override
+    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+        if (desensitization()) {
+            gen.writeString(desensitizedType.desensitizer().apply(value));
+        } else {
+            gen.writeString(value);
+        }
+    }
+
+    @Override
+    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
+            throws JsonMappingException {
+        Sensitive annotation = property.getAnnotation(Sensitive.class);
+        if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
+            this.desensitizedType = annotation.desensitizedType();
+            return this;
+        }
+        return prov.findValueSerializer(property.getType(), property);
+    }
+
+    /**
+     * 是否需要脱敏处理
+     */
+    private boolean desensitization() {
+        try {
+            LoginUser securityUser = SecurityUtils.getLoginUser();
+            // 管理员不脱敏
+            return !securityUser.getUser().isAdmin();
+        } catch (Exception e) {
+            return true;
+        }
+    }
+}

+ 196 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/constant/GenConstants.java

@@ -0,0 +1,196 @@
+package com.lij.himeeter.common.constant;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  GenConstants
+ * 创建时间 : 2025年01月02日 15:39
+ *
+ * 文件描述 : 代码生成通用常量
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface GenConstants {
+
+    /**
+     * 单表(增删改查)
+     */
+    String TPL_CRUD = "crud";
+
+    /**
+     * 树表(增删改查)
+     */
+    String TPL_TREE = "tree";
+
+    /**
+     * 主子表(增删改查)
+     */
+    String TPL_SUB = "sub";
+
+    /**
+     * 树编码字段
+     */
+    String TREE_CODE = "treeCode";
+
+    /**
+     * 树父编码字段
+     */
+    String TREE_PARENT_CODE = "treeParentCode";
+
+    /**
+     * 树名称字段
+     */
+    String TREE_NAME = "treeName";
+
+    /**
+     * 上级菜单ID字段
+     */
+    String PARENT_MENU_ID = "parentMenuId";
+
+    /**
+     * 上级菜单名称字段
+     */
+    String PARENT_MENU_NAME = "parentMenuName";
+
+    /**
+     * 数据库字符串类型
+     */
+    String[] COLUMNTYPE_STR = {"char", "varchar", "nvarchar", "varchar2"};
+
+    /**
+     * 数据库文本类型
+     */
+    String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext"};
+
+    /**
+     * 数据库时间类型
+     */
+    String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp"};
+
+    /**
+     * 数据库数字类型
+     */
+    String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "number", "integer",
+            "bit", "bigint", "float", "double", "decimal"};
+
+    /**
+     * 页面不需要编辑字段
+     */
+    String[] COLUMNNAME_NOT_EDIT = {"id", "create_by", "create_time", "del_flag"};
+
+    /**
+     * 页面不需要显示的列表字段
+     */
+    String[] COLUMNNAME_NOT_LIST = {"id", "create_by", "create_time", "del_flag", "update_by",
+            "update_time"};
+
+    /**
+     * 页面不需要查询字段
+     */
+    String[] COLUMNNAME_NOT_QUERY = {"id", "create_by", "create_time", "del_flag", "update_by",
+            "update_time", "remark"};
+
+    /**
+     * Entity基类字段
+     */
+    String[] BASE_ENTITY = {"createBy", "createTime", "updateBy", "updateTime", "remark"};
+
+    /**
+     * Tree基类字段
+     */
+    String[] TREE_ENTITY = {"parentName", "parentId", "orderNum", "ancestors", "children"};
+
+    /**
+     * 文本框
+     */
+    String HTML_INPUT = "input";
+
+    /**
+     * 文本域
+     */
+    String HTML_TEXTAREA = "textarea";
+
+    /**
+     * 下拉框
+     */
+    String HTML_SELECT = "select";
+
+    /**
+     * 单选框
+     */
+    String HTML_RADIO = "radio";
+
+    /**
+     * 复选框
+     */
+    String HTML_CHECKBOX = "checkbox";
+
+    /**
+     * 日期控件
+     */
+    String HTML_DATETIME = "datetime";
+
+    /**
+     * 图片上传控件
+     */
+    String HTML_IMAGE_UPLOAD = "imageUpload";
+
+    /**
+     * 文件上传控件
+     */
+    String HTML_FILE_UPLOAD = "fileUpload";
+
+    /**
+     * 富文本控件
+     */
+    String HTML_EDITOR = "editor";
+
+    /**
+     * 字符串类型
+     */
+    String TYPE_STRING = "String";
+
+    /**
+     * 整型
+     */
+    String TYPE_INTEGER = "Integer";
+
+    /**
+     * 长整型
+     */
+    String TYPE_LONG = "Long";
+
+    /**
+     * 浮点型
+     */
+    String TYPE_DOUBLE = "Double";
+
+    /**
+     * 高精度计算类型
+     */
+    String TYPE_BIGDECIMAL = "BigDecimal";
+
+    /**
+     * 时间类型
+     */
+    String TYPE_DATE = "Date";
+
+    /**
+     * 模糊查询
+     */
+    String QUERY_LIKE = "LIKE";
+
+    /**
+     * 相等查询
+     */
+    String QUERY_EQ = "EQ";
+
+    /**
+     * 需要
+     */
+    String REQUIRE = "1";
+}

+ 66 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/constant/ScheduleConstants.java

@@ -0,0 +1,66 @@
+package com.lij.himeeter.common.constant;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ScheduleConstants
+ * 创建时间 : 2025年01月02日 15:40
+ *
+ * 文件描述 : 任务调度通用常量
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ScheduleConstants {
+
+    String TASK_CLASS_NAME = "TASK_CLASS_NAME";
+
+    /**
+     * 执行目标key
+     */
+    String TASK_PROPERTIES = "TASK_PROPERTIES";
+
+    /**
+     * 默认
+     */
+    String MISFIRE_DEFAULT = "0";
+
+    /**
+     * 立即触发执行
+     */
+    String MISFIRE_IGNORE_MISFIRES = "1";
+
+    /**
+     * 触发一次执行
+     */
+    String MISFIRE_FIRE_AND_PROCEED = "2";
+
+    /**
+     * 不触发立即执行
+     */
+    String MISFIRE_DO_NOTHING = "3";
+
+    enum Status {
+        /**
+         * 正常
+         */
+        NORMAL("0"),
+        /**
+         * 暂停
+         */
+        PAUSE("1");
+
+        private String value;
+
+        private Status(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+}

+ 193 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/core/controller/BaseController.java

@@ -0,0 +1,193 @@
+package com.lij.himeeter.common.core.controller;
+
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.lij.himeeter.common.constant.HttpStatus;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.core.page.PageDomain;
+import com.lij.himeeter.common.core.page.TableDataInfo;
+import com.lij.himeeter.common.core.page.TableSupport;
+import com.lij.himeeter.common.utils.DateUtils;
+import com.lij.himeeter.common.utils.PageUtils;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.sql.SqlUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.annotation.InitBinder;
+
+import java.beans.PropertyEditorSupport;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  BaseController
+ * 创建时间 : 2025年01月02日 15:41
+ *
+ * 文件描述 : web层通用数据处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class BaseController {
+
+    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /**
+     * 将前台传递过来的日期格式的字符串,自动转化为Date类型
+     */
+    @InitBinder
+    public void initBinder(WebDataBinder binder) {
+        // Date 类型转换
+        binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
+            @Override
+            public void setAsText(String text) {
+                setValue(DateUtils.parseDate(text));
+            }
+        });
+    }
+
+    /**
+     * 设置请求分页数据
+     */
+    protected void startPage() {
+        PageUtils.startPage();
+    }
+
+    /**
+     * 设置请求排序数据
+     */
+    protected void startOrderBy() {
+        PageDomain pageDomain = TableSupport.buildPageRequest();
+        if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) {
+            String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
+            PageHelper.orderBy(orderBy);
+        }
+    }
+
+    /**
+     * 清理分页的线程变量
+     */
+    protected void clearPage() {
+        PageUtils.clearPage();
+    }
+
+    /**
+     * 响应请求分页数据
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    protected TableDataInfo getDataTable(List<?> list) {
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(HttpStatus.SUCCESS);
+        rspData.setMsg("查询成功");
+        rspData.setRows(list);
+        rspData.setTotal(new PageInfo(list).getTotal());
+        return rspData;
+    }
+
+    /**
+     * 返回成功
+     */
+    public AjaxResult success() {
+        return AjaxResult.success();
+    }
+
+    /**
+     * 返回失败消息
+     */
+    public AjaxResult error() {
+        return AjaxResult.error();
+    }
+
+    /**
+     * 返回成功消息
+     */
+    public AjaxResult success(String message) {
+        return AjaxResult.success(message);
+    }
+
+    /**
+     * 返回成功消息
+     */
+    public AjaxResult success(Object data) {
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 返回失败消息
+     */
+    public AjaxResult error(String message) {
+        return AjaxResult.error(message);
+    }
+
+    /**
+     * 返回警告消息
+     */
+    public AjaxResult warn(String message) {
+        return AjaxResult.warn(message);
+    }
+
+    /**
+     * 响应返回结果
+     *
+     * @param rows 影响行数
+     * @return 操作结果
+     */
+    protected AjaxResult toAjax(int rows) {
+        return rows > 0 ? AjaxResult.success() : AjaxResult.error();
+    }
+
+    /**
+     * 响应返回结果
+     *
+     * @param result 结果
+     * @return 操作结果
+     */
+    protected AjaxResult toAjax(boolean result) {
+        return result ? success() : error();
+    }
+
+    /**
+     * 页面跳转
+     */
+    public String redirect(String url) {
+        return StringUtils.format("redirect:{}", url);
+    }
+
+    /**
+     * 获取用户缓存信息
+     */
+    public LoginUser getLoginUser() {
+        return SecurityUtils.getLoginUser();
+    }
+
+    /**
+     * 获取登录用户id
+     */
+    public Long getUserId() {
+        return getLoginUser().getUserId();
+    }
+
+    /**
+     * 获取登录部门id
+     */
+    public Long getDeptId() {
+        return getLoginUser().getDeptId();
+    }
+
+    /**
+     * 获取登录用户名
+     */
+    public String getUsername() {
+        return getLoginUser().getUsername();
+    }
+}
+

+ 102 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/core/page/PageDomain.java

@@ -0,0 +1,102 @@
+package com.lij.himeeter.common.core.page;
+
+import com.lij.himeeter.common.utils.StringUtils;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  PageDomain
+ * 创建时间 : 2025年01月02日 15:44
+ *
+ * 文件描述 : 分页数据
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class PageDomain {
+    /**
+     * 当前记录起始索引
+     */
+    private Integer pageNum;
+
+    /**
+     * 每页显示记录数
+     */
+    private Integer pageSize;
+
+    /**
+     * 排序列
+     */
+    private String orderByColumn;
+
+    /**
+     * 排序的方向desc或者asc
+     */
+    private String isAsc = "asc";
+
+    /**
+     * 分页参数合理化
+     */
+    private Boolean reasonable = true;
+
+    public String getOrderBy() {
+        if (StringUtils.isEmpty(orderByColumn)) {
+            return "";
+        }
+        return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc;
+    }
+
+    public Integer getPageNum() {
+        return pageNum;
+    }
+
+    public void setPageNum(Integer pageNum) {
+        this.pageNum = pageNum;
+    }
+
+    public Integer getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(Integer pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public String getOrderByColumn() {
+        return orderByColumn;
+    }
+
+    public void setOrderByColumn(String orderByColumn) {
+        this.orderByColumn = orderByColumn;
+    }
+
+    public String getIsAsc() {
+        return isAsc;
+    }
+
+    public void setIsAsc(String isAsc) {
+        if (StringUtils.isNotEmpty(isAsc)) {
+            // 兼容前端排序类型
+            if ("ascending".equals(isAsc)) {
+                isAsc = "asc";
+            } else if ("descending".equals(isAsc)) {
+                isAsc = "desc";
+            }
+            this.isAsc = isAsc;
+        }
+    }
+
+    public Boolean getReasonable() {
+        if (StringUtils.isNull(reasonable)) {
+            return Boolean.TRUE;
+        }
+        return reasonable;
+    }
+
+    public void setReasonable(Boolean reasonable) {
+        this.reasonable = reasonable;
+    }
+}

+ 91 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/core/page/TableDataInfo.java

@@ -0,0 +1,91 @@
+package com.lij.himeeter.common.core.page;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  TableDataInfo
+ * 创建时间 : 2025年01月02日 15:47
+ *
+ * 文件描述 : 表格分页数据对象
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class TableDataInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 总记录数
+     */
+    private long total;
+
+    /**
+     * 列表数据
+     */
+    private List<?> rows;
+
+    /**
+     * 消息状态码
+     */
+    private int code;
+
+    /**
+     * 消息内容
+     */
+    private String msg;
+
+    /**
+     * 表格数据对象
+     */
+    public TableDataInfo() {
+    }
+
+    /**
+     * 分页
+     *
+     * @param list  列表数据
+     * @param total 总记录数
+     */
+    public TableDataInfo(List<?> list, int total) {
+        this.rows = list;
+        this.total = total;
+    }
+
+    public long getTotal() {
+        return total;
+    }
+
+    public void setTotal(long total) {
+        this.total = total;
+    }
+
+    public List<?> getRows() {
+        return rows;
+    }
+
+    public void setRows(List<?> rows) {
+        this.rows = rows;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+}

+ 62 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/core/page/TableSupport.java

@@ -0,0 +1,62 @@
+package com.lij.himeeter.common.core.page;
+
+import com.lij.himeeter.common.core.text.Convert;
+import com.lij.himeeter.common.utils.ServletUtils;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  TableSupport
+ * 创建时间 : 2025年01月02日 15:47
+ *
+ * 文件描述 : 表格数据处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class TableSupport {
+    /**
+     * 当前记录起始索引
+     */
+    public static final String PAGE_NUM = "pageNum";
+
+    /**
+     * 每页显示记录数
+     */
+    public static final String PAGE_SIZE = "pageSize";
+
+    /**
+     * 排序列
+     */
+    public static final String ORDER_BY_COLUMN = "orderByColumn";
+
+    /**
+     * 排序的方向 "desc" 或者 "asc".
+     */
+    public static final String IS_ASC = "isAsc";
+
+    /**
+     * 分页参数合理化
+     */
+    public static final String REASONABLE = "reasonable";
+
+    /**
+     * 封装分页对象
+     */
+    public static PageDomain getPageDomain() {
+        PageDomain pageDomain = new PageDomain();
+        pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));
+        pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));
+        pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
+        pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
+        pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
+        return pageDomain;
+    }
+
+    public static PageDomain buildPageRequest() {
+        return getPageDomain();
+    }
+}

+ 1 - 1
himeeter-common/src/main/java/com/lij/himeeter/common/core/RedisCache.java → himeeter-common/src/main/java/com/lij/himeeter/common/core/redis/RedisCache.java

@@ -1,4 +1,4 @@
-package com.lij.himeeter.common.core;
+package com.lij.himeeter.common.core.redis;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.BoundSetOperations;

+ 1 - 1
himeeter-common/src/main/java/com/lij/himeeter/common/core/text/CharsetKit.java

@@ -11,7 +11,7 @@ import java.nio.charset.StandardCharsets;
  * 文件名 :  CharsetKit
  * 创建时间 : 2024年12月26日 17:44
  *
- * 文件描述 : 
+ * 文件描述 : 字符集工具类
  *
  * </pre>
  *

+ 2 - 4
himeeter-common/src/main/java/com/lij/himeeter/common/enums/DesensitizedType.java

@@ -57,13 +57,11 @@ public enum DesensitizedType {
 
     private final Function<String, String> desensitizer;
 
-    DesensitizedType(Function<String, String> desensitizer)
-    {
+    DesensitizedType(Function<String, String> desensitizer) {
         this.desensitizer = desensitizer;
     }
 
-    public Function<String, String> desensitizer()
-    {
+    public Function<String, String> desensitizer() {
         return desensitizer;
     }
 }

+ 42 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/enums/HttpMethod.java

@@ -0,0 +1,42 @@
+package com.lij.himeeter.common.enums;
+
+import org.springframework.lang.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  HttpMethod
+ * 创建时间 : 2025年01月02日 15:59
+ *
+ * 文件描述 : 请求方式
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public enum HttpMethod {
+
+    GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
+
+    private static final Map<String, HttpMethod> mappings = new HashMap<>(16);
+
+    static {
+        for (HttpMethod httpMethod : values()) {
+            mappings.put(httpMethod.name(), httpMethod);
+        }
+    }
+
+    @Nullable
+    public static HttpMethod resolve(@Nullable String method) {
+        return (method != null ? mappings.get(method) : null);
+    }
+
+    public boolean matches(String method) {
+        return (this == resolve(method));
+    }
+}

+ 38 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/enums/UserStatus.java

@@ -0,0 +1,38 @@
+package com.lij.himeeter.common.enums;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  UserStatus
+ * 创建时间 : 2025年01月03日 16:29
+ *
+ * 文件描述 : 用户状态
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public enum UserStatus {
+
+    OK("0", "正常"),
+    DISABLE("1", "停用"),
+    DELETED("2", "删除");
+
+    private final String code;
+    private final String info;
+
+    UserStatus(String code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getInfo() {
+        return info;
+    }
+}

+ 23 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/DemoModeException.java

@@ -0,0 +1,23 @@
+package com.lij.himeeter.common.exception;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  DemoModeException
+ * 创建时间 : 2025年01月03日 18:48
+ *
+ * 文件描述 : 演示模式异常
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class DemoModeException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public DemoModeException() {
+    }
+}

+ 60 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/GlobalException.java

@@ -0,0 +1,60 @@
+package com.lij.himeeter.common.exception;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  GlobalException
+ * 创建时间 : 2025年01月03日 18:49
+ *
+ * 文件描述 : 全局异常
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class GlobalException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 错误明细,内部调试错误
+     * <p>
+     * 和 {@link CommonResult#getDetailMessage()} 一致的设计
+     */
+    private String detailMessage;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public GlobalException() {
+    }
+
+    public GlobalException(String message) {
+        this.message = message;
+    }
+
+    public String getDetailMessage() {
+        return detailMessage;
+    }
+
+    public GlobalException setDetailMessage(String detailMessage) {
+        this.detailMessage = detailMessage;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public GlobalException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+}

+ 31 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/UtilException.java

@@ -0,0 +1,31 @@
+package com.lij.himeeter.common.exception;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  UtilException
+ * 创建时间 : 2025年01月02日 15:01
+ *
+ * 文件描述 : 工具类异常
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class UtilException extends RuntimeException {
+    private static final long serialVersionUID = 8247610319171014183L;
+
+    public UtilException(Throwable e) {
+        super(e.getMessage(), e);
+    }
+
+    public UtilException(String message) {
+        super(message);
+    }
+
+    public UtilException(String message, Throwable throwable) {
+        super(message, throwable);
+    }
+}

+ 94 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/base/BaseException.java

@@ -0,0 +1,94 @@
+package com.lij.himeeter.common.exception.base;
+
+import com.lij.himeeter.common.utils.MessageUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  BaseException
+ * 创建时间 : 2025年01月03日 16:31
+ *
+ * 文件描述 : 基础异常
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class BaseException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 所属模块
+     */
+    private String module;
+
+    /**
+     * 错误码
+     */
+    private String code;
+
+    /**
+     * 错误码对应的参数
+     */
+    private Object[] args;
+
+    /**
+     * 错误消息
+     */
+    private String defaultMessage;
+
+    public BaseException(String module, String code, Object[] args, String defaultMessage) {
+        this.module = module;
+        this.code = code;
+        this.args = args;
+        this.defaultMessage = defaultMessage;
+    }
+
+    public BaseException(String module, String code, Object[] args) {
+        this(module, code, args, null);
+    }
+
+    public BaseException(String module, String defaultMessage) {
+        this(module, null, null, defaultMessage);
+    }
+
+    public BaseException(String code, Object[] args) {
+        this(null, code, args, null);
+    }
+
+    public BaseException(String defaultMessage) {
+        this(null, null, null, defaultMessage);
+    }
+
+    @Override
+    public String getMessage() {
+        String message = null;
+        if (!StringUtils.isEmpty(code)) {
+            message = MessageUtils.message(code, args);
+        }
+        if (message == null) {
+            message = defaultMessage;
+        }
+        return message;
+    }
+
+    public String getModule() {
+        return module;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public Object[] getArgs() {
+        return args;
+    }
+
+    public String getDefaultMessage() {
+        return defaultMessage;
+    }
+}

+ 26 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileException.java

@@ -0,0 +1,26 @@
+package com.lij.himeeter.common.exception.file;
+
+import com.lij.himeeter.common.exception.base.BaseException;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FileException
+ * 创建时间 : 2025年01月03日 16:32
+ *
+ * 文件描述 : 文件信息异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class FileException extends BaseException {
+    private static final long serialVersionUID = 1L;
+
+    public FileException(String code, Object[] args) {
+        super("file", code, args, null);
+    }
+
+}

+ 24 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileNameLengthLimitExceededException.java

@@ -0,0 +1,24 @@
+package com.lij.himeeter.common.exception.file;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FileNameLengthLimitExceededException
+ * 创建时间 : 2025年01月03日 16:33
+ *
+ * 文件描述 : 文件名称超长限制异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class FileNameLengthLimitExceededException extends FileException {
+
+    private static final long serialVersionUID = 1L;
+
+    public FileNameLengthLimitExceededException(int defaultFileNameLength) {
+        super("upload.filename.exceed.length", new Object[]{defaultFileNameLength});
+    }
+}

+ 24 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileSizeLimitExceededException.java

@@ -0,0 +1,24 @@
+package com.lij.himeeter.common.exception.file;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FileSizeLimitExceededException
+ * 创建时间 : 2025年01月03日 16:34
+ *
+ * 文件描述 : 文件名大小限制异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class FileSizeLimitExceededException extends FileException {
+
+    private static final long serialVersionUID = 1L;
+
+    public FileSizeLimitExceededException(long defaultMaxSize) {
+        super("upload.exceed.maxSize", new Object[]{defaultMaxSize});
+    }
+}

+ 62 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/FileUploadException.java

@@ -0,0 +1,62 @@
+package com.lij.himeeter.common.exception.file;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FileUploadException
+ * 创建时间 : 2025年01月03日 18:38
+ *
+ * 文件描述 : 文件上传异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+
+public class FileUploadException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    private final Throwable cause;
+
+    public FileUploadException() {
+        this(null, null);
+    }
+
+    public FileUploadException(final String msg) {
+        this(msg, null);
+    }
+
+    public FileUploadException(String msg, Throwable cause) {
+        super(msg);
+        this.cause = cause;
+    }
+
+    @Override
+    public void printStackTrace(PrintStream stream) {
+        super.printStackTrace(stream);
+        if (cause != null) {
+            stream.println("Caused by:");
+            cause.printStackTrace(stream);
+        }
+    }
+
+    @Override
+    public void printStackTrace(PrintWriter writer) {
+        super.printStackTrace(writer);
+        if (cause != null) {
+            writer.println("Caused by:");
+            cause.printStackTrace(writer);
+        }
+    }
+
+    @Override
+    public Throwable getCause() {
+        return cause;
+    }
+}
+

+ 77 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/file/InvalidExtensionException.java

@@ -0,0 +1,77 @@
+package com.lij.himeeter.common.exception.file;
+
+import java.util.Arrays;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  InvalidExtensionException
+ * 创建时间 : 2025年01月03日 18:39
+ *
+ * 文件描述 : 文件上传 误异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class InvalidExtensionException extends FileUploadException {
+
+    private static final long serialVersionUID = 1L;
+
+    private String[] allowedExtension;
+    private String extension;
+    private String filename;
+
+    public InvalidExtensionException(String[] allowedExtension, String extension, String filename) {
+        super("文件[" + filename + "]后缀[" + extension + "]不正确,请上传" + Arrays.toString(allowedExtension) + "格式");
+        this.allowedExtension = allowedExtension;
+        this.extension = extension;
+        this.filename = filename;
+    }
+
+    public String[] getAllowedExtension() {
+        return allowedExtension;
+    }
+
+    public String getExtension() {
+        return extension;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+
+    public static class InvalidImageExtensionException extends InvalidExtensionException {
+        private static final long serialVersionUID = 1L;
+
+        public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) {
+            super(allowedExtension, extension, filename);
+        }
+    }
+
+    public static class InvalidFlashExtensionException extends InvalidExtensionException {
+        private static final long serialVersionUID = 1L;
+
+        public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) {
+            super(allowedExtension, extension, filename);
+        }
+    }
+
+    public static class InvalidMediaExtensionException extends InvalidExtensionException {
+        private static final long serialVersionUID = 1L;
+
+        public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) {
+            super(allowedExtension, extension, filename);
+        }
+    }
+
+    public static class InvalidVideoExtensionException extends InvalidExtensionException {
+        private static final long serialVersionUID = 1L;
+
+        public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) {
+            super(allowedExtension, extension, filename);
+        }
+    }
+}

+ 39 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/job/TaskException.java

@@ -0,0 +1,39 @@
+package com.lij.himeeter.common.exception.job;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  TaskException
+ * 创建时间 : 2025年01月03日 18:40
+ *
+ * 文件描述 : 计划策略异常
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class TaskException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    private Code code;
+
+    public TaskException(String msg, Code code) {
+        this(msg, code, null);
+    }
+
+    public TaskException(String msg, Code code, Exception nestedEx) {
+        super(msg, nestedEx);
+        this.code = code;
+    }
+
+    public Code getCode() {
+        return code;
+    }
+
+    public enum Code {
+        TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE
+    }
+}

+ 24 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/BlackListException.java

@@ -0,0 +1,24 @@
+package com.lij.himeeter.common.exception.user;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  BlackListException
+ * 创建时间 : 2025年01月03日 18:41
+ *
+ * 文件描述 : 黑名单IP异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class BlackListException extends UserException {
+
+    private static final long serialVersionUID = 1L;
+
+    public BlackListException() {
+        super("login.blocked", null);
+    }
+}

+ 23 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/CaptchaException.java

@@ -0,0 +1,23 @@
+package com.lij.himeeter.common.exception.user;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  CaptchaException
+ * 创建时间 : 2025年01月03日 18:44
+ *
+ * 文件描述 : 验证码错误异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class CaptchaException extends UserException {
+    private static final long serialVersionUID = 1L;
+
+    public CaptchaException() {
+        super("user.jcaptcha.error", null);
+    }
+}

+ 23 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/CaptchaExpireException.java

@@ -0,0 +1,23 @@
+package com.lij.himeeter.common.exception.user;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  CaptchaExpireException
+ * 创建时间 : 2025年01月03日 18:44
+ *
+ * 文件描述 : 验证码失效异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class CaptchaExpireException extends UserException {
+    private static final long serialVersionUID = 1L;
+
+    public CaptchaExpireException() {
+        super("user.jcaptcha.expire", null);
+    }
+}

+ 26 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserException.java

@@ -0,0 +1,26 @@
+package com.lij.himeeter.common.exception.user;
+
+import com.lij.himeeter.common.exception.base.BaseException;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  UserException
+ * 创建时间 : 2025年01月03日 18:42
+ *
+ * 文件描述 : 用户信息异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class UserException extends BaseException {
+
+    private static final long serialVersionUID = 1L;
+
+    public UserException(String code, Object[] args) {
+        super("user", code, args, null);
+    }
+}

+ 23 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserNotExistsException.java

@@ -0,0 +1,23 @@
+package com.lij.himeeter.common.exception.user;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  UserNotExistsException
+ * 创建时间 : 2025年01月03日 18:45
+ *
+ * 文件描述 : 用户不存在异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class UserNotExistsException extends UserException {
+    private static final long serialVersionUID = 1L;
+
+    public UserNotExistsException() {
+        super("user.not.exists", null);
+    }
+}

+ 24 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserPasswordNotMatchException.java

@@ -0,0 +1,24 @@
+package com.lij.himeeter.common.exception.user;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  UserPasswordNotMatchException
+ * 创建时间 : 2025年01月03日 18:46
+ *
+ * 文件描述 : 用户密码不正确或不符合规范异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class UserPasswordNotMatchException extends UserException {
+
+    private static final long serialVersionUID = 1L;
+
+    public UserPasswordNotMatchException() {
+        super("user.password.not.match", null);
+    }
+}

+ 24 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/exception/user/UserPasswordRetryLimitExceedException.java

@@ -0,0 +1,24 @@
+package com.lij.himeeter.common.exception.user;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  UserPasswordRetryLimitExceedException
+ * 创建时间 : 2025年01月03日 18:47
+ *
+ * 文件描述 : 用户错误最大次数异常类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class UserPasswordRetryLimitExceedException extends UserException {
+
+    private static final long serialVersionUID = 1L;
+
+    public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) {
+        super("user.password.retry.limit.exceed", new Object[]{retryLimitCount, lockTime});
+    }
+}

+ 49 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/filter/RepeatableFilter.java

@@ -0,0 +1,49 @@
+package com.lij.himeeter.common.filter;
+
+import com.lij.himeeter.common.utils.StringUtils;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.MediaType;
+
+import java.io.IOException;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  RepeatableFilter
+ * 创建时间 : 2024年12月30日 19:05
+ *
+ * 文件描述 : Repeatable 过滤器
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class RepeatableFilter implements Filter {
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        ServletRequest requestWrapper = null;
+        if (request instanceof HttpServletRequest
+                && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
+            requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
+        }
+        if (null == requestWrapper) {
+            chain.doFilter(request, response);
+        } else {
+            chain.doFilter(requestWrapper, response);
+        }
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}

+ 77 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/filter/RepeatedlyRequestWrapper.java

@@ -0,0 +1,77 @@
+package com.lij.himeeter.common.filter;
+
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.utils.http.HttpHelper;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  RepeatedlyRequestWrapper
+ * 创建时间 : 2024年12月30日 19:06
+ *
+ * 文件描述 : 构建可重复读取inputStream的request
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
+
+    private final byte[] body;
+
+    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
+        super(request);
+        request.setCharacterEncoding(Constants.UTF8);
+        response.setCharacterEncoding(Constants.UTF8);
+
+        body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8);
+    }
+
+    @Override
+    public BufferedReader getReader() throws IOException {
+        return new BufferedReader(new InputStreamReader(getInputStream()));
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
+        return new ServletInputStream() {
+            @Override
+            public int read() throws IOException {
+                return bais.read();
+            }
+
+            @Override
+            public int available() throws IOException {
+                return body.length;
+            }
+
+            @Override
+            public boolean isFinished() {
+                return false;
+            }
+
+            @Override
+            public boolean isReady() {
+                return false;
+            }
+
+            @Override
+            public void setReadListener(ReadListener readListener) {
+
+            }
+        };
+    }
+}

+ 72 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/filter/XssFilter.java

@@ -0,0 +1,72 @@
+package com.lij.himeeter.common.filter;
+
+import com.lij.himeeter.common.utils.StringUtils;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpMethod;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  XssFilter
+ * 创建时间 : 2024年12月30日 19:04
+ *
+ * 文件描述 : 防止XSS攻击的过滤器
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class XssFilter implements Filter {
+
+    /**
+     * 排除链接
+     */
+    public List<String> excludes = new ArrayList<>();
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        String tempExcludes = filterConfig.getInitParameter("excludes");
+        if (StringUtils.isNotEmpty(tempExcludes)) {
+            String[] urls = tempExcludes.split(",");
+            for (String url : urls) {
+                excludes.add(url);
+            }
+        }
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        HttpServletRequest req = (HttpServletRequest) request;
+        HttpServletResponse resp = (HttpServletResponse) response;
+        if (handleExcludeURL(req, resp)) {
+            chain.doFilter(request, response);
+            return;
+        }
+        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
+        chain.doFilter(xssRequest, response);
+    }
+
+    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
+        String url = request.getServletPath();
+        String method = request.getMethod();
+        // GET DELETE 不过滤
+        if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) {
+            return true;
+        }
+        return StringUtils.matches(url, excludes);
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}

+ 120 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/filter/XssHttpServletRequestWrapper.java

@@ -0,0 +1,120 @@
+package com.lij.himeeter.common.filter;
+
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.html.EscapeUtil;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import org.apache.commons.io.IOUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  XssHttpServletRequestWrapper
+ * 创建时间 : 2024年12月30日 19:04
+ *
+ * 文件描述 : XSS过滤处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
+
+    /**
+     * @param request
+     */
+    public XssHttpServletRequestWrapper(HttpServletRequest request)
+    {
+        super(request);
+    }
+
+    @Override
+    public String[] getParameterValues(String name)
+    {
+        String[] values = super.getParameterValues(name);
+        if (values != null)
+        {
+            int length = values.length;
+            String[] escapesValues = new String[length];
+            for (int i = 0; i < length; i++)
+            {
+                // 防xss攻击和过滤前后空格
+                escapesValues[i] = EscapeUtil.clean(values[i]).trim();
+            }
+            return escapesValues;
+        }
+        return super.getParameterValues(name);
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException
+    {
+        // 非json类型,直接返回
+        if (!isJsonRequest())
+        {
+            return super.getInputStream();
+        }
+
+        // 为空,直接返回
+        String json = IOUtils.toString(super.getInputStream(), "utf-8");
+        if (StringUtils.isEmpty(json))
+        {
+            return super.getInputStream();
+        }
+
+        // xss过滤
+        json = EscapeUtil.clean(json).trim();
+        byte[] jsonBytes = json.getBytes("utf-8");
+        final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes);
+        return new ServletInputStream()
+        {
+            @Override
+            public boolean isFinished()
+            {
+                return true;
+            }
+
+            @Override
+            public boolean isReady()
+            {
+                return true;
+            }
+
+            @Override
+            public int available() throws IOException
+            {
+                return jsonBytes.length;
+            }
+
+            @Override
+            public void setReadListener(ReadListener readListener)
+            {
+            }
+
+            @Override
+            public int read() throws IOException
+            {
+                return bis.read();
+            }
+        };
+    }
+
+    /**
+     * 是否是Json请求
+     *
+     */
+    public boolean isJsonRequest()
+    {
+        String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
+        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
+    }
+}

+ 122 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/Arith.java

@@ -0,0 +1,122 @@
+package com.lij.himeeter.common.utils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  Arith
+ * 创建时间 : 2025年01月06日 03:17
+ *
+ * 文件描述 : 精确的浮点数运算
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class Arith {
+
+    /**
+     * 默认除法运算精度
+     */
+    private static final int DEF_DIV_SCALE = 10;
+
+    /**
+     * 这个类不能实例化
+     */
+    private Arith() {
+    }
+
+    /**
+     * 提供精确的加法运算。
+     *
+     * @param v1 被加数
+     * @param v2 加数
+     * @return 两个参数的和
+     */
+    public static double add(double v1, double v2) {
+        BigDecimal b1 = new BigDecimal(Double.toString(v1));
+        BigDecimal b2 = new BigDecimal(Double.toString(v2));
+        return b1.add(b2).doubleValue();
+    }
+
+    /**
+     * 提供精确的减法运算。
+     *
+     * @param v1 被减数
+     * @param v2 减数
+     * @return 两个参数的差
+     */
+    public static double sub(double v1, double v2) {
+        BigDecimal b1 = new BigDecimal(Double.toString(v1));
+        BigDecimal b2 = new BigDecimal(Double.toString(v2));
+        return b1.subtract(b2).doubleValue();
+    }
+
+    /**
+     * 提供精确的乘法运算。
+     *
+     * @param v1 被乘数
+     * @param v2 乘数
+     * @return 两个参数的积
+     */
+    public static double mul(double v1, double v2) {
+        BigDecimal b1 = new BigDecimal(Double.toString(v1));
+        BigDecimal b2 = new BigDecimal(Double.toString(v2));
+        return b1.multiply(b2).doubleValue();
+    }
+
+    /**
+     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
+     * 小数点以后10位,以后的数字四舍五入。
+     *
+     * @param v1 被除数
+     * @param v2 除数
+     * @return 两个参数的商
+     */
+    public static double div(double v1, double v2) {
+        return div(v1, v2, DEF_DIV_SCALE);
+    }
+
+    /**
+     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
+     * 定精度,以后的数字四舍五入。
+     *
+     * @param v1    被除数
+     * @param v2    除数
+     * @param scale 表示表示需要精确到小数点以后几位。
+     * @return 两个参数的商
+     */
+    public static double div(double v1, double v2, int scale) {
+        if (scale < 0) {
+            throw new IllegalArgumentException(
+                    "The scale must be a positive integer or zero");
+        }
+        BigDecimal b1 = new BigDecimal(Double.toString(v1));
+        BigDecimal b2 = new BigDecimal(Double.toString(v2));
+        if (b1.compareTo(BigDecimal.ZERO) == 0) {
+            return BigDecimal.ZERO.doubleValue();
+        }
+        return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
+    }
+
+    /**
+     * 提供精确的小数位四舍五入处理。
+     *
+     * @param v     需要四舍五入的数字
+     * @param scale 小数点后保留几位
+     * @return 四舍五入后的结果
+     */
+    public static double round(double v, int scale) {
+        if (scale < 0) {
+            throw new IllegalArgumentException(
+                    "The scale must be a positive integer or zero");
+        }
+        BigDecimal b = new BigDecimal(Double.toString(v));
+        BigDecimal one = BigDecimal.ONE;
+        return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
+    }
+}

+ 171 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/DateUtils.java

@@ -0,0 +1,171 @@
+package com.lij.himeeter.common.utils;
+
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.*;
+import java.util.Date;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  DateUtils
+ * 创建时间 : 2025年01月02日 15:19
+ *
+ * 文件描述 : 时间工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
+
+    public static String YYYY = "yyyy";
+
+    public static String YYYY_MM = "yyyy-MM";
+
+    public static String YYYY_MM_DD = "yyyy-MM-dd";
+
+    public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+    public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+    private static String[] parsePatterns = {"yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+    /**
+     * 获取当前Date型日期
+     *
+     * @return Date() 当前日期
+     */
+    public static Date getNowDate() {
+        return new Date();
+    }
+
+    /**
+     * 获取当前日期, 默认格式为yyyy-MM-dd
+     *
+     * @return String
+     */
+    public static String getDate() {
+        return dateTimeNow(YYYY_MM_DD);
+    }
+
+    public static final String getTime() {
+        return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+    }
+
+    public static final String dateTimeNow() {
+        return dateTimeNow(YYYYMMDDHHMMSS);
+    }
+
+    public static final String dateTimeNow(final String format) {
+        return parseDateToStr(format, new Date());
+    }
+
+    public static final String dateTime(final Date date) {
+        return parseDateToStr(YYYY_MM_DD, date);
+    }
+
+    public static final String parseDateToStr(final String format, final Date date) {
+        return new SimpleDateFormat(format).format(date);
+    }
+
+    public static final Date dateTime(final String format, final String ts) {
+        try {
+            return new SimpleDateFormat(format).parse(ts);
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 日期路径 即年/月/日 如2018/08/08
+     */
+    public static final String datePath() {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyy/MM/dd");
+    }
+
+    /**
+     * 日期路径 即年/月/日 如20180808
+     */
+    public static final String dateTime() {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyyMMdd");
+    }
+
+    /**
+     * 日期型字符串转化为日期 格式
+     */
+    public static Date parseDate(Object str) {
+        if (str == null) {
+            return null;
+        }
+        try {
+            return parseDate(str.toString(), parsePatterns);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取服务器启动时间
+     */
+    public static Date getServerStartDate() {
+        long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+        return new Date(time);
+    }
+
+    /**
+     * 计算相差天数
+     */
+    public static int differentDaysByMillisecond(Date date1, Date date2) {
+        return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
+    }
+
+    /**
+     * 计算时间差
+     *
+     * @param endDate   最后时间
+     * @param startTime 开始时间
+     * @return 时间差(天/小时/分钟)
+     */
+    public static String timeDistance(Date endDate, Date startTime) {
+        long nd = 1000 * 24 * 60 * 60;
+        long nh = 1000 * 60 * 60;
+        long nm = 1000 * 60;
+        // long ns = 1000;
+        // 获得两个时间的毫秒时间差异
+        long diff = endDate.getTime() - startTime.getTime();
+        // 计算差多少天
+        long day = diff / nd;
+        // 计算差多少小时
+        long hour = diff % nd / nh;
+        // 计算差多少分钟
+        long min = diff % nd % nh / nm;
+        // 计算差多少秒//输出结果
+        // long sec = diff % nd % nh % nm / ns;
+        return day + "天" + hour + "小时" + min + "分钟";
+    }
+
+    /**
+     * 增加 LocalDateTime ==> Date
+     */
+    public static Date toDate(LocalDateTime temporalAccessor) {
+        ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+
+    /**
+     * 增加 LocalDate ==> Date
+     */
+    public static Date toDate(LocalDate temporalAccessor) {
+        LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+        ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+}

+ 214 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/DictUtils.java

@@ -0,0 +1,214 @@
+package com.lij.himeeter.common.utils;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.lij.himeeter.common.constant.CacheConstants;
+import com.lij.himeeter.common.core.redis.RedisCache;
+import com.lij.himeeter.common.core.domain.entity.SysDictData;
+import com.lij.himeeter.common.utils.spring.SpringUtils;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  DictUtils
+ * 创建时间 : 2024年12月31日 17:11
+ *
+ * 文件描述 : 字典工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class DictUtils {
+
+    /**
+     * 分隔符
+     */
+    public static final String SEPARATOR = ",";
+
+    /**
+     * 设置字典缓存
+     *
+     * @param key       参数键
+     * @param dictDatas 字典数据列表
+     */
+    public static void setDictCache(String key, List<SysDictData> dictDatas) {
+        SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas);
+    }
+
+    /**
+     * 获取字典缓存
+     *
+     * @param key 参数键
+     * @return dictDatas 字典数据列表
+     */
+    public static List<SysDictData> getDictCache(String key) {
+        JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
+        if (StringUtils.isNotNull(arrayCache)) {
+            return arrayCache.toList(SysDictData.class);
+        }
+        return null;
+    }
+
+    /**
+     * 根据字典类型和字典值获取字典标签
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典值
+     * @return 字典标签
+     */
+    public static String getDictLabel(String dictType, String dictValue) {
+        if (StringUtils.isEmpty(dictValue)) {
+            return StringUtils.EMPTY;
+        }
+        return getDictLabel(dictType, dictValue, SEPARATOR);
+    }
+
+    /**
+     * 根据字典类型和字典标签获取字典值
+     *
+     * @param dictType  字典类型
+     * @param dictLabel 字典标签
+     * @return 字典值
+     */
+    public static String getDictValue(String dictType, String dictLabel) {
+        if (StringUtils.isEmpty(dictLabel)) {
+            return StringUtils.EMPTY;
+        }
+        return getDictValue(dictType, dictLabel, SEPARATOR);
+    }
+
+    /**
+     * 根据字典类型和字典值获取字典标签
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典值
+     * @param separator 分隔符
+     * @return 字典标签
+     */
+    public static String getDictLabel(String dictType, String dictValue, String separator) {
+        StringBuilder propertyString = new StringBuilder();
+        List<SysDictData> datas = getDictCache(dictType);
+        if (StringUtils.isNull(datas)) {
+            return StringUtils.EMPTY;
+        }
+        if (StringUtils.containsAny(separator, dictValue)) {
+            for (SysDictData dict : datas) {
+                for (String value : dictValue.split(separator)) {
+                    if (value.equals(dict.getDictValue())) {
+                        propertyString.append(dict.getDictLabel()).append(separator);
+                        break;
+                    }
+                }
+            }
+        } else {
+            for (SysDictData dict : datas) {
+                if (dictValue.equals(dict.getDictValue())) {
+                    return dict.getDictLabel();
+                }
+            }
+        }
+        return StringUtils.stripEnd(propertyString.toString(), separator);
+    }
+
+    /**
+     * 根据字典类型和字典标签获取字典值
+     *
+     * @param dictType  字典类型
+     * @param dictLabel 字典标签
+     * @param separator 分隔符
+     * @return 字典值
+     */
+    public static String getDictValue(String dictType, String dictLabel, String separator) {
+        StringBuilder propertyString = new StringBuilder();
+        List<SysDictData> datas = getDictCache(dictType);
+        if (StringUtils.isNull(datas)) {
+            return StringUtils.EMPTY;
+        }
+        if (StringUtils.containsAny(separator, dictLabel)) {
+            for (SysDictData dict : datas) {
+                for (String label : dictLabel.split(separator)) {
+                    if (label.equals(dict.getDictLabel())) {
+                        propertyString.append(dict.getDictValue()).append(separator);
+                        break;
+                    }
+                }
+            }
+        } else {
+            for (SysDictData dict : datas) {
+                if (dictLabel.equals(dict.getDictLabel())) {
+                    return dict.getDictValue();
+                }
+            }
+        }
+        return StringUtils.stripEnd(propertyString.toString(), separator);
+    }
+
+    /**
+     * 根据字典类型获取字典所有值
+     *
+     * @param dictType 字典类型
+     * @return 字典值
+     */
+    public static String getDictValues(String dictType) {
+        StringBuilder propertyString = new StringBuilder();
+        List<SysDictData> datas = getDictCache(dictType);
+        if (StringUtils.isNull(datas)) {
+            return StringUtils.EMPTY;
+        }
+        for (SysDictData dict : datas) {
+            propertyString.append(dict.getDictValue()).append(SEPARATOR);
+        }
+        return StringUtils.stripEnd(propertyString.toString(), SEPARATOR);
+    }
+
+    /**
+     * 根据字典类型获取字典所有标签
+     *
+     * @param dictType 字典类型
+     * @return 字典值
+     */
+    public static String getDictLabels(String dictType) {
+        StringBuilder propertyString = new StringBuilder();
+        List<SysDictData> datas = getDictCache(dictType);
+        if (StringUtils.isNull(datas)) {
+            return StringUtils.EMPTY;
+        }
+        for (SysDictData dict : datas) {
+            propertyString.append(dict.getDictLabel()).append(SEPARATOR);
+        }
+        return StringUtils.stripEnd(propertyString.toString(), SEPARATOR);
+    }
+
+    /**
+     * 删除指定字典缓存
+     *
+     * @param key 字典键
+     */
+    public static void removeDictCache(String key) {
+        SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key));
+    }
+
+    /**
+     * 清空字典缓存
+     */
+    public static void clearDictCache() {
+        Collection<String> keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*");
+        SpringUtils.getBean(RedisCache.class).deleteObject(keys);
+    }
+
+    /**
+     * 设置cache key
+     *
+     * @param configKey 参数键
+     * @return 缓存键key
+     */
+    public static String getCacheKey(String configKey) {
+        return CacheConstants.SYS_DICT_KEY + configKey;
+    }
+
+}

+ 45 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/ExceptionUtil.java

@@ -0,0 +1,45 @@
+package com.lij.himeeter.common.utils;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ExceptionUtil
+ * 创建时间 : 2025年01月06日 03:22
+ *
+ * 文件描述 : 错误信息处理类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class ExceptionUtil {
+
+    /**
+     * 获取exception的详细错误信息。
+     */
+    public static String getExceptionMessage(Throwable e) {
+        StringWriter sw = new StringWriter();
+        e.printStackTrace(new PrintWriter(sw, true));
+        return sw.toString();
+    }
+
+    public static String getRootErrorMessage(Exception e) {
+        Throwable root = ExceptionUtils.getRootCause(e);
+        root = (root == null ? e : root);
+        if (root == null) {
+            return "";
+        }
+        String msg = root.getMessage();
+        if (msg == null) {
+            return "null";
+        }
+        return StringUtils.defaultString(msg);
+    }
+}

+ 35 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/MessageUtils.java

@@ -0,0 +1,35 @@
+package com.lij.himeeter.common.utils;
+
+import com.lij.himeeter.common.utils.spring.SpringUtils;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  MessageUtils
+ * 创建时间 : 2025年01月02日 15:29
+ *
+ * 文件描述 : 获取i18n资源文件
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class MessageUtils {
+
+    /**
+     * 根据消息键和参数 获取消息 委托给spring messageSource
+     *
+     * @param code 消息键
+     * @param args 参数
+     * @return 获取国际化翻译值
+     */
+    public static String message(String code, Object... args)
+    {
+        MessageSource messageSource = SpringUtils.getBean(MessageSource.class);
+        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
+    }
+}

+ 42 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/PageUtils.java

@@ -0,0 +1,42 @@
+package com.lij.himeeter.common.utils;
+
+import com.github.pagehelper.PageHelper;
+import com.lij.himeeter.common.core.page.PageDomain;
+import com.lij.himeeter.common.core.page.TableSupport;
+import com.lij.himeeter.common.utils.sql.SqlUtil;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  PageUtils
+ * 创建时间 : 2025年01月02日 15:51
+ *
+ * 文件描述 : 分页工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class PageUtils extends PageHelper {
+
+    /**
+     * 设置请求分页数据
+     */
+    public static void startPage() {
+        PageDomain pageDomain = TableSupport.buildPageRequest();
+        Integer pageNum = pageDomain.getPageNum();
+        Integer pageSize = pageDomain.getPageSize();
+        String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
+        Boolean reasonable = pageDomain.getReasonable();
+        PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
+    }
+
+    /**
+     * 清理分页的线程变量
+     */
+    public static void clearPage() {
+        PageHelper.clearPage();
+    }
+}

+ 114 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/bean/BeanUtils.java

@@ -0,0 +1,114 @@
+package com.lij.himeeter.common.utils.bean;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  BeanUtils
+ * 创建时间 : 2025年01月03日 18:50
+ *
+ * 文件描述 : Bean 工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class BeanUtils extends org.springframework.beans.BeanUtils {
+
+    /**
+     * Bean方法名中属性名开始的下标
+     */
+    private static final int BEAN_METHOD_PROP_INDEX = 3;
+
+    /**
+     * 匹配getter方法的正则表达式
+     */
+    private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)");
+
+    /**
+     * 匹配setter方法的正则表达式
+     */
+    private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)");
+
+    /**
+     * Bean属性复制工具方法。
+     *
+     * @param dest 目标对象
+     * @param src  源对象
+     */
+    public static void copyBeanProp(Object dest, Object src) {
+        try {
+            copyProperties(src, dest);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取对象的setter方法。
+     *
+     * @param obj 对象
+     * @return 对象的setter方法列表
+     */
+    public static List<Method> getSetterMethods(Object obj) {
+        // setter方法列表
+        List<Method> setterMethods = new ArrayList<Method>();
+
+        // 获取所有方法
+        Method[] methods = obj.getClass().getMethods();
+
+        // 查找setter方法
+
+        for (Method method : methods) {
+            Matcher m = SET_PATTERN.matcher(method.getName());
+            if (m.matches() && (method.getParameterTypes().length == 1)) {
+                setterMethods.add(method);
+            }
+        }
+        // 返回setter方法列表
+        return setterMethods;
+    }
+
+    /**
+     * 获取对象的getter方法。
+     *
+     * @param obj 对象
+     * @return 对象的getter方法列表
+     */
+
+    public static List<Method> getGetterMethods(Object obj) {
+        // getter方法列表
+        List<Method> getterMethods = new ArrayList<Method>();
+        // 获取所有方法
+        Method[] methods = obj.getClass().getMethods();
+        // 查找getter方法
+        for (Method method : methods) {
+            Matcher m = GET_PATTERN.matcher(method.getName());
+            if (m.matches() && (method.getParameterTypes().length == 0)) {
+                getterMethods.add(method);
+            }
+        }
+        // 返回getter方法列表
+        return getterMethods;
+    }
+
+    /**
+     * 检查Bean方法名中的属性名是否相等。<br>
+     * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。
+     *
+     * @param m1 方法名1
+     * @param m2 方法名2
+     * @return 属性名一样返回true,否则返回false
+     */
+
+    public static boolean isMethodPropEquals(String m1, String m2) {
+        return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX));
+    }
+}

+ 32 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/bean/BeanValidators.java

@@ -0,0 +1,32 @@
+package com.lij.himeeter.common.utils.bean;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
+
+import java.util.Set;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  BeanValidators
+ * 创建时间 : 2024年12月31日 16:59
+ *
+ * 文件描述 : bean对象属性验证
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class BeanValidators {
+
+    public static void validateWithException(Validator validator, Object object, Class<?>... groups)
+            throws ConstraintViolationException {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+        if (!constraintViolations.isEmpty()) {
+            throw new ConstraintViolationException(constraintViolations);
+        }
+    }
+}

+ 74 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/FileTypeUtils.java

@@ -0,0 +1,74 @@
+package com.lij.himeeter.common.utils.file;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FileTypeUtils
+ * 创建时间 : 2025年01月03日 18:53
+ *
+ * 文件描述 : 文件类型工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class FileTypeUtils {
+
+    /**
+     * 获取文件类型
+     * <p>
+     * 例如: himeeter.txt, 返回: txt
+     *
+     * @param file 文件名
+     * @return 后缀(不含".")
+     */
+    public static String getFileType(File file) {
+        if (null == file) {
+            return StringUtils.EMPTY;
+        }
+        return getFileType(file.getName());
+    }
+
+    /**
+     * 获取文件类型
+     * <p>
+     * 例如: himeeter.txt, 返回: txt
+     *
+     * @param fileName 文件名
+     * @return 后缀(不含".")
+     */
+    public static String getFileType(String fileName) {
+        int separatorIndex = fileName.lastIndexOf(".");
+        if (separatorIndex < 0) {
+            return "";
+        }
+        return fileName.substring(separatorIndex + 1).toLowerCase();
+    }
+
+    /**
+     * 获取文件类型
+     *
+     * @param photoByte 文件字节码
+     * @return 后缀(不含".")
+     */
+    public static String getFileExtendName(byte[] photoByte) {
+        String strFileExtendName = "JPG";
+        if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56)
+                && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) {
+            strFileExtendName = "GIF";
+        } else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) {
+            strFileExtendName = "JPG";
+        } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) {
+            strFileExtendName = "BMP";
+        } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) {
+            strFileExtendName = "PNG";
+        }
+        return strFileExtendName;
+    }
+}

+ 241 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/FileUploadUtils.java

@@ -0,0 +1,241 @@
+package com.lij.himeeter.common.utils.file;
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.exception.file.FileNameLengthLimitExceededException;
+import com.lij.himeeter.common.exception.file.FileSizeLimitExceededException;
+import com.lij.himeeter.common.exception.file.InvalidExtensionException;
+import com.lij.himeeter.common.utils.DateUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.uuid.Seq;
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Objects;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FileUploadUtils
+ * 创建时间 : 2025年01月03日 18:54
+ *
+ * 文件描述 : 文件上传工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class FileUploadUtils {
+    /**
+     * 默认大小 50M
+     */
+    public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024L;
+
+    /**
+     * 默认的文件名最大长度 100
+     */
+    public static final int DEFAULT_FILE_NAME_LENGTH = 100;
+
+    /**
+     * 默认上传的地址
+     */
+    private static String defaultBaseDir = HimeeterConfig.getProfile();
+
+    public static void setDefaultBaseDir(String defaultBaseDir)
+    {
+        FileUploadUtils.defaultBaseDir = defaultBaseDir;
+    }
+
+    public static String getDefaultBaseDir()
+    {
+        return defaultBaseDir;
+    }
+
+    /**
+     * 以默认配置进行文件上传
+     *
+     * @param file 上传的文件
+     * @return 文件名称
+     * @throws Exception
+     */
+    public static final String upload(MultipartFile file) throws IOException
+    {
+        try
+        {
+            return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
+        }
+        catch (Exception e)
+        {
+            throw new IOException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 根据文件路径上传
+     *
+     * @param baseDir 相对应用的基目录
+     * @param file 上传的文件
+     * @return 文件名称
+     * @throws IOException
+     */
+    public static final String upload(String baseDir, MultipartFile file) throws IOException
+    {
+        try
+        {
+            return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
+        }
+        catch (Exception e)
+        {
+            throw new IOException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 文件上传
+     *
+     * @param baseDir 相对应用的基目录
+     * @param file 上传的文件
+     * @param allowedExtension 上传文件类型
+     * @return 返回上传成功的文件名
+     * @throws FileSizeLimitExceededException 如果超出最大大小
+     * @throws FileNameLengthLimitExceededException 文件名太长
+     * @throws IOException 比如读写文件出错时
+     * @throws InvalidExtensionException 文件校验异常
+     */
+    public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
+            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
+            InvalidExtensionException
+    {
+        int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
+        if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
+        {
+            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
+        }
+
+        assertAllowed(file, allowedExtension);
+
+        String fileName = extractFilename(file);
+
+        String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
+        file.transferTo(Paths.get(absPath));
+        return getPathFileName(baseDir, fileName);
+    }
+
+    /**
+     * 编码文件名
+     */
+    public static final String extractFilename(MultipartFile file)
+    {
+        return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
+                FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));
+    }
+
+    public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
+    {
+        File desc = new File(uploadDir + File.separator + fileName);
+
+        if (!desc.exists())
+        {
+            if (!desc.getParentFile().exists())
+            {
+                desc.getParentFile().mkdirs();
+            }
+        }
+        return desc;
+    }
+
+    public static final String getPathFileName(String uploadDir, String fileName) throws IOException
+    {
+        int dirLastIndex = HimeeterConfig.getProfile().length() + 1;
+        String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
+        return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
+    }
+
+    /**
+     * 文件大小校验
+     *
+     * @param file 上传的文件
+     * @return
+     * @throws FileSizeLimitExceededException 如果超出最大大小
+     * @throws InvalidExtensionException
+     */
+    public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
+            throws FileSizeLimitExceededException, InvalidExtensionException
+    {
+        long size = file.getSize();
+        if (size > DEFAULT_MAX_SIZE)
+        {
+            throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
+        }
+
+        String fileName = file.getOriginalFilename();
+        String extension = getExtension(file);
+        if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension))
+        {
+            if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION)
+            {
+                throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension,
+                        fileName);
+            }
+            else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION)
+            {
+                throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension,
+                        fileName);
+            }
+            else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION)
+            {
+                throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension,
+                        fileName);
+            }
+            else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION)
+            {
+                throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension,
+                        fileName);
+            }
+            else
+            {
+                throw new InvalidExtensionException(allowedExtension, extension, fileName);
+            }
+        }
+    }
+
+    /**
+     * 判断MIME类型是否是允许的MIME类型
+     *
+     * @param extension
+     * @param allowedExtension
+     * @return
+     */
+    public static final boolean isAllowedExtension(String extension, String[] allowedExtension)
+    {
+        for (String str : allowedExtension)
+        {
+            if (str.equalsIgnoreCase(extension))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 获取文件名的后缀
+     *
+     * @param file 表单文件
+     * @return 后缀名
+     */
+    public static final String getExtension(MultipartFile file)
+    {
+        String extension = FilenameUtils.getExtension(file.getOriginalFilename());
+        if (StringUtils.isEmpty(extension))
+        {
+            extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
+        }
+        return extension;
+    }
+}

+ 253 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/FileUtils.java

@@ -0,0 +1,253 @@
+package com.lij.himeeter.common.utils.file;
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.utils.DateUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.uuid.IdUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FileUtils
+ * 创建时间 : 2025年01月03日 18:52
+ *
+ * 文件描述 : 文件处理工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class FileUtils {
+    public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+";
+
+    /**
+     * 输出指定文件的byte数组
+     *
+     * @param filePath 文件路径
+     * @param os       输出流
+     * @return
+     */
+    public static void writeBytes(String filePath, OutputStream os) throws IOException {
+        FileInputStream fis = null;
+        try {
+            File file = new File(filePath);
+            if (!file.exists()) {
+                throw new FileNotFoundException(filePath);
+            }
+            fis = new FileInputStream(file);
+            byte[] b = new byte[1024];
+            int length;
+            while ((length = fis.read(b)) > 0) {
+                os.write(b, 0, length);
+            }
+        } catch (IOException e) {
+            throw e;
+        } finally {
+            IOUtils.close(os);
+            IOUtils.close(fis);
+        }
+    }
+
+    /**
+     * 写数据到文件中
+     *
+     * @param data 数据
+     * @return 目标文件
+     * @throws IOException IO异常
+     */
+    public static String writeImportBytes(byte[] data) throws IOException {
+        return writeBytes(data, HimeeterConfig.getImportPath());
+    }
+
+    /**
+     * 写数据到文件中
+     *
+     * @param data      数据
+     * @param uploadDir 目标文件
+     * @return 目标文件
+     * @throws IOException IO异常
+     */
+    public static String writeBytes(byte[] data, String uploadDir) throws IOException {
+        FileOutputStream fos = null;
+        String pathName = "";
+        try {
+            String extension = getFileExtendName(data);
+            pathName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension;
+            File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName);
+            fos = new FileOutputStream(file);
+            fos.write(data);
+        } finally {
+            IOUtils.close(fos);
+        }
+        return FileUploadUtils.getPathFileName(uploadDir, pathName);
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param filePath 文件
+     * @return
+     */
+    public static boolean deleteFile(String filePath) {
+        boolean flag = false;
+        File file = new File(filePath);
+        // 路径为文件且不为空则进行删除
+        if (file.isFile() && file.exists()) {
+            flag = file.delete();
+        }
+        return flag;
+    }
+
+    /**
+     * 文件名称验证
+     *
+     * @param filename 文件名称
+     * @return true 正常 false 非法
+     */
+    public static boolean isValidFilename(String filename) {
+        return filename.matches(FILENAME_PATTERN);
+    }
+
+    /**
+     * 检查文件是否可下载
+     *
+     * @param resource 需要下载的文件
+     * @return true 正常 false 非法
+     */
+    public static boolean checkAllowDownload(String resource) {
+        // 禁止目录上跳级别
+        if (StringUtils.contains(resource, "..")) {
+            return false;
+        }
+
+        // 检查允许下载的文件规则
+        if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) {
+            return true;
+        }
+
+        // 不在允许下载的文件规则
+        return false;
+    }
+
+    /**
+     * 下载文件名重新编码
+     *
+     * @param request  请求对象
+     * @param fileName 文件名
+     * @return 编码后的文件名
+     */
+    public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException {
+        final String agent = request.getHeader("USER-AGENT");
+        String filename = fileName;
+        if (agent.contains("MSIE")) {
+            // IE浏览器
+            filename = URLEncoder.encode(filename, "utf-8");
+            filename = filename.replace("+", " ");
+        } else if (agent.contains("Firefox")) {
+            // 火狐浏览器
+            filename = new String(fileName.getBytes(), "ISO8859-1");
+        } else if (agent.contains("Chrome")) {
+            // google浏览器
+            filename = URLEncoder.encode(filename, "utf-8");
+        } else {
+            // 其它浏览器
+            filename = URLEncoder.encode(filename, "utf-8");
+        }
+        return filename;
+    }
+
+    /**
+     * 下载文件名重新编码
+     *
+     * @param response     响应对象
+     * @param realFileName 真实文件名
+     */
+    public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException {
+        String percentEncodedFileName = percentEncode(realFileName);
+
+        StringBuilder contentDispositionValue = new StringBuilder();
+        contentDispositionValue.append("attachment; filename=")
+                .append(percentEncodedFileName)
+                .append(";")
+                .append("filename*=")
+                .append("utf-8''")
+                .append(percentEncodedFileName);
+
+        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+        response.setHeader("Content-disposition", contentDispositionValue.toString());
+        response.setHeader("download-filename", percentEncodedFileName);
+    }
+
+    /**
+     * 百分号编码工具方法
+     *
+     * @param s 需要百分号编码的字符串
+     * @return 百分号编码后的字符串
+     */
+    public static String percentEncode(String s) throws UnsupportedEncodingException {
+        String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString());
+        return encode.replaceAll("\\+", "%20");
+    }
+
+    /**
+     * 获取图像后缀
+     *
+     * @param photoByte 图像数据
+     * @return 后缀名
+     */
+    public static String getFileExtendName(byte[] photoByte) {
+        String strFileExtendName = "jpg";
+        if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56)
+                && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) {
+            strFileExtendName = "gif";
+        } else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) {
+            strFileExtendName = "jpg";
+        } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) {
+            strFileExtendName = "bmp";
+        } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) {
+            strFileExtendName = "png";
+        }
+        return strFileExtendName;
+    }
+
+    /**
+     * 获取文件名称 /profile/upload/2022/04/16/himeeter.png -- himeeter.png
+     *
+     * @param fileName 路径名称
+     * @return 没有文件路径的名称
+     */
+    public static String getName(String fileName) {
+        if (fileName == null) {
+            return null;
+        }
+        int lastUnixPos = fileName.lastIndexOf('/');
+        int lastWindowsPos = fileName.lastIndexOf('\\');
+        int index = Math.max(lastUnixPos, lastWindowsPos);
+        return fileName.substring(index + 1);
+    }
+
+    /**
+     * 获取不带后缀文件名称 /profile/upload/2022/04/16/himeeter.png -- himeeter
+     *
+     * @param fileName 路径名称
+     * @return 没有文件路径和后缀的名称
+     */
+    public static String getNameNotSuffix(String fileName) {
+        if (fileName == null) {
+            return null;
+        }
+        String baseName = FilenameUtils.getBaseName(fileName);
+        return baseName;
+    }
+}

+ 89 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/ImageUtils.java

@@ -0,0 +1,89 @@
+package com.lij.himeeter.common.utils.file;
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.utils.StringUtils;
+import org.apache.poi.util.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ImageUtils
+ * 创建时间 : 2025年01月03日 18:55
+ *
+ * 文件描述 : 图片处理工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class ImageUtils {
+
+    private static final Logger log = LoggerFactory.getLogger(ImageUtils.class);
+
+    public static byte[] getImage(String imagePath) {
+        InputStream is = getFile(imagePath);
+        try {
+            return IOUtils.toByteArray(is);
+        } catch (Exception e) {
+            log.error("图片加载异常 {}", e);
+            return null;
+        } finally {
+            IOUtils.closeQuietly(is);
+        }
+    }
+
+    public static InputStream getFile(String imagePath) {
+        try {
+            byte[] result = readFile(imagePath);
+            result = Arrays.copyOf(result, result.length);
+            return new ByteArrayInputStream(result);
+        } catch (Exception e) {
+            log.error("获取图片异常 {}", e);
+        }
+        return null;
+    }
+
+    /**
+     * 读取文件为字节数据
+     *
+     * @param url 地址
+     * @return 字节数据
+     */
+    public static byte[] readFile(String url) {
+        InputStream in = null;
+        try {
+            if (url.startsWith("http")) {
+                // 网络地址
+                URL urlObj = new URL(url);
+                URLConnection urlConnection = urlObj.openConnection();
+                urlConnection.setConnectTimeout(30 * 1000);
+                urlConnection.setReadTimeout(60 * 1000);
+                urlConnection.setDoInput(true);
+                in = urlConnection.getInputStream();
+            } else {
+                // 本机地址
+                String localPath = HimeeterConfig.getProfile();
+                String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX);
+                in = new FileInputStream(downloadPath);
+            }
+            return IOUtils.toByteArray(in);
+        } catch (Exception e) {
+            log.error("获取文件路径异常 {}", e);
+            return null;
+        } finally {
+            IOUtils.closeQuietly(in);
+        }
+    }
+}

+ 66 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/file/MimeTypeUtils.java

@@ -0,0 +1,66 @@
+package com.lij.himeeter.common.utils.file;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  MimeTypeUtils
+ * 创建时间 : 2025年01月03日 18:57
+ *
+ * 文件描述 : 媒体类型工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class MimeTypeUtils {
+
+    public static final String IMAGE_PNG = "image/png";
+
+    public static final String IMAGE_JPG = "image/jpg";
+
+    public static final String IMAGE_JPEG = "image/jpeg";
+
+    public static final String IMAGE_BMP = "image/bmp";
+
+    public static final String IMAGE_GIF = "image/gif";
+
+    public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+    public static final String[] FLASH_EXTENSION = {"swf", "flv"};
+
+    public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+            "asf", "rm", "rmvb"};
+
+    public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+    public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+            // 图片
+            "bmp", "gif", "jpg", "jpeg", "png",
+            // word excel powerpoint
+            "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+            // 压缩文件
+            "rar", "zip", "gz", "bz2",
+            // 视频格式
+            "mp4", "avi", "rmvb",
+            // pdf
+            "pdf"};
+
+    public static String getExtension(String prefix) {
+        switch (prefix) {
+            case IMAGE_PNG:
+                return "png";
+            case IMAGE_JPG:
+                return "jpg";
+            case IMAGE_JPEG:
+                return "jpeg";
+            case IMAGE_BMP:
+                return "bmp";
+            case IMAGE_GIF:
+                return "gif";
+            default:
+                return "";
+        }
+    }
+}

+ 150 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/html/EscapeUtil.java

@@ -0,0 +1,150 @@
+package com.lij.himeeter.common.utils.html;
+
+import com.lij.himeeter.common.utils.StringUtils;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  EscapeUtil
+ * 创建时间 : 2024年12月30日 19:09
+ *
+ * 文件描述 : 转义和反转义工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class EscapeUtil {
+
+    public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
+
+    private static final char[][] TEXT = new char[64][];
+
+    static {
+        for (int i = 0; i < 64; i++) {
+            TEXT[i] = new char[]{(char) i};
+        }
+
+        // special HTML characters
+        TEXT['\''] = "&#039;".toCharArray(); // 单引号
+        TEXT['"'] = "&#34;".toCharArray(); // 双引号
+        TEXT['&'] = "&#38;".toCharArray(); // &符
+        TEXT['<'] = "&#60;".toCharArray(); // 小于号
+        TEXT['>'] = "&#62;".toCharArray(); // 大于号
+    }
+
+    /**
+     * 转义文本中的HTML字符为安全的字符
+     *
+     * @param text 被转义的文本
+     * @return 转义后的文本
+     */
+    public static String escape(String text) {
+        return encode(text);
+    }
+
+    /**
+     * 还原被转义的HTML特殊字符
+     *
+     * @param content 包含转义符的HTML内容
+     * @return 转换后的字符串
+     */
+    public static String unescape(String content) {
+        return decode(content);
+    }
+
+    /**
+     * 清除所有HTML标签,但是不删除标签内的内容
+     *
+     * @param content 文本
+     * @return 清除标签后的文本
+     */
+    public static String clean(String content) {
+        return new HTMLFilter().filter(content);
+    }
+
+    /**
+     * Escape编码
+     *
+     * @param text 被编码的文本
+     * @return 编码后的字符
+     */
+    private static String encode(String text) {
+        if (StringUtils.isEmpty(text)) {
+            return StringUtils.EMPTY;
+        }
+
+        final StringBuilder tmp = new StringBuilder(text.length() * 6);
+        char c;
+        for (int i = 0; i < text.length(); i++) {
+            c = text.charAt(i);
+            if (c < 256) {
+                tmp.append("%");
+                if (c < 16) {
+                    tmp.append("0");
+                }
+                tmp.append(Integer.toString(c, 16));
+            } else {
+                tmp.append("%u");
+                if (c <= 0xfff) {
+                    // issue#I49JU8@Gitee
+                    tmp.append("0");
+                }
+                tmp.append(Integer.toString(c, 16));
+            }
+        }
+        return tmp.toString();
+    }
+
+    /**
+     * Escape解码
+     *
+     * @param content 被转义的内容
+     * @return 解码后的字符串
+     */
+    public static String decode(String content) {
+        if (StringUtils.isEmpty(content)) {
+            return content;
+        }
+
+        StringBuilder tmp = new StringBuilder(content.length());
+        int lastPos = 0, pos = 0;
+        char ch;
+        while (lastPos < content.length()) {
+            pos = content.indexOf("%", lastPos);
+            if (pos == lastPos) {
+                if (content.charAt(pos + 1) == 'u') {
+                    ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
+                    tmp.append(ch);
+                    lastPos = pos + 6;
+                } else {
+                    ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
+                    tmp.append(ch);
+                    lastPos = pos + 3;
+                }
+            } else {
+                if (pos == -1) {
+                    tmp.append(content.substring(lastPos));
+                    lastPos = content.length();
+                } else {
+                    tmp.append(content.substring(lastPos, pos));
+                    lastPos = pos;
+                }
+            }
+        }
+        return tmp.toString();
+    }
+
+    public static void main(String[] args) {
+        String html = "<script>alert(1);</script>";
+        String escape = EscapeUtil.escape(html);
+        // String html = "<scr<script>ipt>alert(\"XSS\")</scr<script>ipt>";
+        // String html = "<123";
+        // String html = "123>";
+        System.out.println("clean: " + EscapeUtil.clean(html));
+        System.out.println("escape: " + escape);
+        System.out.println("unescape: " + EscapeUtil.unescape(escape));
+    }
+}

+ 507 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/html/HTMLFilter.java

@@ -0,0 +1,507 @@
+package com.lij.himeeter.common.utils.html;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  HTMLFilter
+ * 创建时间 : 2024年12月30日 19:10
+ *
+ * 文件描述 : HTML过滤器,用于去除XSS漏洞隐患。
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class HTMLFilter {
+
+    /**
+     * regex flag union representing /si modifiers in php
+     **/
+    private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
+    private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL);
+    private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
+    private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
+    private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
+    private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
+    private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
+    private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
+    private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
+    private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?");
+    private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?");
+    private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
+    private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
+    private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
+    private static final Pattern P_END_ARROW = Pattern.compile("^>");
+    private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
+    private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
+    private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
+    private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
+    private static final Pattern P_AMP = Pattern.compile("&");
+    private static final Pattern P_QUOTE = Pattern.compile("\"");
+    private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
+    private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
+    private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
+
+    // @xxx could grow large... maybe use sesat's ReferenceMap
+    private static final ConcurrentMap<String, Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>();
+
+    /**
+     * set of allowed html elements, along with allowed attributes for each element
+     **/
+    private final Map<String, List<String>> vAllowed;
+    /**
+     * counts of open tags for each (allowable) html element
+     **/
+    private final Map<String, Integer> vTagCounts = new HashMap<>();
+
+    /**
+     * html elements which must always be self-closing (e.g. "<img />")
+     **/
+    private final String[] vSelfClosingTags;
+    /**
+     * html elements which must always have separate opening and closing tags (e.g. "<b></b>")
+     **/
+    private final String[] vNeedClosingTags;
+    /**
+     * set of disallowed html elements
+     **/
+    private final String[] vDisallowed;
+    /**
+     * attributes which should be checked for valid protocols
+     **/
+    private final String[] vProtocolAtts;
+    /**
+     * allowed protocols
+     **/
+    private final String[] vAllowedProtocols;
+    /**
+     * tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />")
+     **/
+    private final String[] vRemoveBlanks;
+    /**
+     * entities allowed within html markup
+     **/
+    private final String[] vAllowedEntities;
+    /**
+     * flag determining whether comments are allowed in input String.
+     */
+    private final boolean stripComment;
+    private final boolean encodeQuotes;
+    /**
+     * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "<b text </b>"
+     * becomes "<b> text </b>"). If set to false, unbalanced angle brackets will be html escaped.
+     */
+    private final boolean alwaysMakeTags;
+
+    /**
+     * Default constructor.
+     */
+    public HTMLFilter() {
+        vAllowed = new HashMap<>();
+
+        final ArrayList<String> a_atts = new ArrayList<>();
+        a_atts.add("href");
+        a_atts.add("target");
+        vAllowed.put("a", a_atts);
+
+        final ArrayList<String> img_atts = new ArrayList<>();
+        img_atts.add("src");
+        img_atts.add("width");
+        img_atts.add("height");
+        img_atts.add("alt");
+        vAllowed.put("img", img_atts);
+
+        final ArrayList<String> no_atts = new ArrayList<>();
+        vAllowed.put("b", no_atts);
+        vAllowed.put("strong", no_atts);
+        vAllowed.put("i", no_atts);
+        vAllowed.put("em", no_atts);
+
+        vSelfClosingTags = new String[]{"img"};
+        vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"};
+        vDisallowed = new String[]{};
+        vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp.
+        vProtocolAtts = new String[]{"src", "href"};
+        vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"};
+        vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"};
+        stripComment = true;
+        encodeQuotes = true;
+        alwaysMakeTags = false;
+    }
+
+    /**
+     * Map-parameter configurable constructor.
+     *
+     * @param conf map containing configuration. keys match field names.
+     */
+    @SuppressWarnings("unchecked")
+    public HTMLFilter(final Map<String, Object> conf) {
+
+        assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
+        assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
+        assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
+        assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
+        assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
+        assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts";
+        assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
+        assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
+
+        vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed"));
+        vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
+        vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
+        vDisallowed = (String[]) conf.get("vDisallowed");
+        vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
+        vProtocolAtts = (String[]) conf.get("vProtocolAtts");
+        vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
+        vAllowedEntities = (String[]) conf.get("vAllowedEntities");
+        stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
+        encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
+        alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
+    }
+
+    private void reset() {
+        vTagCounts.clear();
+    }
+
+    // ---------------------------------------------------------------
+    // my versions of some PHP library functions
+    public static String chr(final int decimal) {
+        return String.valueOf((char) decimal);
+    }
+
+    public static String htmlSpecialChars(final String s) {
+        String result = s;
+        result = regexReplace(P_AMP, "&amp;", result);
+        result = regexReplace(P_QUOTE, "&quot;", result);
+        result = regexReplace(P_LEFT_ARROW, "&lt;", result);
+        result = regexReplace(P_RIGHT_ARROW, "&gt;", result);
+        return result;
+    }
+
+    // ---------------------------------------------------------------
+
+    /**
+     * given a user submitted input String, filter out any invalid or restricted html.
+     *
+     * @param input text (i.e. submitted by a user) than may contain html
+     * @return "clean" version of input, with only valid, whitelisted html elements allowed
+     */
+    public String filter(final String input) {
+        reset();
+        String s = input;
+
+        s = escapeComments(s);
+
+        s = balanceHTML(s);
+
+        s = checkTags(s);
+
+        s = processRemoveBlanks(s);
+
+        // s = validateEntities(s);
+
+        return s;
+    }
+
+    public boolean isAlwaysMakeTags() {
+        return alwaysMakeTags;
+    }
+
+    public boolean isStripComments() {
+        return stripComment;
+    }
+
+    private String escapeComments(final String s) {
+        final Matcher m = P_COMMENTS.matcher(s);
+        final StringBuffer buf = new StringBuffer();
+        if (m.find()) {
+            final String match = m.group(1); // (.*?)
+            m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->"));
+        }
+        m.appendTail(buf);
+
+        return buf.toString();
+    }
+
+    private String balanceHTML(String s) {
+        if (alwaysMakeTags) {
+            //
+            // try and form html
+            //
+            s = regexReplace(P_END_ARROW, "", s);
+            // 不追加结束标签
+            s = regexReplace(P_BODY_TO_END, "<$1>", s);
+            s = regexReplace(P_XML_CONTENT, "$1<$2", s);
+
+        } else {
+            //
+            // escape stray brackets
+            //
+            s = regexReplace(P_STRAY_LEFT_ARROW, "&lt;$1", s);
+            s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2&gt;<", s);
+
+            //
+            // the last regexp causes '<>' entities to appear
+            // (we need to do a lookahead assertion so that the last bracket can
+            // be used in the next pass of the regexp)
+            //
+            s = regexReplace(P_BOTH_ARROWS, "", s);
+        }
+
+        return s;
+    }
+
+    private String checkTags(String s) {
+        Matcher m = P_TAGS.matcher(s);
+
+        final StringBuffer buf = new StringBuffer();
+        while (m.find()) {
+            String replaceStr = m.group(1);
+            replaceStr = processTag(replaceStr);
+            m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
+        }
+        m.appendTail(buf);
+
+        // these get tallied in processTag
+        // (remember to reset before subsequent calls to filter method)
+        final StringBuilder sBuilder = new StringBuilder(buf.toString());
+        for (String key : vTagCounts.keySet()) {
+            for (int ii = 0; ii < vTagCounts.get(key); ii++) {
+                sBuilder.append("</").append(key).append(">");
+            }
+        }
+        s = sBuilder.toString();
+
+        return s;
+    }
+
+    private String processRemoveBlanks(final String s) {
+        String result = s;
+        for (String tag : vRemoveBlanks) {
+            if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) {
+                P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">"));
+            }
+            result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
+            if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) {
+                P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
+            }
+            result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
+        }
+
+        return result;
+    }
+
+    private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) {
+        Matcher m = regex_pattern.matcher(s);
+        return m.replaceAll(replacement);
+    }
+
+    private String processTag(final String s) {
+        // ending tags
+        Matcher m = P_END_TAG.matcher(s);
+        if (m.find()) {
+            final String name = m.group(1).toLowerCase();
+            if (allowed(name)) {
+                if (!inArray(name, vSelfClosingTags)) {
+                    if (vTagCounts.containsKey(name)) {
+                        vTagCounts.put(name, vTagCounts.get(name) - 1);
+                        return "</" + name + ">";
+                    }
+                }
+            }
+        }
+
+        // starting tags
+        m = P_START_TAG.matcher(s);
+        if (m.find()) {
+            final String name = m.group(1).toLowerCase();
+            final String body = m.group(2);
+            String ending = m.group(3);
+
+            // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
+            if (allowed(name)) {
+                final StringBuilder params = new StringBuilder();
+
+                final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
+                final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
+                final List<String> paramNames = new ArrayList<>();
+                final List<String> paramValues = new ArrayList<>();
+                while (m2.find()) {
+                    paramNames.add(m2.group(1)); // ([a-z0-9]+)
+                    paramValues.add(m2.group(3)); // (.*?)
+                }
+                while (m3.find()) {
+                    paramNames.add(m3.group(1)); // ([a-z0-9]+)
+                    paramValues.add(m3.group(3)); // ([^\"\\s']+)
+                }
+
+                String paramName, paramValue;
+                for (int ii = 0; ii < paramNames.size(); ii++) {
+                    paramName = paramNames.get(ii).toLowerCase();
+                    paramValue = paramValues.get(ii);
+
+                    // debug( "paramName='" + paramName + "'" );
+                    // debug( "paramValue='" + paramValue + "'" );
+                    // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
+
+                    if (allowedAttribute(name, paramName)) {
+                        if (inArray(paramName, vProtocolAtts)) {
+                            paramValue = processParamProtocol(paramValue);
+                        }
+                        params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\"");
+                    }
+                }
+
+                if (inArray(name, vSelfClosingTags)) {
+                    ending = " /";
+                }
+
+                if (inArray(name, vNeedClosingTags)) {
+                    ending = "";
+                }
+
+                if (ending == null || ending.length() < 1) {
+                    if (vTagCounts.containsKey(name)) {
+                        vTagCounts.put(name, vTagCounts.get(name) + 1);
+                    } else {
+                        vTagCounts.put(name, 1);
+                    }
+                } else {
+                    ending = " /";
+                }
+                return "<" + name + params + ending + ">";
+            } else {
+                return "";
+            }
+        }
+
+        // comments
+        m = P_COMMENT.matcher(s);
+        if (!stripComment && m.find()) {
+            return "<" + m.group() + ">";
+        }
+
+        return "";
+    }
+
+    private String processParamProtocol(String s) {
+        s = decodeEntities(s);
+        final Matcher m = P_PROTOCOL.matcher(s);
+        if (m.find()) {
+            final String protocol = m.group(1);
+            if (!inArray(protocol, vAllowedProtocols)) {
+                // bad protocol, turn into local anchor link instead
+                s = "#" + s.substring(protocol.length() + 1);
+                if (s.startsWith("#//")) {
+                    s = "#" + s.substring(3);
+                }
+            }
+        }
+
+        return s;
+    }
+
+    private String decodeEntities(String s) {
+        StringBuffer buf = new StringBuffer();
+
+        Matcher m = P_ENTITY.matcher(s);
+        while (m.find()) {
+            final String match = m.group(1);
+            final int decimal = Integer.decode(match).intValue();
+            m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+        }
+        m.appendTail(buf);
+        s = buf.toString();
+
+        buf = new StringBuffer();
+        m = P_ENTITY_UNICODE.matcher(s);
+        while (m.find()) {
+            final String match = m.group(1);
+            final int decimal = Integer.valueOf(match, 16).intValue();
+            m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+        }
+        m.appendTail(buf);
+        s = buf.toString();
+
+        buf = new StringBuffer();
+        m = P_ENCODE.matcher(s);
+        while (m.find()) {
+            final String match = m.group(1);
+            final int decimal = Integer.valueOf(match, 16).intValue();
+            m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+        }
+        m.appendTail(buf);
+        s = buf.toString();
+
+        s = validateEntities(s);
+        return s;
+    }
+
+    private String validateEntities(final String s) {
+        StringBuffer buf = new StringBuffer();
+
+        // validate entities throughout the string
+        Matcher m = P_VALID_ENTITIES.matcher(s);
+        while (m.find()) {
+            final String one = m.group(1); // ([^&;]*)
+            final String two = m.group(2); // (?=(;|&|$))
+            m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
+        }
+        m.appendTail(buf);
+
+        return encodeQuotes(buf.toString());
+    }
+
+    private String encodeQuotes(final String s) {
+        if (encodeQuotes) {
+            StringBuffer buf = new StringBuffer();
+            Matcher m = P_VALID_QUOTES.matcher(s);
+            while (m.find()) {
+                final String one = m.group(1); // (>|^)
+                final String two = m.group(2); // ([^<]+?)
+                final String three = m.group(3); // (<|$)
+                // 不替换双引号为&quot;,防止json格式无效 regexReplace(P_QUOTE, "&quot;", two)
+                m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three));
+            }
+            m.appendTail(buf);
+            return buf.toString();
+        } else {
+            return s;
+        }
+    }
+
+    private String checkEntity(final String preamble, final String term) {
+
+        return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&amp;" + preamble;
+    }
+
+    private boolean isValidEntity(final String entity) {
+        return inArray(entity, vAllowedEntities);
+    }
+
+    private static boolean inArray(final String s, final String[] array) {
+        for (String item : array) {
+            if (item != null && item.equals(s)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean allowed(final String name) {
+        return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
+    }
+
+    private boolean allowedAttribute(final String name, final String paramName) {
+        return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
+    }
+}

+ 36 - 1
himeeter-common/src/main/java/com/lij/himeeter/common/utils/http/HttpHelper.java

@@ -1,12 +1,23 @@
 package com.lij.himeeter.common.utils.http;
 
+import jakarta.servlet.ServletRequest;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
 /**
  * <pre>
  *
  * 文件名 :  HttpHelper
  * 创建时间 : 2024年12月30日 15:08
  *
- * 文件描述 : 无
+ * 文件描述 : 通用http工具封装
  *
  * </pre>
  *
@@ -15,4 +26,28 @@ package com.lij.himeeter.common.utils.http;
  */
 
 public class HttpHelper {
+    private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);
+
+    public static String getBodyString(ServletRequest request) {
+        StringBuilder sb = new StringBuilder();
+        BufferedReader reader = null;
+        try (InputStream inputStream = request.getInputStream()) {
+            reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+            String line = "";
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+        } catch (IOException e) {
+            LOGGER.warn("getBodyString出现问题!");
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (IOException e) {
+                    LOGGER.error(ExceptionUtils.getMessage(e));
+                }
+            }
+        }
+        return sb.toString();
+    }
 }

+ 7 - 14
himeeter-common/src/main/java/com/lij/himeeter/common/utils/ip/AddressUtils.java

@@ -15,7 +15,7 @@ import org.slf4j.LoggerFactory;
  * 文件名 :  AddressUtils
  * 创建时间 : 2024年12月30日 15:04
  *
- * 文件描述 : 
+ * 文件描述 : 获取地址类
  *
  * </pre>
  *
@@ -32,20 +32,15 @@ public class AddressUtils {
     // 未知地址
     public static final String UNKNOWN = "XX XX";
 
-    public static String getRealAddressByIP(String ip)
-    {
+    public static String getRealAddressByIP(String ip) {
         // 内网不查询
-        if (IpUtils.internalIp(ip))
-        {
+        if (IpUtils.internalIp(ip)) {
             return "内网IP";
         }
-        if (HimeeterConfig.isAddressEnabled())
-        {
-            try
-            {
+        if (HimeeterConfig.isAddressEnabled()) {
+            try {
                 String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK);
-                if (StringUtils.isEmpty(rspStr))
-                {
+                if (StringUtils.isEmpty(rspStr)) {
                     log.error("获取地理位置异常 {}", ip);
                     return UNKNOWN;
                 }
@@ -53,9 +48,7 @@ public class AddressUtils {
                 String region = obj.getString("pro");
                 String city = obj.getString("city");
                 return String.format("%s %s", region, city);
-            }
-            catch (Exception e)
-            {
+            } catch (Exception e) {
                 log.error("获取地理位置异常 {}", ip);
             }
         }

+ 1530 - 2
himeeter-common/src/main/java/com/lij/himeeter/common/utils/poi/ExcelUtil.java

@@ -1,12 +1,54 @@
 package com.lij.himeeter.common.utils.poi;
 
+import com.lij.himeeter.common.annotation.Excel;
+import com.lij.himeeter.common.annotation.Excel.ColumnType;
+import com.lij.himeeter.common.annotation.Excel.Type;
+import com.lij.himeeter.common.annotation.Excels;
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.core.text.Convert;
+import com.lij.himeeter.common.exception.UtilException;
+import com.lij.himeeter.common.utils.DateUtils;
+import com.lij.himeeter.common.utils.DictUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.file.FileTypeUtils;
+import com.lij.himeeter.common.utils.file.FileUtils;
+import com.lij.himeeter.common.utils.file.ImageUtils;
+import com.lij.himeeter.common.utils.reflect.ReflectUtils;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.poi.hssf.usermodel.*;
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.apache.poi.xssf.usermodel.*;
+import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
 /**
  * <pre>
  *
  * 文件名 :  ExcelUtil
  * 创建时间 : 2024年12月25日 17:15
  *
- * 文件描述 : 无
+ * 文件描述 : Excel相关处理
  *
  * </pre>
  *
@@ -14,5 +56,1491 @@ package com.lij.himeeter.common.utils.poi;
  * @version 1.0.0
  */
 
-public class ExcelUtil {
+public class ExcelUtil<T> {
+    private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);
+
+    public static final String FORMULA_REGEX_STR = "=|-|\\+|@";
+
+    public static final String[] FORMULA_STR = {"=", "-", "+", "@"};
+
+    /**
+     * 用于dictType属性数据存储,避免重复查缓存
+     */
+    public Map<String, String> sysDictMap = new HashMap<String, String>();
+
+    /**
+     * Excel sheet最大行数,默认65536
+     */
+    public static final int sheetSize = 65536;
+
+    /**
+     * 工作表名称
+     */
+    private String sheetName;
+
+    /**
+     * 导出类型(EXPORT:导出数据;IMPORT:导入模板)
+     */
+    private Type type;
+
+    /**
+     * 工作薄对象
+     */
+    private Workbook wb;
+
+    /**
+     * 工作表对象
+     */
+    private Sheet sheet;
+
+    /**
+     * 样式列表
+     */
+    private Map<String, CellStyle> styles;
+
+    /**
+     * 导入导出数据列表
+     */
+    private List<T> list;
+
+    /**
+     * 注解列表
+     */
+    private List<Object[]> fields;
+
+    /**
+     * 当前行号
+     */
+    private int rownum;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 最大高度
+     */
+    private short maxHeight;
+
+    /**
+     * 合并后最后行数
+     */
+    private int subMergedLastRowNum = 0;
+
+    /**
+     * 合并后开始行数
+     */
+    private int subMergedFirstRowNum = 1;
+
+    /**
+     * 对象的子列表方法
+     */
+    private Method subMethod;
+
+    /**
+     * 对象的子列表属性
+     */
+    private List<Field> subFields;
+
+    /**
+     * 统计列表
+     */
+    private Map<Integer, Double> statistics = new HashMap<Integer, Double>();
+
+    /**
+     * 数字格式
+     */
+    private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");
+
+    /**
+     * 实体对象
+     */
+    public Class<T> clazz;
+
+    /**
+     * 需要显示列属性
+     */
+    public String[] includeFields;
+
+    /**
+     * 需要排除列属性
+     */
+    public String[] excludeFields;
+
+    public ExcelUtil(Class<T> clazz) {
+        this.clazz = clazz;
+    }
+
+    /**
+     * 仅在Excel中显示列属性
+     *
+     * @param fields 列属性名 示例[单个"name"/多个"id","name"]
+     */
+    public void showColumn(String... fields) {
+        this.includeFields = fields;
+    }
+
+    /**
+     * 隐藏Excel中列属性
+     *
+     * @param fields 列属性名 示例[单个"name"/多个"id","name"]
+     */
+    public void hideColumn(String... fields) {
+        this.excludeFields = fields;
+    }
+
+    public void init(List<T> list, String sheetName, String title, Type type) {
+        if (list == null) {
+            list = new ArrayList<T>();
+        }
+        this.list = list;
+        this.sheetName = sheetName;
+        this.type = type;
+        this.title = title;
+        createExcelField();
+        createWorkbook();
+        createTitle();
+        createSubHead();
+    }
+
+    /**
+     * 创建excel第一行标题
+     */
+    public void createTitle() {
+        if (StringUtils.isNotEmpty(title)) {
+            int titleLastCol = this.fields.size() - 1;
+            if (isSubList()) {
+                titleLastCol = titleLastCol + subFields.size() - 1;
+            }
+            Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
+            titleRow.setHeightInPoints(30);
+            Cell titleCell = titleRow.createCell(0);
+            titleCell.setCellStyle(styles.get("title"));
+            titleCell.setCellValue(title);
+            sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), 0, titleLastCol));
+        }
+    }
+
+    /**
+     * 创建对象的子列表名称
+     */
+    public void createSubHead() {
+        if (isSubList()) {
+            Row subRow = sheet.createRow(rownum);
+            int column = 0;
+            int subFieldSize = subFields != null ? subFields.size() : 0;
+            for (Object[] objects : fields) {
+                Field field = (Field) objects[0];
+                Excel attr = (Excel) objects[1];
+                if (Collection.class.isAssignableFrom(field.getType())) {
+                    Cell cell = subRow.createCell(column);
+                    cell.setCellValue(attr.name());
+                    cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+                    if (subFieldSize > 1) {
+                        CellRangeAddress cellAddress = new CellRangeAddress(rownum, rownum, column, column + subFieldSize - 1);
+                        sheet.addMergedRegion(cellAddress);
+                    }
+                    column += subFieldSize;
+                } else {
+                    Cell cell = subRow.createCell(column++);
+                    cell.setCellValue(attr.name());
+                    cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+                }
+            }
+            rownum++;
+        }
+    }
+
+    /**
+     * 对excel表单默认第一个索引名转换成list
+     *
+     * @param is 输入流
+     * @return 转换后集合
+     */
+    public List<T> importExcel(InputStream is) {
+        return importExcel(is, 0);
+    }
+
+    /**
+     * 对excel表单默认第一个索引名转换成list
+     *
+     * @param is       输入流
+     * @param titleNum 标题占用行数
+     * @return 转换后集合
+     */
+    public List<T> importExcel(InputStream is, int titleNum) {
+        List<T> list = null;
+        try {
+            list = importExcel(StringUtils.EMPTY, is, titleNum);
+        } catch (Exception e) {
+            log.error("导入Excel异常{}", e.getMessage());
+            throw new UtilException(e.getMessage());
+        } finally {
+            IOUtils.closeQuietly(is);
+        }
+        return list;
+    }
+
+    /**
+     * 对excel表单指定表格索引名转换成list
+     *
+     * @param sheetName 表格索引名
+     * @param titleNum  标题占用行数
+     * @param is        输入流
+     * @return 转换后集合
+     */
+    public List<T> importExcel(String sheetName, InputStream is, int titleNum) throws Exception {
+        this.type = Type.IMPORT;
+        this.wb = WorkbookFactory.create(is);
+        List<T> list = new ArrayList<T>();
+        // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
+        Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
+        if (sheet == null) {
+            throw new IOException("文件sheet不存在");
+        }
+        boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook);
+        Map<String, PictureData> pictures;
+        if (isXSSFWorkbook) {
+            pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb);
+        } else {
+            pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb);
+        }
+        // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
+        int rows = sheet.getLastRowNum();
+        if (rows > 0) {
+            // 定义一个map用于存放excel列的序号和field.
+            Map<String, Integer> cellMap = new HashMap<String, Integer>();
+            // 获取表头
+            Row heard = sheet.getRow(titleNum);
+            for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) {
+                Cell cell = heard.getCell(i);
+                if (StringUtils.isNotNull(cell)) {
+                    String value = this.getCellValue(heard, i).toString();
+                    cellMap.put(value, i);
+                } else {
+                    cellMap.put(null, i);
+                }
+            }
+            // 有数据时才处理 得到类的所有field.
+            List<Object[]> fields = this.getFields();
+            Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
+            for (Object[] objects : fields) {
+                Excel attr = (Excel) objects[1];
+                Integer column = cellMap.get(attr.name());
+                if (column != null) {
+                    fieldsMap.put(column, objects);
+                }
+            }
+            for (int i = titleNum + 1; i <= rows; i++) {
+                // 从第2行开始取数据,默认第一行是表头.
+                Row row = sheet.getRow(i);
+                // 判断当前行是否是空行
+                if (isRowEmpty(row)) {
+                    continue;
+                }
+                T entity = null;
+                for (Map.Entry<Integer, Object[]> entry : fieldsMap.entrySet()) {
+                    Object val = this.getCellValue(row, entry.getKey());
+
+                    // 如果不存在实例则新建.
+                    entity = (entity == null ? clazz.getDeclaredConstructor().newInstance() : entity);
+                    // 从map中得到对应列的field.
+                    Field field = (Field) entry.getValue()[0];
+                    Excel attr = (Excel) entry.getValue()[1];
+                    // 取得类型,并根据对象类型设置值.
+                    Class<?> fieldType = field.getType();
+                    if (String.class == fieldType) {
+                        String s = Convert.toStr(val);
+                        if (StringUtils.endsWith(s, ".0")) {
+                            val = StringUtils.substringBefore(s, ".0");
+                        } else {
+                            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
+                            if (StringUtils.isNotEmpty(dateFormat)) {
+                                val = parseDateToStr(dateFormat, val);
+                            } else {
+                                val = Convert.toStr(val);
+                            }
+                        }
+                    } else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) {
+                        val = Convert.toInt(val);
+                    } else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) {
+                        val = Convert.toLong(val);
+                    } else if (Double.TYPE == fieldType || Double.class == fieldType) {
+                        val = Convert.toDouble(val);
+                    } else if (Float.TYPE == fieldType || Float.class == fieldType) {
+                        val = Convert.toFloat(val);
+                    } else if (BigDecimal.class == fieldType) {
+                        val = Convert.toBigDecimal(val);
+                    } else if (Date.class == fieldType) {
+                        if (val instanceof String) {
+                            val = DateUtils.parseDate(val);
+                        } else if (val instanceof Double) {
+                            val = DateUtil.getJavaDate((Double) val);
+                        }
+                    } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
+                        val = Convert.toBool(val, false);
+                    }
+                    if (StringUtils.isNotNull(fieldType)) {
+                        String propertyName = field.getName();
+                        if (StringUtils.isNotEmpty(attr.targetAttr())) {
+                            propertyName = field.getName() + "." + attr.targetAttr();
+                        }
+                        if (StringUtils.isNotEmpty(attr.readConverterExp())) {
+                            val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
+                        } else if (StringUtils.isNotEmpty(attr.dictType())) {
+                            if (!sysDictMap.containsKey(attr.dictType() + val)) {
+                                String dictValue = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator());
+                                sysDictMap.put(attr.dictType() + val, dictValue);
+                            }
+                            val = sysDictMap.get(attr.dictType() + val);
+                        } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) {
+                            val = dataFormatHandlerAdapter(val, attr, null);
+                        } else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) {
+                            PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey());
+                            if (image == null) {
+                                val = "";
+                            } else {
+                                byte[] data = image.getData();
+                                val = FileUtils.writeImportBytes(data);
+                            }
+                        }
+                        ReflectUtils.invokeSetter(entity, propertyName, val);
+                    }
+                }
+                list.add(entity);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @return 结果
+     */
+    public AjaxResult exportExcel(List<T> list, String sheetName) {
+        return exportExcel(list, sheetName, StringUtils.EMPTY);
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param title     标题
+     * @return 结果
+     */
+    public AjaxResult exportExcel(List<T> list, String sheetName, String title) {
+        this.init(list, sheetName, title, Type.EXPORT);
+        return exportExcel();
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param response  返回数据
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @return 结果
+     */
+    public void exportExcel(HttpServletResponse response, List<T> list, String sheetName) {
+        exportExcel(response, list, sheetName, StringUtils.EMPTY);
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param response  返回数据
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param title     标题
+     * @return 结果
+     */
+    public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String title) {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        this.init(list, sheetName, title, Type.EXPORT);
+        exportExcel(response);
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param sheetName 工作表的名称
+     * @return 结果
+     */
+    public AjaxResult importTemplateExcel(String sheetName) {
+        return importTemplateExcel(sheetName, StringUtils.EMPTY);
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param sheetName 工作表的名称
+     * @param title     标题
+     * @return 结果
+     */
+    public AjaxResult importTemplateExcel(String sheetName, String title) {
+        this.init(null, sheetName, title, Type.IMPORT);
+        return exportExcel();
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param sheetName 工作表的名称
+     * @return 结果
+     */
+    public void importTemplateExcel(HttpServletResponse response, String sheetName) {
+        importTemplateExcel(response, sheetName, StringUtils.EMPTY);
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param sheetName 工作表的名称
+     * @param title     标题
+     * @return 结果
+     */
+    public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        this.init(null, sheetName, title, Type.IMPORT);
+        exportExcel(response);
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @return 结果
+     */
+    public void exportExcel(HttpServletResponse response) {
+        try {
+            writeSheet();
+            wb.write(response.getOutputStream());
+        } catch (Exception e) {
+            log.error("导出Excel异常{}", e.getMessage());
+        } finally {
+            IOUtils.closeQuietly(wb);
+        }
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @return 结果
+     */
+    public AjaxResult exportExcel() {
+        OutputStream out = null;
+        try {
+            writeSheet();
+            String filename = encodingFilename(sheetName);
+            out = new FileOutputStream(getAbsoluteFile(filename));
+            wb.write(out);
+            return AjaxResult.success(filename);
+        } catch (Exception e) {
+            log.error("导出Excel异常{}", e.getMessage());
+            throw new UtilException("导出Excel失败,请联系网站管理员!");
+        } finally {
+            IOUtils.closeQuietly(wb);
+            IOUtils.closeQuietly(out);
+        }
+    }
+
+    /**
+     * 创建写入数据到Sheet
+     */
+    public void writeSheet() {
+        // 取出一共有多少个sheet.
+        int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
+        for (int index = 0; index < sheetNo; index++) {
+            createSheet(sheetNo, index);
+
+            // 产生一行
+            Row row = sheet.createRow(rownum);
+            int column = 0;
+            // 写入各个字段的列头名称
+            for (Object[] os : fields) {
+                Field field = (Field) os[0];
+                Excel excel = (Excel) os[1];
+                if (Collection.class.isAssignableFrom(field.getType())) {
+                    for (Field subField : subFields) {
+                        Excel subExcel = subField.getAnnotation(Excel.class);
+                        this.createHeadCell(subExcel, row, column++);
+                    }
+                } else {
+                    this.createHeadCell(excel, row, column++);
+                }
+            }
+            if (Type.EXPORT.equals(type)) {
+                fillExcelData(index, row);
+                addStatisticsRow();
+            }
+        }
+    }
+
+    /**
+     * 填充excel数据
+     *
+     * @param index 序号
+     * @param row   单元格行
+     */
+    @SuppressWarnings("unchecked")
+    public void fillExcelData(int index, Row row) {
+        int startNo = index * sheetSize;
+        int endNo = Math.min(startNo + sheetSize, list.size());
+        int currentRowNum = rownum + 1; // 从标题行后开始
+
+        for (int i = startNo; i < endNo; i++) {
+            row = sheet.createRow(currentRowNum);
+            T vo = (T) list.get(i);
+            int column = 0;
+            int maxSubListSize = getCurrentMaxSubListSize(vo);
+            for (Object[] os : fields) {
+                Field field = (Field) os[0];
+                Excel excel = (Excel) os[1];
+                if (Collection.class.isAssignableFrom(field.getType())) {
+                    try {
+                        Collection<?> subList = (Collection<?>) getTargetValue(vo, field, excel);
+                        if (subList != null && !subList.isEmpty()) {
+                            int subIndex = 0;
+                            for (Object subVo : subList) {
+                                Row subRow = sheet.getRow(currentRowNum + subIndex);
+                                if (subRow == null) {
+                                    subRow = sheet.createRow(currentRowNum + subIndex);
+                                }
+
+                                int subColumn = column;
+                                for (Field subField : subFields) {
+                                    Excel subExcel = subField.getAnnotation(Excel.class);
+                                    addCell(subExcel, subRow, (T) subVo, subField, subColumn++);
+                                }
+                                subIndex++;
+                            }
+                            column += subFields.size();
+                        }
+                    } catch (Exception e) {
+                        log.error("填充集合数据失败", e);
+                    }
+                } else {
+                    // 创建单元格并设置值
+                    addCell(excel, row, vo, field, column);
+                    if (maxSubListSize > 1 && excel.needMerge()) {
+                        sheet.addMergedRegion(new CellRangeAddress(currentRowNum, currentRowNum + maxSubListSize - 1, column, column));
+                    }
+                    column++;
+                }
+            }
+            currentRowNum += maxSubListSize;
+        }
+    }
+
+    /**
+     * 获取子列表最大数
+     */
+    private int getCurrentMaxSubListSize(T vo) {
+        int maxSubListSize = 1;
+        for (Object[] os : fields) {
+            Field field = (Field) os[0];
+            if (Collection.class.isAssignableFrom(field.getType())) {
+                try {
+                    Collection<?> subList = (Collection<?>) getTargetValue(vo, field, (Excel) os[1]);
+                    if (subList != null && !subList.isEmpty()) {
+                        maxSubListSize = Math.max(maxSubListSize, subList.size());
+                    }
+                } catch (Exception e) {
+                    log.error("获取集合大小失败", e);
+                }
+            }
+        }
+        return maxSubListSize;
+    }
+
+    /**
+     * 创建表格样式
+     *
+     * @param wb 工作薄对象
+     * @return 样式列表
+     */
+    private Map<String, CellStyle> createStyles(Workbook wb) {
+        // 写入各条记录,每条记录对应excel表中的一行
+        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
+        CellStyle style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        Font titleFont = wb.createFont();
+        titleFont.setFontName("Arial");
+        titleFont.setFontHeightInPoints((short) 16);
+        titleFont.setBold(true);
+        style.setFont(titleFont);
+        DataFormat dataFormat = wb.createDataFormat();
+        style.setDataFormat(dataFormat.getFormat("@"));
+        styles.put("title", style);
+
+        style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderRight(BorderStyle.THIN);
+        style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderLeft(BorderStyle.THIN);
+        style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderTop(BorderStyle.THIN);
+        style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        Font dataFont = wb.createFont();
+        dataFont.setFontName("Arial");
+        dataFont.setFontHeightInPoints((short) 10);
+        style.setFont(dataFont);
+        styles.put("data", style);
+
+        style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        Font totalFont = wb.createFont();
+        totalFont.setFontName("Arial");
+        totalFont.setFontHeightInPoints((short) 10);
+        style.setFont(totalFont);
+        styles.put("total", style);
+
+        styles.putAll(annotationHeaderStyles(wb, styles));
+
+        styles.putAll(annotationDataStyles(wb));
+
+        return styles;
+    }
+
+    /**
+     * 根据Excel注解创建表格头样式
+     *
+     * @param wb 工作薄对象
+     * @return 自定义样式列表
+     */
+    private Map<String, CellStyle> annotationHeaderStyles(Workbook wb, Map<String, CellStyle> styles) {
+        Map<String, CellStyle> headerStyles = new HashMap<String, CellStyle>();
+        for (Object[] os : fields) {
+            Excel excel = (Excel) os[1];
+            String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor());
+            if (!headerStyles.containsKey(key)) {
+                CellStyle style = wb.createCellStyle();
+                style.cloneStyleFrom(styles.get("data"));
+                style.setAlignment(HorizontalAlignment.CENTER);
+                style.setVerticalAlignment(VerticalAlignment.CENTER);
+                style.setFillForegroundColor(excel.headerBackgroundColor().index);
+                style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+                Font headerFont = wb.createFont();
+                headerFont.setFontName("Arial");
+                headerFont.setFontHeightInPoints((short) 10);
+                headerFont.setBold(true);
+                headerFont.setColor(excel.headerColor().index);
+                style.setFont(headerFont);
+                // 设置表格头单元格文本形式
+                DataFormat dataFormat = wb.createDataFormat();
+                style.setDataFormat(dataFormat.getFormat("@"));
+                headerStyles.put(key, style);
+            }
+        }
+        return headerStyles;
+    }
+
+    /**
+     * 根据Excel注解创建表格列样式
+     *
+     * @param wb 工作薄对象
+     * @return 自定义样式列表
+     */
+    private Map<String, CellStyle> annotationDataStyles(Workbook wb) {
+        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
+        for (Object[] os : fields) {
+            Field field = (Field) os[0];
+            Excel excel = (Excel) os[1];
+            if (Collection.class.isAssignableFrom(field.getType())) {
+                ParameterizedType pt = (ParameterizedType) field.getGenericType();
+                Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
+                List<Field> subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
+                for (Field subField : subFields) {
+                    Excel subExcel = subField.getAnnotation(Excel.class);
+                    annotationDataStyles(styles, subField, subExcel);
+                }
+            } else {
+                annotationDataStyles(styles, field, excel);
+            }
+        }
+        return styles;
+    }
+
+    /**
+     * 根据Excel注解创建表格列样式
+     *
+     * @param styles 自定义样式列表
+     * @param field  属性列信息
+     * @param excel  注解信息
+     */
+    public void annotationDataStyles(Map<String, CellStyle> styles, Field field, Excel excel) {
+        String key = StringUtils.format("data_{}_{}_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor(), excel.cellType(), excel.wrapText());
+        if (!styles.containsKey(key)) {
+            CellStyle style = wb.createCellStyle();
+            style.setAlignment(excel.align());
+            style.setVerticalAlignment(VerticalAlignment.CENTER);
+            style.setBorderRight(BorderStyle.THIN);
+            style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+            style.setBorderLeft(BorderStyle.THIN);
+            style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+            style.setBorderTop(BorderStyle.THIN);
+            style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+            style.setBorderBottom(BorderStyle.THIN);
+            style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+            style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            style.setFillForegroundColor(excel.backgroundColor().getIndex());
+            style.setWrapText(excel.wrapText());
+            Font dataFont = wb.createFont();
+            dataFont.setFontName("Arial");
+            dataFont.setFontHeightInPoints((short) 10);
+            dataFont.setColor(excel.color().index);
+            style.setFont(dataFont);
+            if (ColumnType.TEXT == excel.cellType()) {
+                DataFormat dataFormat = wb.createDataFormat();
+                style.setDataFormat(dataFormat.getFormat("@"));
+            }
+            styles.put(key, style);
+        }
+    }
+
+    /**
+     * 创建单元格
+     */
+    public Cell createHeadCell(Excel attr, Row row, int column) {
+        // 创建列
+        Cell cell = row.createCell(column);
+        // 写入列信息
+        cell.setCellValue(attr.name());
+        setDataValidation(attr, row, column);
+        cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
+        if (isSubList()) {
+            // 填充默认样式,防止合并单元格样式失效
+            sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText())));
+            if (attr.needMerge()) {
+                sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column));
+            }
+        }
+        return cell;
+    }
+
+    /**
+     * 设置单元格信息
+     *
+     * @param value 单元格值
+     * @param attr  注解相关
+     * @param cell  单元格信息
+     */
+    public void setCellVo(Object value, Excel attr, Cell cell) {
+        if (ColumnType.STRING == attr.cellType() || ColumnType.TEXT == attr.cellType()) {
+            String cellValue = Convert.toStr(value);
+            // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。
+            if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) {
+                cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0");
+            }
+            if (value instanceof Collection && StringUtils.equals("[]", cellValue)) {
+                cellValue = StringUtils.EMPTY;
+            }
+            cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix());
+        } else if (ColumnType.NUMERIC == attr.cellType()) {
+            if (StringUtils.isNotNull(value)) {
+                cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value));
+            }
+        } else if (ColumnType.IMAGE == attr.cellType()) {
+            ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1);
+            String imagePath = Convert.toStr(value);
+            if (StringUtils.isNotEmpty(imagePath)) {
+                byte[] data = ImageUtils.getImage(imagePath);
+                getDrawingPatriarch(cell.getSheet()).createPicture(anchor,
+                        cell.getSheet().getWorkbook().addPicture(data, getImageType(data)));
+            }
+        }
+    }
+
+    /**
+     * 获取画布
+     */
+    public static Drawing<?> getDrawingPatriarch(Sheet sheet) {
+        if (sheet.getDrawingPatriarch() == null) {
+            sheet.createDrawingPatriarch();
+        }
+        return sheet.getDrawingPatriarch();
+    }
+
+    /**
+     * 获取图片类型,设置图片插入类型
+     */
+    public int getImageType(byte[] value) {
+        String type = FileTypeUtils.getFileExtendName(value);
+        if ("JPG".equalsIgnoreCase(type)) {
+            return Workbook.PICTURE_TYPE_JPEG;
+        } else if ("PNG".equalsIgnoreCase(type)) {
+            return Workbook.PICTURE_TYPE_PNG;
+        }
+        return Workbook.PICTURE_TYPE_JPEG;
+    }
+
+    /**
+     * 创建表格样式
+     */
+    public void setDataValidation(Excel attr, Row row, int column) {
+        if (attr.name().indexOf("注:") >= 0) {
+            sheet.setColumnWidth(column, 6000);
+        } else {
+            // 设置列宽
+            sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256));
+        }
+        if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0 || attr.comboReadDict()) {
+            String[] comboArray = attr.combo();
+            if (attr.comboReadDict()) {
+                if (!sysDictMap.containsKey("combo_" + attr.dictType())) {
+                    String labels = DictUtils.getDictLabels(attr.dictType());
+                    sysDictMap.put("combo_" + attr.dictType(), labels);
+                }
+                String val = sysDictMap.get("combo_" + attr.dictType());
+                comboArray = StringUtils.split(val, DictUtils.SEPARATOR);
+            }
+            if (comboArray.length > 15 || StringUtils.join(comboArray).length() > 255) {
+                // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到
+                setXSSFValidationWithHidden(sheet, comboArray, attr.prompt(), 1, 100, column, column);
+            } else {
+                // 提示信息或只能选择不能输入的列内容.
+                setPromptOrValidation(sheet, comboArray, attr.prompt(), 1, 100, column, column);
+            }
+        }
+    }
+
+    /**
+     * 添加单元格
+     */
+    public Cell addCell(Excel attr, Row row, T vo, Field field, int column) {
+        Cell cell = null;
+        try {
+            // 设置行高
+            row.setHeight(maxHeight);
+            // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
+            if (attr.isExport()) {
+                // 创建cell
+                cell = row.createCell(column);
+                if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) {
+                    if (subMergedLastRowNum >= subMergedFirstRowNum) {
+                        sheet.addMergedRegion(new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column));
+                    }
+                }
+                cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText())));
+
+                // 用于读取对象中的属性
+                Object value = getTargetValue(vo, field, attr);
+                String dateFormat = attr.dateFormat();
+                String readConverterExp = attr.readConverterExp();
+                String separator = attr.separator();
+                String dictType = attr.dictType();
+                if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) {
+                    cell.setCellValue(parseDateToStr(dateFormat, value));
+                } else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) {
+                    cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator));
+                } else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) {
+                    if (!sysDictMap.containsKey(dictType + value)) {
+                        String lable = convertDictByExp(Convert.toStr(value), dictType, separator);
+                        sysDictMap.put(dictType + value, lable);
+                    }
+                    cell.setCellValue(sysDictMap.get(dictType + value));
+                } else if (value instanceof BigDecimal && -1 != attr.scale()) {
+                    cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue());
+                } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) {
+                    cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell));
+                } else {
+                    // 设置列类型
+                    setCellVo(value, attr, cell);
+                }
+                addStatisticsData(column, Convert.toStr(value), attr);
+            }
+        } catch (Exception e) {
+            log.error("导出Excel失败{}", e);
+        }
+        return cell;
+    }
+
+    /**
+     * 设置 POI XSSFSheet 单元格提示或选择框
+     *
+     * @param sheet         表单
+     * @param textlist      下拉框显示的内容
+     * @param promptContent 提示内容
+     * @param firstRow      开始行
+     * @param endRow        结束行
+     * @param firstCol      开始列
+     * @param endCol        结束列
+     */
+    public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow,
+                                      int firstCol, int endCol) {
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1");
+        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+        DataValidation dataValidation = helper.createValidation(constraint, regions);
+        if (StringUtils.isNotEmpty(promptContent)) {
+            // 如果设置了提示信息则鼠标放上去提示
+            dataValidation.createPromptBox("", promptContent);
+            dataValidation.setShowPromptBox(true);
+        }
+        // 处理Excel兼容性问题
+        if (dataValidation instanceof XSSFDataValidation) {
+            dataValidation.setSuppressDropDownArrow(true);
+            dataValidation.setShowErrorBox(true);
+        } else {
+            dataValidation.setSuppressDropDownArrow(false);
+        }
+        sheet.addValidationData(dataValidation);
+    }
+
+    /**
+     * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框).
+     *
+     * @param sheet         要设置的sheet.
+     * @param textlist      下拉框显示的内容
+     * @param promptContent 提示内容
+     * @param firstRow      开始行
+     * @param endRow        结束行
+     * @param firstCol      开始列
+     * @param endCol        结束列
+     */
+    public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) {
+        String hideSheetName = "combo_" + firstCol + "_" + endCol;
+        Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据
+        for (int i = 0; i < textlist.length; i++) {
+            hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]);
+        }
+        // 创建名称,可被其他单元格引用
+        Name name = wb.createName();
+        name.setNameName(hideSheetName + "_data");
+        name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length);
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        // 加载下拉列表内容
+        DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data");
+        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+        // 数据有效性对象
+        DataValidation dataValidation = helper.createValidation(constraint, regions);
+        if (StringUtils.isNotEmpty(promptContent)) {
+            // 如果设置了提示信息则鼠标放上去提示
+            dataValidation.createPromptBox("", promptContent);
+            dataValidation.setShowPromptBox(true);
+        }
+        // 处理Excel兼容性问题
+        if (dataValidation instanceof XSSFDataValidation) {
+            dataValidation.setSuppressDropDownArrow(true);
+            dataValidation.setShowErrorBox(true);
+        } else {
+            dataValidation.setSuppressDropDownArrow(false);
+        }
+
+        sheet.addValidationData(dataValidation);
+        // 设置hiddenSheet隐藏
+        wb.setSheetHidden(wb.getSheetIndex(hideSheet), true);
+    }
+
+    /**
+     * 解析导出值 0=男,1=女,2=未知
+     *
+     * @param propertyValue 参数值
+     * @param converterExp  翻译注解
+     * @param separator     分隔符
+     * @return 解析后值
+     */
+    public static String convertByExp(String propertyValue, String converterExp, String separator) {
+        StringBuilder propertyString = new StringBuilder();
+        String[] convertSource = converterExp.split(",");
+        for (String item : convertSource) {
+            String[] itemArray = item.split("=");
+            if (StringUtils.containsAny(propertyValue, separator)) {
+                for (String value : propertyValue.split(separator)) {
+                    if (itemArray[0].equals(value)) {
+                        propertyString.append(itemArray[1] + separator);
+                        break;
+                    }
+                }
+            } else {
+                if (itemArray[0].equals(propertyValue)) {
+                    return itemArray[1];
+                }
+            }
+        }
+        return StringUtils.stripEnd(propertyString.toString(), separator);
+    }
+
+    /**
+     * 反向解析值 男=0,女=1,未知=2
+     *
+     * @param propertyValue 参数值
+     * @param converterExp  翻译注解
+     * @param separator     分隔符
+     * @return 解析后值
+     */
+    public static String reverseByExp(String propertyValue, String converterExp, String separator) {
+        StringBuilder propertyString = new StringBuilder();
+        String[] convertSource = converterExp.split(",");
+        for (String item : convertSource) {
+            String[] itemArray = item.split("=");
+            if (StringUtils.containsAny(propertyValue, separator)) {
+                for (String value : propertyValue.split(separator)) {
+                    if (itemArray[1].equals(value)) {
+                        propertyString.append(itemArray[0] + separator);
+                        break;
+                    }
+                }
+            } else {
+                if (itemArray[1].equals(propertyValue)) {
+                    return itemArray[0];
+                }
+            }
+        }
+        return StringUtils.stripEnd(propertyString.toString(), separator);
+    }
+
+    /**
+     * 解析字典值
+     *
+     * @param dictValue 字典值
+     * @param dictType  字典类型
+     * @param separator 分隔符
+     * @return 字典标签
+     */
+    public static String convertDictByExp(String dictValue, String dictType, String separator) {
+        return DictUtils.getDictLabel(dictType, dictValue, separator);
+    }
+
+    /**
+     * 反向解析值字典值
+     *
+     * @param dictLabel 字典标签
+     * @param dictType  字典类型
+     * @param separator 分隔符
+     * @return 字典值
+     */
+    public static String reverseDictByExp(String dictLabel, String dictType, String separator) {
+        return DictUtils.getDictValue(dictType, dictLabel, separator);
+    }
+
+    /**
+     * 数据处理器
+     *
+     * @param value 数据值
+     * @param excel 数据注解
+     * @return
+     */
+    public String dataFormatHandlerAdapter(Object value, Excel excel, Cell cell) {
+        try {
+            Object instance = excel.handler().getDeclaredConstructor().newInstance();
+            Method formatMethod = excel.handler().getMethod("format", new Class[]{Object.class, String[].class, Cell.class, Workbook.class});
+            value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb);
+        } catch (Exception e) {
+            log.error("不能格式化数据 " + excel.handler(), e.getMessage());
+        }
+        return Convert.toStr(value);
+    }
+
+    /**
+     * 合计统计信息
+     */
+    private void addStatisticsData(Integer index, String text, Excel entity) {
+        if (entity != null && entity.isStatistics()) {
+            Double temp = 0D;
+            if (!statistics.containsKey(index)) {
+                statistics.put(index, temp);
+            }
+            try {
+                temp = Double.valueOf(text);
+            } catch (NumberFormatException e) {
+            }
+            statistics.put(index, statistics.get(index) + temp);
+        }
+    }
+
+    /**
+     * 创建统计行
+     */
+    public void addStatisticsRow() {
+        if (statistics.size() > 0) {
+            Row row = sheet.createRow(sheet.getLastRowNum() + 1);
+            Set<Integer> keys = statistics.keySet();
+            Cell cell = row.createCell(0);
+            cell.setCellStyle(styles.get("total"));
+            cell.setCellValue("合计");
+
+            for (Integer key : keys) {
+                cell = row.createCell(key);
+                cell.setCellStyle(styles.get("total"));
+                cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key)));
+            }
+            statistics.clear();
+        }
+    }
+
+    /**
+     * 编码文件名
+     */
+    public String encodingFilename(String filename) {
+        filename = UUID.randomUUID() + "_" + filename + ".xlsx";
+        return filename;
+    }
+
+    /**
+     * 获取下载路径
+     *
+     * @param filename 文件名称
+     */
+    public String getAbsoluteFile(String filename) {
+        String downloadPath = HimeeterConfig.getDownloadPath() + filename;
+        File desc = new File(downloadPath);
+        if (!desc.getParentFile().exists()) {
+            desc.getParentFile().mkdirs();
+        }
+        return downloadPath;
+    }
+
+    /**
+     * 获取bean中的属性值
+     *
+     * @param vo    实体对象
+     * @param field 字段
+     * @param excel 注解
+     * @return 最终的属性值
+     * @throws Exception
+     */
+    private Object getTargetValue(T vo, Field field, Excel excel) throws Exception {
+        field.setAccessible(true);
+        Object o = field.get(vo);
+        if (StringUtils.isNotEmpty(excel.targetAttr())) {
+            String target = excel.targetAttr();
+            if (target.contains(".")) {
+                String[] targets = target.split("[.]");
+                for (String name : targets) {
+                    o = getValue(o, name);
+                }
+            } else {
+                o = getValue(o, target);
+            }
+        }
+        return o;
+    }
+
+    /**
+     * 以类的属性的get方法方法形式获取值
+     *
+     * @param o
+     * @param name
+     * @return value
+     * @throws Exception
+     */
+    private Object getValue(Object o, String name) throws Exception {
+        if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) {
+            Class<?> clazz = o.getClass();
+            Field field = clazz.getDeclaredField(name);
+            field.setAccessible(true);
+            o = field.get(o);
+        }
+        return o;
+    }
+
+    /**
+     * 得到所有定义字段
+     */
+    private void createExcelField() {
+        this.fields = getFields();
+        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
+        this.maxHeight = getRowHeight();
+    }
+
+    /**
+     * 获取字段注解信息
+     */
+    public List<Object[]> getFields() {
+        List<Object[]> fields = new ArrayList<Object[]>();
+        List<Field> tempFields = new ArrayList<>();
+        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
+        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+        if (StringUtils.isNotEmpty(includeFields)) {
+            for (Field field : tempFields) {
+                if (ArrayUtils.contains(this.includeFields, field.getName()) || field.isAnnotationPresent(Excels.class)) {
+                    addField(fields, field);
+                }
+            }
+        } else if (StringUtils.isNotEmpty(excludeFields)) {
+            for (Field field : tempFields) {
+                if (!ArrayUtils.contains(this.excludeFields, field.getName())) {
+                    addField(fields, field);
+                }
+            }
+        } else {
+            for (Field field : tempFields) {
+                addField(fields, field);
+            }
+        }
+        return fields;
+    }
+
+    /**
+     * 添加字段信息
+     */
+    public void addField(List<Object[]> fields, Field field) {
+        // 单注解
+        if (field.isAnnotationPresent(Excel.class)) {
+            Excel attr = field.getAnnotation(Excel.class);
+            if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) {
+                fields.add(new Object[]{field, attr});
+            }
+            if (Collection.class.isAssignableFrom(field.getType())) {
+                subMethod = getSubMethod(field.getName(), clazz);
+                ParameterizedType pt = (ParameterizedType) field.getGenericType();
+                Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
+                this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
+            }
+        }
+
+        // 多注解
+        if (field.isAnnotationPresent(Excels.class)) {
+            Excels attrs = field.getAnnotation(Excels.class);
+            Excel[] excels = attrs.value();
+            for (Excel attr : excels) {
+                if (StringUtils.isNotEmpty(includeFields)) {
+                    if (ArrayUtils.contains(this.includeFields, field.getName() + "." + attr.targetAttr())
+                            && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) {
+                        fields.add(new Object[]{field, attr});
+                    }
+                } else {
+                    if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr())
+                            && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) {
+                        fields.add(new Object[]{field, attr});
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 根据注解获取最大行高
+     */
+    public short getRowHeight() {
+        double maxHeight = 0;
+        for (Object[] os : this.fields) {
+            Excel excel = (Excel) os[1];
+            maxHeight = Math.max(maxHeight, excel.height());
+        }
+        return (short) (maxHeight * 20);
+    }
+
+    /**
+     * 创建一个工作簿
+     */
+    public void createWorkbook() {
+        this.wb = new SXSSFWorkbook(500);
+        this.sheet = wb.createSheet();
+        wb.setSheetName(0, sheetName);
+        this.styles = createStyles(wb);
+    }
+
+    /**
+     * 创建工作表
+     *
+     * @param sheetNo sheet数量
+     * @param index   序号
+     */
+    public void createSheet(int sheetNo, int index) {
+        // 设置工作表的名称.
+        if (sheetNo > 1 && index > 0) {
+            this.sheet = wb.createSheet();
+            this.createTitle();
+            wb.setSheetName(index, sheetName + index);
+        }
+    }
+
+    /**
+     * 获取单元格值
+     *
+     * @param row    获取的行
+     * @param column 获取单元格列号
+     * @return 单元格值
+     */
+    public Object getCellValue(Row row, int column) {
+        if (row == null) {
+            return row;
+        }
+        Object val = "";
+        try {
+            Cell cell = row.getCell(column);
+            if (StringUtils.isNotNull(cell)) {
+                if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) {
+                    val = cell.getNumericCellValue();
+                    if (DateUtil.isCellDateFormatted(cell)) {
+                        val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
+                    } else {
+                        if ((Double) val % 1 != 0) {
+                            val = new BigDecimal(val.toString());
+                        } else {
+                            val = new DecimalFormat("0").format(val);
+                        }
+                    }
+                } else if (cell.getCellType() == CellType.STRING) {
+                    val = cell.getStringCellValue();
+                } else if (cell.getCellType() == CellType.BOOLEAN) {
+                    val = cell.getBooleanCellValue();
+                } else if (cell.getCellType() == CellType.ERROR) {
+                    val = cell.getErrorCellValue();
+                }
+
+            }
+        } catch (Exception e) {
+            return val;
+        }
+        return val;
+    }
+
+    /**
+     * 判断是否是空行
+     *
+     * @param row 判断的行
+     * @return
+     */
+    private boolean isRowEmpty(Row row) {
+        if (row == null) {
+            return true;
+        }
+        for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) {
+            Cell cell = row.getCell(i);
+            if (cell != null && cell.getCellType() != CellType.BLANK) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 获取Excel2003图片
+     *
+     * @param sheet    当前sheet对象
+     * @param workbook 工作簿对象
+     * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData
+     */
+    public static Map<String, PictureData> getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) {
+        Map<String, PictureData> sheetIndexPicMap = new HashMap<String, PictureData>();
+        List<HSSFPictureData> pictures = workbook.getAllPictures();
+        if (!pictures.isEmpty()) {
+            for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) {
+                HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor();
+                if (shape instanceof HSSFPicture) {
+                    HSSFPicture pic = (HSSFPicture) shape;
+                    int pictureIndex = pic.getPictureIndex() - 1;
+                    HSSFPictureData picData = pictures.get(pictureIndex);
+                    String picIndex = anchor.getRow1() + "_" + anchor.getCol1();
+                    sheetIndexPicMap.put(picIndex, picData);
+                }
+            }
+            return sheetIndexPicMap;
+        } else {
+            return sheetIndexPicMap;
+        }
+    }
+
+    /**
+     * 获取Excel2007图片
+     *
+     * @param sheet    当前sheet对象
+     * @param workbook 工作簿对象
+     * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData
+     */
+    public static Map<String, PictureData> getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) {
+        Map<String, PictureData> sheetIndexPicMap = new HashMap<String, PictureData>();
+        for (POIXMLDocumentPart dr : sheet.getRelations()) {
+            if (dr instanceof XSSFDrawing) {
+                XSSFDrawing drawing = (XSSFDrawing) dr;
+                List<XSSFShape> shapes = drawing.getShapes();
+                for (XSSFShape shape : shapes) {
+                    if (shape instanceof XSSFPicture) {
+                        XSSFPicture pic = (XSSFPicture) shape;
+                        XSSFClientAnchor anchor = pic.getPreferredSize();
+                        CTMarker ctMarker = anchor.getFrom();
+                        String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol();
+                        sheetIndexPicMap.put(picIndex, pic.getPictureData());
+                    }
+                }
+            }
+        }
+        return sheetIndexPicMap;
+    }
+
+    /**
+     * 格式化不同类型的日期对象
+     *
+     * @param dateFormat 日期格式
+     * @param val        被格式化的日期对象
+     * @return 格式化后的日期字符
+     */
+    public String parseDateToStr(String dateFormat, Object val) {
+        if (val == null) {
+            return "";
+        }
+        String str;
+        if (val instanceof Date) {
+            str = DateUtils.parseDateToStr(dateFormat, (Date) val);
+        } else if (val instanceof LocalDateTime) {
+            str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val));
+        } else if (val instanceof LocalDate) {
+            str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val));
+        } else {
+            str = val.toString();
+        }
+        return str;
+    }
+
+    /**
+     * 是否有对象的子列表
+     */
+    public boolean isSubList() {
+        return StringUtils.isNotNull(subFields) && subFields.size() > 0;
+    }
+
+    /**
+     * 是否有对象的子列表,集合不为空
+     */
+    public boolean isSubListValue(T vo) {
+        return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0;
+    }
+
+    /**
+     * 获取集合的值
+     */
+    public Collection<?> getListCellValue(Object obj) {
+        Object value;
+        try {
+            value = subMethod.invoke(obj, new Object[]{});
+        } catch (Exception e) {
+            return new ArrayList<Object>();
+        }
+        return (Collection<?>) value;
+    }
+
+    /**
+     * 获取对象的子列表方法
+     *
+     * @param name      名称
+     * @param pojoClass 类对象
+     * @return 子列表方法
+     */
+    public Method getSubMethod(String name, Class<?> pojoClass) {
+        StringBuffer getMethodName = new StringBuffer("get");
+        getMethodName.append(name.substring(0, 1).toUpperCase());
+        getMethodName.append(name.substring(1));
+        Method method = null;
+        try {
+            method = pojoClass.getMethod(getMethodName.toString(), new Class[]{});
+        } catch (Exception e) {
+            log.error("获取对象异常{}", e.getMessage());
+        }
+        return method;
+    }
 }

+ 334 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/reflect/ReflectUtils.java

@@ -0,0 +1,334 @@
+package com.lij.himeeter.common.utils.reflect;
+
+import com.lij.himeeter.common.core.text.Convert;
+import com.lij.himeeter.common.utils.DateUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.*;
+import java.util.Date;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ReflectUtils
+ * 创建时间 : 2025年01月03日 19:06
+ *
+ * 文件描述 : 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@SuppressWarnings("rawtypes")
+public class ReflectUtils {
+
+    private static final String SETTER_PREFIX = "set";
+
+    private static final String GETTER_PREFIX = "get";
+
+    private static final String CGLIB_CLASS_SEPARATOR = "$$";
+
+    private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class);
+
+    /**
+     * 调用Getter方法.
+     * 支持多级,如:对象名.对象名.方法
+     */
+    @SuppressWarnings("unchecked")
+    public static <E> E invokeGetter(Object obj, String propertyName) {
+        Object object = obj;
+        for (String name : StringUtils.split(propertyName, ".")) {
+            String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
+            object = invokeMethod(object, getterMethodName, new Class[]{}, new Object[]{});
+        }
+        return (E) object;
+    }
+
+    /**
+     * 调用Setter方法, 仅匹配方法名。
+     * 支持多级,如:对象名.对象名.方法
+     */
+    public static <E> void invokeSetter(Object obj, String propertyName, E value) {
+        Object object = obj;
+        String[] names = StringUtils.split(propertyName, ".");
+        for (int i = 0; i < names.length; i++) {
+            if (i < names.length - 1) {
+                String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
+                object = invokeMethod(object, getterMethodName, new Class[]{}, new Object[]{});
+            } else {
+                String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
+                invokeMethodByName(object, setterMethodName, new Object[]{value});
+            }
+        }
+    }
+
+    /**
+     * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
+     */
+    @SuppressWarnings("unchecked")
+    public static <E> E getFieldValue(final Object obj, final String fieldName) {
+        Field field = getAccessibleField(obj, fieldName);
+        if (field == null) {
+            logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+            return null;
+        }
+        E result = null;
+        try {
+            result = (E) field.get(obj);
+        } catch (IllegalAccessException e) {
+            logger.error("不可能抛出的异常{}", e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数.
+     */
+    public static <E> void setFieldValue(final Object obj, final String fieldName, final E value) {
+        Field field = getAccessibleField(obj, fieldName);
+        if (field == null) {
+            // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+            logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
+            return;
+        }
+        try {
+            field.set(obj, value);
+        } catch (IllegalAccessException e) {
+            logger.error("不可能抛出的异常: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * 直接调用对象方法, 无视private/protected修饰符.
+     * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用.
+     * 同时匹配方法名+参数类型,
+     */
+    @SuppressWarnings("unchecked")
+    public static <E> E invokeMethod(final Object obj, final String methodName, final Class<?>[] parameterTypes,
+                                     final Object[] args) {
+        if (obj == null || methodName == null) {
+            return null;
+        }
+        Method method = getAccessibleMethod(obj, methodName, parameterTypes);
+        if (method == null) {
+            logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
+            return null;
+        }
+        try {
+            return (E) method.invoke(obj, args);
+        } catch (Exception e) {
+            String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
+            throw convertReflectionExceptionToUnchecked(msg, e);
+        }
+    }
+
+    /**
+     * 直接调用对象方法, 无视private/protected修饰符,
+     * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用.
+     * 只匹配函数名,如果有多个同名函数调用第一个。
+     */
+    @SuppressWarnings("unchecked")
+    public static <E> E invokeMethodByName(final Object obj, final String methodName, final Object[] args) {
+        Method method = getAccessibleMethodByName(obj, methodName, args.length);
+        if (method == null) {
+            // 如果为空不报错,直接返回空。
+            logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
+            return null;
+        }
+        try {
+            // 类型转换(将参数数据类型转换为目标方法参数类型)
+            Class<?>[] cs = method.getParameterTypes();
+            for (int i = 0; i < cs.length; i++) {
+                if (args[i] != null && !args[i].getClass().equals(cs[i])) {
+                    if (cs[i] == String.class) {
+                        args[i] = Convert.toStr(args[i]);
+                        if (StringUtils.endsWith((String) args[i], ".0")) {
+                            args[i] = StringUtils.substringBefore((String) args[i], ".0");
+                        }
+                    } else if (cs[i] == Integer.class) {
+                        args[i] = Convert.toInt(args[i]);
+                    } else if (cs[i] == Long.class) {
+                        args[i] = Convert.toLong(args[i]);
+                    } else if (cs[i] == Double.class) {
+                        args[i] = Convert.toDouble(args[i]);
+                    } else if (cs[i] == Float.class) {
+                        args[i] = Convert.toFloat(args[i]);
+                    } else if (cs[i] == Date.class) {
+                        if (args[i] instanceof String) {
+                            args[i] = DateUtils.parseDate(args[i]);
+                        } else {
+                            args[i] = DateUtil.getJavaDate((Double) args[i]);
+                        }
+                    } else if (cs[i] == boolean.class || cs[i] == Boolean.class) {
+                        args[i] = Convert.toBool(args[i]);
+                    }
+                }
+            }
+            return (E) method.invoke(obj, args);
+        } catch (Exception e) {
+            String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
+            throw convertReflectionExceptionToUnchecked(msg, e);
+        }
+    }
+
+    /**
+     * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
+     * 如向上转型到Object仍无法找到, 返回null.
+     */
+    public static Field getAccessibleField(final Object obj, final String fieldName) {
+        // 为空不报错。直接返回 null
+        if (obj == null) {
+            return null;
+        }
+        Validate.notBlank(fieldName, "fieldName can't be blank");
+        for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
+            try {
+                Field field = superClass.getDeclaredField(fieldName);
+                makeAccessible(field);
+                return field;
+            } catch (NoSuchFieldException e) {
+                continue;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
+     * 如向上转型到Object仍无法找到, 返回null.
+     * 匹配函数名+参数类型。
+     * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
+     */
+    public static Method getAccessibleMethod(final Object obj, final String methodName,
+                                             final Class<?>... parameterTypes) {
+        // 为空不报错。直接返回 null
+        if (obj == null) {
+            return null;
+        }
+        Validate.notBlank(methodName, "methodName can't be blank");
+        for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) {
+            try {
+                Method method = searchType.getDeclaredMethod(methodName, parameterTypes);
+                makeAccessible(method);
+                return method;
+            } catch (NoSuchMethodException e) {
+                continue;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
+     * 如向上转型到Object仍无法找到, 返回null.
+     * 只匹配函数名。
+     * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
+     */
+    public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) {
+        // 为空不报错。直接返回 null
+        if (obj == null) {
+            return null;
+        }
+        Validate.notBlank(methodName, "methodName can't be blank");
+        for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) {
+            Method[] methods = searchType.getDeclaredMethods();
+            for (Method method : methods) {
+                if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) {
+                    makeAccessible(method);
+                    return method;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
+     */
+    public static void makeAccessible(Method method) {
+        if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))
+                && !method.isAccessible()) {
+            method.setAccessible(true);
+        }
+    }
+
+    /**
+     * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
+     */
+    public static void makeAccessible(Field field) {
+        if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())
+                || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) {
+            field.setAccessible(true);
+        }
+    }
+
+    /**
+     * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处
+     * 如无法找到, 返回Object.class.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Class<T> getClassGenricType(final Class clazz) {
+        return getClassGenricType(clazz, 0);
+    }
+
+    /**
+     * 通过反射, 获得Class定义中声明的父类的泛型参数的类型.
+     * 如无法找到, 返回Object.class.
+     */
+    public static Class getClassGenricType(final Class clazz, final int index) {
+        Type genType = clazz.getGenericSuperclass();
+
+        if (!(genType instanceof ParameterizedType)) {
+            logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType");
+            return Object.class;
+        }
+
+        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
+
+        if (index >= params.length || index < 0) {
+            logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: "
+                    + params.length);
+            return Object.class;
+        }
+        if (!(params[index] instanceof Class)) {
+            logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
+            return Object.class;
+        }
+
+        return (Class) params[index];
+    }
+
+    public static Class<?> getUserClass(Object instance) {
+        if (instance == null) {
+            throw new RuntimeException("Instance must not be null");
+        }
+        Class clazz = instance.getClass();
+        if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) {
+            Class<?> superClass = clazz.getSuperclass();
+            if (superClass != null && !Object.class.equals(superClass)) {
+                return superClass;
+            }
+        }
+        return clazz;
+
+    }
+
+    /**
+     * 将反射时的checked exception转换为unchecked exception.
+     */
+    public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) {
+        if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException
+                || e instanceof NoSuchMethodException) {
+            return new IllegalArgumentException(msg, e);
+        } else if (e instanceof InvocationTargetException) {
+            return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException());
+        }
+        return new RuntimeException(msg, e);
+    }
+}

+ 263 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/sign/Base64.java

@@ -0,0 +1,263 @@
+package com.lij.himeeter.common.utils.sign;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  Base64
+ * 创建时间 : 2025年01月06日 03:14
+ *
+ * 文件描述 : Base64工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public final class Base64 {
+
+    static private final int BASELENGTH = 128;
+    static private final int LOOKUPLENGTH = 64;
+    static private final int TWENTYFOURBITGROUP = 24;
+    static private final int EIGHTBIT = 8;
+    static private final int SIXTEENBIT = 16;
+    static private final int FOURBYTE = 4;
+    static private final int SIGN = -128;
+    static private final char PAD = '=';
+    static final private byte[] base64Alphabet = new byte[BASELENGTH];
+    static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];
+
+    static {
+        for (int i = 0; i < BASELENGTH; ++i) {
+            base64Alphabet[i] = -1;
+        }
+        for (int i = 'Z'; i >= 'A'; i--) {
+            base64Alphabet[i] = (byte) (i - 'A');
+        }
+        for (int i = 'z'; i >= 'a'; i--) {
+            base64Alphabet[i] = (byte) (i - 'a' + 26);
+        }
+
+        for (int i = '9'; i >= '0'; i--) {
+            base64Alphabet[i] = (byte) (i - '0' + 52);
+        }
+
+        base64Alphabet['+'] = 62;
+        base64Alphabet['/'] = 63;
+
+        for (int i = 0; i <= 25; i++) {
+            lookUpBase64Alphabet[i] = (char) ('A' + i);
+        }
+
+        for (int i = 26, j = 0; i <= 51; i++, j++) {
+            lookUpBase64Alphabet[i] = (char) ('a' + j);
+        }
+
+        for (int i = 52, j = 0; i <= 61; i++, j++) {
+            lookUpBase64Alphabet[i] = (char) ('0' + j);
+        }
+        lookUpBase64Alphabet[62] = (char) '+';
+        lookUpBase64Alphabet[63] = (char) '/';
+    }
+
+    private static boolean isWhiteSpace(char octect) {
+        return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
+    }
+
+    private static boolean isPad(char octect) {
+        return (octect == PAD);
+    }
+
+    private static boolean isData(char octect) {
+        return (octect < BASELENGTH && base64Alphabet[octect] != -1);
+    }
+
+    /**
+     * Encodes hex octects into Base64
+     *
+     * @param binaryData Array containing binaryData
+     * @return Encoded Base64 array
+     */
+    public static String encode(byte[] binaryData) {
+        if (binaryData == null) {
+            return null;
+        }
+
+        int lengthDataBits = binaryData.length * EIGHTBIT;
+        if (lengthDataBits == 0) {
+            return "";
+        }
+
+        int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
+        int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
+        int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
+        char encodedData[] = null;
+
+        encodedData = new char[numberQuartet * 4];
+
+        byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;
+
+        int encodedIndex = 0;
+        int dataIndex = 0;
+
+        for (int i = 0; i < numberTriplets; i++) {
+            b1 = binaryData[dataIndex++];
+            b2 = binaryData[dataIndex++];
+            b3 = binaryData[dataIndex++];
+
+            l = (byte) (b2 & 0x0f);
+            k = (byte) (b1 & 0x03);
+
+            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+            byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);
+
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
+        }
+
+        // form integral number of 6-bit groups
+        if (fewerThan24bits == EIGHTBIT) {
+            b1 = binaryData[dataIndex];
+            k = (byte) (b1 & 0x03);
+            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
+            encodedData[encodedIndex++] = PAD;
+            encodedData[encodedIndex++] = PAD;
+        } else if (fewerThan24bits == SIXTEENBIT) {
+            b1 = binaryData[dataIndex];
+            b2 = binaryData[dataIndex + 1];
+            l = (byte) (b2 & 0x0f);
+            k = (byte) (b1 & 0x03);
+
+            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
+            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
+
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
+            encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
+            encodedData[encodedIndex++] = PAD;
+        }
+        return new String(encodedData);
+    }
+
+    /**
+     * Decodes Base64 data into octects
+     *
+     * @param encoded string containing Base64 data
+     * @return Array containind decoded data.
+     */
+    public static byte[] decode(String encoded) {
+        if (encoded == null) {
+            return null;
+        }
+
+        char[] base64Data = encoded.toCharArray();
+        // remove white spaces
+        int len = removeWhiteSpace(base64Data);
+
+        if (len % FOURBYTE != 0) {
+            return null;// should be divisible by four
+        }
+
+        int numberQuadruple = (len / FOURBYTE);
+
+        if (numberQuadruple == 0) {
+            return new byte[0];
+        }
+
+        byte decodedData[] = null;
+        byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
+        char d1 = 0, d2 = 0, d3 = 0, d4 = 0;
+
+        int i = 0;
+        int encodedIndex = 0;
+        int dataIndex = 0;
+        decodedData = new byte[(numberQuadruple) * 3];
+
+        for (; i < numberQuadruple - 1; i++) {
+
+            if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
+                    || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) {
+                return null;
+            } // if found "no data" just return null
+
+            b1 = base64Alphabet[d1];
+            b2 = base64Alphabet[d2];
+            b3 = base64Alphabet[d3];
+            b4 = base64Alphabet[d4];
+
+            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+        }
+
+        if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) {
+            return null;// if found "no data" just return null
+        }
+
+        b1 = base64Alphabet[d1];
+        b2 = base64Alphabet[d2];
+
+        d3 = base64Data[dataIndex++];
+        d4 = base64Data[dataIndex++];
+        if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
+            if (isPad(d3) && isPad(d4)) {
+                if ((b2 & 0xf) != 0)// last 4 bits should be zero
+                {
+                    return null;
+                }
+                byte[] tmp = new byte[i * 3 + 1];
+                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+                tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
+                return tmp;
+            } else if (!isPad(d3) && isPad(d4)) {
+                b3 = base64Alphabet[d3];
+                if ((b3 & 0x3) != 0)// last 2 bits should be zero
+                {
+                    return null;
+                }
+                byte[] tmp = new byte[i * 3 + 2];
+                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
+                tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+                tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+                return tmp;
+            } else {
+                return null;
+            }
+        } else { // No PAD e.g 3cQl
+            b3 = base64Alphabet[d3];
+            b4 = base64Alphabet[d4];
+            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
+            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
+            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
+
+        }
+        return decodedData;
+    }
+
+    /**
+     * remove WhiteSpace from MIME containing encoded Base64 data.
+     *
+     * @param data the byte array of base64 data (with WS)
+     * @return the new length
+     */
+    private static int removeWhiteSpace(char[] data) {
+        if (data == null) {
+            return 0;
+        }
+
+        // count characters that's not whitespace
+        int newSize = 0;
+        int len = data.length;
+        for (int i = 0; i < len; i++) {
+            if (!isWhiteSpace(data[i])) {
+                data[newSize++] = data[i];
+            }
+        }
+        return newSize;
+    }
+}

+ 65 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/sign/Md5Utils.java

@@ -0,0 +1,65 @@
+package com.lij.himeeter.common.utils.sign;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  Md5Utils
+ * 创建时间 : 2025年01月06日 03:15
+ *
+ * 文件描述 : Md5加密方法
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class Md5Utils {
+
+    private static final Logger log = LoggerFactory.getLogger(Md5Utils.class);
+
+    private static byte[] md5(String s) {
+        MessageDigest algorithm;
+        try {
+            algorithm = MessageDigest.getInstance("MD5");
+            algorithm.reset();
+            algorithm.update(s.getBytes("UTF-8"));
+            byte[] messageDigest = algorithm.digest();
+            return messageDigest;
+        } catch (Exception e) {
+            log.error("MD5 Error...", e);
+        }
+        return null;
+    }
+
+    private static final String toHex(byte hash[]) {
+        if (hash == null) {
+            return null;
+        }
+        StringBuffer buf = new StringBuffer(hash.length * 2);
+        int i;
+
+        for (i = 0; i < hash.length; i++) {
+            if ((hash[i] & 0xff) < 0x10) {
+                buf.append("0");
+            }
+            buf.append(Long.toString(hash[i] & 0xff, 16));
+        }
+        return buf.toString();
+    }
+
+    public static String hash(String s) {
+        try {
+            return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            log.error("not supported charset...{}", e);
+            return s;
+        }
+    }
+}

+ 2 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/spring/SpringUtils.java

@@ -8,6 +8,7 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
 
 /**
  * <pre>
@@ -23,6 +24,7 @@ import org.springframework.context.ApplicationContextAware;
  * @version 1.0.0
  */
 
+@Component
 public class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
 
     /**

+ 71 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/sql/SqlUtil.java

@@ -0,0 +1,71 @@
+package com.lij.himeeter.common.utils.sql;
+
+import com.lij.himeeter.common.exception.UtilException;
+import com.lij.himeeter.common.utils.StringUtils;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SqlUtil
+ * 创建时间 : 2025年01月02日 15:53
+ *
+ * 文件描述 : sql操作工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class SqlUtil {
+
+    /**
+     * 定义常用的 sql关键字
+     */
+    public static String SQL_REGEX = "\u000B|and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
+
+    /**
+     * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
+     */
+    public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
+
+    /**
+     * 限制orderBy最大长度
+     */
+    private static final int ORDER_BY_MAX_LENGTH = 500;
+
+    /**
+     * 检查字符,防止注入绕过
+     */
+    public static String escapeOrderBySql(String value) {
+        if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
+            throw new UtilException("参数不符合规范,不能进行查询");
+        }
+        if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH) {
+            throw new UtilException("参数已超过最大限制,不能进行查询");
+        }
+        return value;
+    }
+
+    /**
+     * 验证 order by 语法是否符合规范
+     */
+    public static boolean isValidOrderBySql(String value) {
+        return value.matches(SQL_PATTERN);
+    }
+
+    /**
+     * SQL关键字检查
+     */
+    public static void filterKeyword(String value) {
+        if (StringUtils.isEmpty(value)) {
+            return;
+        }
+        String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
+        for (String sqlKeyword : sqlKeywords) {
+            if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) {
+                throw new UtilException("参数存在SQL注入风险");
+            }
+        }
+    }
+}

+ 60 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/IdUtils.java

@@ -0,0 +1,60 @@
+package com.lij.himeeter.common.utils.uuid;
+
+
+/**
+ * <pre>
+ *
+ * 文件名 :  IdUtils
+ * 创建时间 : 2025年01月02日 14:58
+ *
+ * 文件描述 : ID生成器工具类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class IdUtils {
+
+    /**
+     * 获取随机UUID
+     *
+     * @return 随机UUID
+     */
+    public static String randomUUID()
+    {
+        return UUID.randomUUID().toString();
+    }
+
+    /**
+     * 简化的UUID,去掉了横线
+     *
+     * @return 简化的UUID,去掉了横线
+     */
+    public static String simpleUUID()
+    {
+        return UUID.randomUUID().toString(true);
+    }
+
+    /**
+     * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID
+     *
+     * @return 随机UUID
+     */
+    public static String fastUUID()
+    {
+        return UUID.fastUUID().toString();
+    }
+
+    /**
+     * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID
+     *
+     * @return 简化的UUID,去掉了横线
+     */
+    public static String fastSimpleUUID()
+    {
+        return UUID.fastUUID().toString(true);
+    }
+
+}

+ 98 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/Seq.java

@@ -0,0 +1,98 @@
+package com.lij.himeeter.common.utils.uuid;
+
+import com.lij.himeeter.common.utils.DateUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  Seq
+ * 创建时间 : 2025年01月02日 15:03
+ *
+ * 文件描述 : 序列生成类
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class Seq
+{
+    // 通用序列类型
+    public static final String commSeqType = "COMMON";
+
+    // 上传序列类型
+    public static final String uploadSeqType = "UPLOAD";
+
+    // 通用接口序列数
+    private static AtomicInteger commSeq = new AtomicInteger(1);
+
+    // 上传接口序列数
+    private static AtomicInteger uploadSeq = new AtomicInteger(1);
+
+    // 机器标识
+    private static final String machineCode = "A";
+
+    /**
+     * 获取通用序列号
+     *
+     * @return 序列值
+     */
+    public static String getId()
+    {
+        return getId(commSeqType);
+    }
+
+    /**
+     * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串
+     *
+     * @return 序列值
+     */
+    public static String getId(String type)
+    {
+        AtomicInteger atomicInt = commSeq;
+        if (uploadSeqType.equals(type))
+        {
+            atomicInt = uploadSeq;
+        }
+        return getId(atomicInt, 3);
+    }
+
+    /**
+     * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串
+     *
+     * @param atomicInt 序列数
+     * @param length 数值长度
+     * @return 序列值
+     */
+    public static String getId(AtomicInteger atomicInt, int length)
+    {
+        String result = DateUtils.dateTimeNow();
+        result += machineCode;
+        result += getSeq(atomicInt, length);
+        return result;
+    }
+
+    /**
+     * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数
+     *
+     * @return 序列值
+     */
+    private synchronized static String getSeq(AtomicInteger atomicInt, int length)
+    {
+        // 先取值再+1
+        int value = atomicInt.getAndIncrement();
+
+        // 如果更新后值>=10 的 (length)幂次方则重置为1
+        int maxSeq = (int) Math.pow(10, length);
+        if (atomicInt.get() >= maxSeq)
+        {
+            atomicInt.set(1);
+        }
+        // 转字符串,用0左补齐
+        return StringUtils.padl(value, length);
+    }
+}

+ 450 - 0
himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/UUID.java

@@ -0,0 +1,450 @@
+package com.lij.himeeter.common.utils.uuid;
+
+import com.lij.himeeter.common.exception.UtilException;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  UUID
+ * 创建时间 : 2025年01月02日 14:59
+ *
+ * 文件描述 : 提供通用唯一识别码(universally unique identifier)(UUID)实现
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public final class UUID implements java.io.Serializable, Comparable<UUID> {
+    private static final long serialVersionUID = -1185015143654744140L;
+
+    /**
+     * SecureRandom 的单例
+     */
+    private static class Holder {
+        static final SecureRandom numberGenerator = getSecureRandom();
+    }
+
+    /**
+     * 此UUID的最高64有效位
+     */
+    private final long mostSigBits;
+
+    /**
+     * 此UUID的最低64有效位
+     */
+    private final long leastSigBits;
+
+    /**
+     * 私有构造
+     *
+     * @param data 数据
+     */
+    private UUID(byte[] data) {
+        long msb = 0;
+        long lsb = 0;
+        assert data.length == 16 : "data must be 16 bytes in length";
+        for (int i = 0; i < 8; i++) {
+            msb = (msb << 8) | (data[i] & 0xff);
+        }
+        for (int i = 8; i < 16; i++) {
+            lsb = (lsb << 8) | (data[i] & 0xff);
+        }
+        this.mostSigBits = msb;
+        this.leastSigBits = lsb;
+    }
+
+    /**
+     * 使用指定的数据构造新的 UUID。
+     *
+     * @param mostSigBits  用于 {@code UUID} 的最高有效 64 位
+     * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位
+     */
+    public UUID(long mostSigBits, long leastSigBits) {
+        this.mostSigBits = mostSigBits;
+        this.leastSigBits = leastSigBits;
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。
+     *
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID fastUUID() {
+        return randomUUID(false);
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+     *
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID randomUUID() {
+        return randomUUID(true);
+    }
+
+    /**
+     * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。
+     *
+     * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能
+     * @return 随机生成的 {@code UUID}
+     */
+    public static UUID randomUUID(boolean isSecure) {
+        final Random ng = isSecure ? Holder.numberGenerator : getRandom();
+
+        byte[] randomBytes = new byte[16];
+        ng.nextBytes(randomBytes);
+        randomBytes[6] &= 0x0f; /* clear version */
+        randomBytes[6] |= 0x40; /* set to version 4 */
+        randomBytes[8] &= 0x3f; /* clear variant */
+        randomBytes[8] |= 0x80; /* set to IETF variant */
+        return new UUID(randomBytes);
+    }
+
+    /**
+     * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。
+     *
+     * @param name 用于构造 UUID 的字节数组。
+     * @return 根据指定数组生成的 {@code UUID}
+     */
+    public static UUID nameUUIDFromBytes(byte[] name) {
+        MessageDigest md;
+        try {
+            md = MessageDigest.getInstance("MD5");
+        } catch (NoSuchAlgorithmException nsae) {
+            throw new InternalError("MD5 not supported");
+        }
+        byte[] md5Bytes = md.digest(name);
+        md5Bytes[6] &= 0x0f; /* clear version */
+        md5Bytes[6] |= 0x30; /* set to version 3 */
+        md5Bytes[8] &= 0x3f; /* clear variant */
+        md5Bytes[8] |= 0x80; /* set to IETF variant */
+        return new UUID(md5Bytes);
+    }
+
+    /**
+     * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。
+     *
+     * @param name 指定 {@code UUID} 字符串
+     * @return 具有指定值的 {@code UUID}
+     * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常
+     */
+    public static UUID fromString(String name) {
+        String[] components = name.split("-");
+        if (components.length != 5) {
+            throw new IllegalArgumentException("Invalid UUID string: " + name);
+        }
+        for (int i = 0; i < 5; i++) {
+            components[i] = "0x" + components[i];
+        }
+
+        long mostSigBits = Long.decode(components[0]).longValue();
+        mostSigBits <<= 16;
+        mostSigBits |= Long.decode(components[1]).longValue();
+        mostSigBits <<= 16;
+        mostSigBits |= Long.decode(components[2]).longValue();
+
+        long leastSigBits = Long.decode(components[3]).longValue();
+        leastSigBits <<= 48;
+        leastSigBits |= Long.decode(components[4]).longValue();
+
+        return new UUID(mostSigBits, leastSigBits);
+    }
+
+    /**
+     * 返回此 UUID 的 128 位值中的最低有效 64 位。
+     *
+     * @return 此 UUID 的 128 位值中的最低有效 64 位。
+     */
+    public long getLeastSignificantBits() {
+        return leastSigBits;
+    }
+
+    /**
+     * 返回此 UUID 的 128 位值中的最高有效 64 位。
+     *
+     * @return 此 UUID 的 128 位值中最高有效 64 位。
+     */
+    public long getMostSignificantBits() {
+        return mostSigBits;
+    }
+
+    /**
+     * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。
+     * <p>
+     * 版本号具有以下含意:
+     * <ul>
+     * <li>1 基于时间的 UUID
+     * <li>2 DCE 安全 UUID
+     * <li>3 基于名称的 UUID
+     * <li>4 随机生成的 UUID
+     * </ul>
+     *
+     * @return 此 {@code UUID} 的版本号
+     */
+    public int version() {
+        // Version is bits masked by 0x000000000000F000 in MS long
+        return (int) ((mostSigBits >> 12) & 0x0f);
+    }
+
+    /**
+     * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。
+     * <p>
+     * 变体号具有以下含意:
+     * <ul>
+     * <li>0 为 NCS 向后兼容保留
+     * <li>2 <a href="http://www.ietf.org/rfc/rfc4122.txt">IETF&nbsp;RFC&nbsp;4122</a>(Leach-Salz), 用于此类
+     * <li>6 保留,微软向后兼容
+     * <li>7 保留供以后定义使用
+     * </ul>
+     *
+     * @return 此 {@code UUID} 相关联的变体号
+     */
+    public int variant() {
+        // This field is composed of a varying number of bits.
+        // 0 - - Reserved for NCS backward compatibility
+        // 1 0 - The IETF aka Leach-Salz variant (used by this class)
+        // 1 1 0 Reserved, Microsoft backward compatibility
+        // 1 1 1 Reserved for future definition.
+        return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63));
+    }
+
+    /**
+     * 与此 UUID 相关联的时间戳值。
+     *
+     * <p>
+     * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。<br>
+     * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。
+     *
+     * <p>
+     * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+     * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+     *
+     * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。
+     */
+    public long timestamp() throws UnsupportedOperationException {
+        checkTimeBase();
+        return (mostSigBits & 0x0FFFL) << 48//
+                | ((mostSigBits >> 16) & 0x0FFFFL) << 32//
+                | mostSigBits >>> 32;
+    }
+
+    /**
+     * 与此 UUID 相关联的时钟序列值。
+     *
+     * <p>
+     * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。
+     * <p>
+     * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出
+     * UnsupportedOperationException。
+     *
+     * @return 此 {@code UUID} 的时钟序列
+     * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+     */
+    public int clockSequence() throws UnsupportedOperationException {
+        checkTimeBase();
+        return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48);
+    }
+
+    /**
+     * 与此 UUID 相关的节点值。
+     *
+     * <p>
+     * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。
+     * <p>
+     * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。<br>
+     * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。
+     *
+     * @return 此 {@code UUID} 的节点值
+     * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1
+     */
+    public long node() throws UnsupportedOperationException {
+        checkTimeBase();
+        return leastSigBits & 0x0000FFFFFFFFFFFFL;
+    }
+
+    /**
+     * 返回此{@code UUID} 的字符串表现形式。
+     *
+     * <p>
+     * UUID 的字符串表示形式由此 BNF 描述:
+     *
+     * <pre>
+     * {@code
+     * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+     * time_low               = 4*<hexOctet>
+     * time_mid               = 2*<hexOctet>
+     * time_high_and_version  = 2*<hexOctet>
+     * variant_and_sequence   = 2*<hexOctet>
+     * node                   = 6*<hexOctet>
+     * hexOctet               = <hexDigit><hexDigit>
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * </pre>
+     *
+     * </blockquote>
+     *
+     * @return 此{@code UUID} 的字符串表现形式
+     * @see #toString(boolean)
+     */
+    @Override
+    public String toString() {
+        return toString(false);
+    }
+
+    /**
+     * 返回此{@code UUID} 的字符串表现形式。
+     *
+     * <p>
+     * UUID 的字符串表示形式由此 BNF 描述:
+     *
+     * <pre>
+     * {@code
+     * UUID                   = <time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
+     * time_low               = 4*<hexOctet>
+     * time_mid               = 2*<hexOctet>
+     * time_high_and_version  = 2*<hexOctet>
+     * variant_and_sequence   = 2*<hexOctet>
+     * node                   = 6*<hexOctet>
+     * hexOctet               = <hexDigit><hexDigit>
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * </pre>
+     *
+     * </blockquote>
+     *
+     * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串
+     * @return 此{@code UUID} 的字符串表现形式
+     */
+    public String toString(boolean isSimple) {
+        final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36);
+        // time_low
+        builder.append(digits(mostSigBits >> 32, 8));
+        if (!isSimple) {
+            builder.append('-');
+        }
+        // time_mid
+        builder.append(digits(mostSigBits >> 16, 4));
+        if (!isSimple) {
+            builder.append('-');
+        }
+        // time_high_and_version
+        builder.append(digits(mostSigBits, 4));
+        if (!isSimple) {
+            builder.append('-');
+        }
+        // variant_and_sequence
+        builder.append(digits(leastSigBits >> 48, 4));
+        if (!isSimple) {
+            builder.append('-');
+        }
+        // node
+        builder.append(digits(leastSigBits, 12));
+
+        return builder.toString();
+    }
+
+    /**
+     * 返回此 UUID 的哈希码。
+     *
+     * @return UUID 的哈希码值。
+     */
+    @Override
+    public int hashCode() {
+        long hilo = mostSigBits ^ leastSigBits;
+        return ((int) (hilo >> 32)) ^ (int) hilo;
+    }
+
+    /**
+     * 将此对象与指定对象比较。
+     * <p>
+     * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。
+     *
+     * @param obj 要与之比较的对象
+     * @return 如果对象相同,则返回 {@code true};否则返回 {@code false}
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if ((null == obj) || (obj.getClass() != UUID.class)) {
+            return false;
+        }
+        UUID id = (UUID) obj;
+        return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits);
+    }
+
+    // Comparison Operations
+
+    /**
+     * 将此 UUID 与指定的 UUID 比较。
+     *
+     * <p>
+     * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。
+     *
+     * @param val 与此 UUID 比较的 UUID
+     * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。
+     */
+    @Override
+    public int compareTo(UUID val) {
+        // The ordering is intentionally set up so that the UUIDs
+        // can simply be numerically compared as two numbers
+        return (this.mostSigBits < val.mostSigBits ? -1 : //
+                (this.mostSigBits > val.mostSigBits ? 1 : //
+                        (this.leastSigBits < val.leastSigBits ? -1 : //
+                                (this.leastSigBits > val.leastSigBits ? 1 : //
+                                        0))));
+    }
+
+    // -------------------------------------------------------------------------------------------------------------------
+    // Private method start
+
+    /**
+     * 返回指定数字对应的hex值
+     *
+     * @param val    值
+     * @param digits 位
+     * @return 值
+     */
+    private static String digits(long val, int digits) {
+        long hi = 1L << (digits * 4);
+        return Long.toHexString(hi | (val & (hi - 1))).substring(1);
+    }
+
+    /**
+     * 检查是否为time-based版本UUID
+     */
+    private void checkTimeBase() {
+        if (version() != 1) {
+            throw new UnsupportedOperationException("Not a time-based UUID");
+        }
+    }
+
+    /**
+     * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG)
+     *
+     * @return {@link SecureRandom}
+     */
+    public static SecureRandom getSecureRandom() {
+        try {
+            return SecureRandom.getInstance("SHA1PRNG");
+        } catch (NoSuchAlgorithmException e) {
+            throw new UtilException(e);
+        }
+    }
+
+    /**
+     * 获取随机数生成器对象<br>
+     * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。
+     *
+     * @return {@link ThreadLocalRandom}
+     */
+    public static ThreadLocalRandom getRandom() {
+        return ThreadLocalRandom.current();
+    }
+}

+ 1 - 1
himeeter-framework/src/main/java/com/lij/himeeter/framework/aspectj/LogAspect.java

@@ -12,7 +12,7 @@ import com.lij.himeeter.common.utils.StringUtils;
 import com.lij.himeeter.common.utils.ip.IpUtils;
 import com.lij.himeeter.framework.manager.AsyncManager;
 import com.lij.himeeter.framework.manager.factory.AsyncFactory;
-import com.lij.himeeter.system.domin.SysOperLog;
+import com.lij.himeeter.system.domain.SysOperLog;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.apache.commons.lang3.ArrayUtils;

+ 71 - 1
himeeter-framework/src/main/java/com/lij/himeeter/framework/aspectj/RateLimiterAspect.java

@@ -1,12 +1,32 @@
 package com.lij.himeeter.framework.aspectj;
 
+import com.lij.himeeter.common.annotation.RateLimiter;
+import com.lij.himeeter.common.enums.LimitType;
+import com.lij.himeeter.common.exception.ServiceException;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.ip.IpUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
 /**
  * <pre>
  *
  * 文件名 :  RateLimiterAspect
  * 创建时间 : 2024年12月27日 17:30
  *
- * 文件描述 : 无
+ * 文件描述 : 限流处理
  *
  * </pre>
  *
@@ -14,5 +34,55 @@ package com.lij.himeeter.framework.aspectj;
  * @version 1.0.0
  */
 
+@Aspect
+@Component
 public class RateLimiterAspect {
+
+    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
+
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    private RedisScript<Long> limitScript;
+
+    @Autowired
+    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Autowired
+    public void setLimitScript(RedisScript<Long> limitScript) {
+        this.limitScript = limitScript;
+    }
+
+    @Before("@annotation(rateLimiter)")
+    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
+        int time = rateLimiter.time();
+        int count = rateLimiter.count();
+
+        String combineKey = getCombineKey(rateLimiter, point);
+        List<Object> keys = Collections.singletonList(combineKey);
+        try {
+            Long number = redisTemplate.execute(limitScript, keys, count, time);
+            if (StringUtils.isNull(number) || number.intValue() > count) {
+                throw new ServiceException("访问过于频繁,请稍候再试");
+            }
+            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
+        } catch (ServiceException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new RuntimeException("服务器限流异常,请稍候再试");
+        }
+    }
+
+    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
+        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
+        if (rateLimiter.limitType() == LimitType.IP) {
+            stringBuffer.append(IpUtils.getIpAddr()).append("-");
+        }
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        Class<?> targetClass = method.getDeclaringClass();
+        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
+        return stringBuffer.toString();
+    }
 }

+ 38 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ApplicationConfig.java

@@ -0,0 +1,38 @@
+package com.lij.himeeter.framework.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+import java.util.TimeZone;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ApplicationConfig
+ * 创建时间 : 2024年12月30日 18:58
+ *
+ * 文件描述 : 程序注解配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.lij.himeeter.**.mapper")
+public class ApplicationConfig {
+    /**
+     * 时区配置
+     */
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() {
+        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+    }
+}

+ 92 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/CaptchaConfig.java

@@ -0,0 +1,92 @@
+package com.lij.himeeter.framework.config;
+
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Properties;
+
+import static com.google.code.kaptcha.Constants.*;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  CaptchaConfig
+ * 创建时间 : 2024年12月30日 19:00
+ *
+ * 文件描述 : 验证码配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class CaptchaConfig {
+
+    @Bean(name = "captchaProducer")
+    public DefaultKaptcha getKaptchaBean() {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+
+    @Bean(name = "captchaProducerMath")
+    public DefaultKaptcha getKaptchaBeanMath() {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 边框颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
+        // 验证码文本生成器
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.lij.himeeter.framework.config.KaptchaTextCreator");
+        // 验证码文本字符间距 默认为2
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 验证码噪点颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
+        // 干扰实现类
+        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+}

+ 122 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/DruidConfig.java

@@ -0,0 +1,122 @@
+package com.lij.himeeter.framework.config;
+
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
+import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.lij.himeeter.common.enums.DataSourceType;
+import com.lij.himeeter.common.utils.spring.SpringUtils;
+import com.lij.himeeter.framework.config.properties.DruidProperties;
+import com.lij.himeeter.framework.datasource.DynamicDataSource;
+import jakarta.servlet.*;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  DruidConfig
+ * 创建时间 : 2024年12月30日 18:44
+ *
+ * 文件描述 : druid 配置多数据源
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class DruidConfig {
+
+    @Bean
+    @ConfigurationProperties("spring.datasource.druid.master")
+    public DataSource masterDataSource(DruidProperties druidProperties) {
+        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+        return druidProperties.dataSource(dataSource);
+    }
+
+    @Bean
+    @ConfigurationProperties("spring.datasource.druid.slave")
+    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
+    public DataSource slaveDataSource(DruidProperties druidProperties) {
+        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+        return druidProperties.dataSource(dataSource);
+    }
+
+    @Bean(name = "dynamicDataSource")
+    @Primary
+    public DynamicDataSource dataSource(DataSource masterDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 设置数据源
+     *
+     * @param targetDataSources 备选数据源集合
+     * @param sourceName        数据源名称
+     * @param beanName          bean名称
+     */
+    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
+        try {
+            DataSource dataSource = SpringUtils.getBean(beanName);
+            targetDataSources.put(sourceName, dataSource);
+        } catch (Exception e) {
+        }
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter() {
+            @Override
+            public void init(jakarta.servlet.FilterConfig filterConfig) throws ServletException {
+            }
+
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+
+            @Override
+            public void destroy() {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 56 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,56 @@
+package com.lij.himeeter.framework.config;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+import com.alibaba.fastjson2.filter.Filter;
+import com.lij.himeeter.common.constant.Constants;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+
+import java.nio.charset.Charset;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FastJson2JsonRedisSerializer
+ * 创建时间 : 2024年12月30日 19:02
+ *
+ * 文件描述 : Redis使用FastJson序列化
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
+    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+    static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
+
+    private Class<T> clazz;
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length <= 0) {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+
+        return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
+    }
+}

+ 68 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/FilterConfig.java

@@ -0,0 +1,68 @@
+package com.lij.himeeter.framework.config;
+
+import com.lij.himeeter.common.filter.RepeatableFilter;
+import com.lij.himeeter.common.filter.XssFilter;
+import com.lij.himeeter.common.utils.StringUtils;
+import jakarta.servlet.DispatcherType;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  FilterConfig
+ * 创建时间 : 2024年12月30日 19:03
+ *
+ * 文件描述 : Filter配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class FilterConfig
+{
+    @Value("${xss.excludes}")
+    private String excludes;
+
+    @Value("${xss.urlPatterns}")
+    private String urlPatterns;
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+    public FilterRegistrationBean xssFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+        registration.setName("xssFilter");
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+        Map<String, String> initParameters = new HashMap<String, String>();
+        initParameters.put("excludes", excludes);
+        registration.setInitParameters(initParameters);
+        return registration;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean someFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setFilter(new RepeatableFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("repeatableFilter");
+        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+        return registration;
+    }
+
+}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác