Nobita 11 сар өмнө
parent
commit
362bcdfb44
54 өөрчлөгдсөн 6978 нэмэгдсэн , 2 устгасан
  1. 31 0
      himeeter-common/src/main/java/com/lij/himeeter/common/exception/UtilException.java
  2. 49 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/RepeatableFilter.java
  3. 77 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/RepeatedlyRequestWrapper.java
  4. 72 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/XssFilter.java
  5. 120 0
      himeeter-common/src/main/java/com/lij/himeeter/common/filter/XssHttpServletRequestWrapper.java
  6. 171 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/DateUtils.java
  7. 214 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/DictUtils.java
  8. 32 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/bean/BeanValidators.java
  9. 150 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/html/EscapeUtil.java
  10. 507 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/html/HTMLFilter.java
  11. 36 1
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/http/HttpHelper.java
  12. 60 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/IdUtils.java
  13. 98 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/Seq.java
  14. 450 0
      himeeter-common/src/main/java/com/lij/himeeter/common/utils/uuid/UUID.java
  15. 71 1
      himeeter-framework/src/main/java/com/lij/himeeter/framework/aspectj/RateLimiterAspect.java
  16. 38 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ApplicationConfig.java
  17. 92 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/CaptchaConfig.java
  18. 122 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/DruidConfig.java
  19. 56 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/FastJson2JsonRedisSerializer.java
  20. 68 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/FilterConfig.java
  21. 49 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/I18nConfig.java
  22. 66 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/KaptchaTextCreator.java
  23. 119 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/MyBatisConfig.java
  24. 77 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/RedisConfig.java
  25. 81 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ResourcesConfig.java
  26. 145 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/SecurityConfig.java
  27. 38 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ServerConfig.java
  28. 67 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ThreadPoolConfig.java
  29. 97 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/properties/DruidProperties.java
  30. 73 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/config/properties/PermitAllUrlProperties.java
  31. 35 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/datasource/DynamicDataSource.java
  32. 52 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/datasource/DynamicDataSourceContextHolder.java
  33. 59 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/interceptor/RepeatSubmitInterceptor.java
  34. 119 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/interceptor/impl/SameUrlDataInterceptor.java
  35. 39 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/security/AuthenticationEntryPointImpl.java
  36. 27 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/security/LogoutSuccessHandlerImpl.java
  37. 52 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/security/filter/JwtAuthenticationTokenFilter.java
  38. 220 0
      himeeter-framework/src/main/java/com/lij/himeeter/framework/web/service/TokenService.java
  39. 70 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysDictDataService.java
  40. 108 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysDictTypeService.java
  41. 154 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysMenuService.java
  42. 70 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysNoticeService.java
  43. 110 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysPostService.java
  44. 183 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysRoleService.java
  45. 58 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysUserOnlineService.java
  46. 217 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysUserService.java
  47. 112 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysDictDataServiceImpl.java
  48. 211 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysDictTypeServiceImpl.java
  49. 483 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysMenuServiceImpl.java
  50. 102 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysNoticeServiceImpl.java
  51. 172 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysPostServiceImpl.java
  52. 392 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysRoleServiceImpl.java
  53. 105 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysUserOnlineServiceImpl.java
  54. 502 0
      himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysUserServiceImpl.java

+ 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);
+    }
+}

+ 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);
+    }
+}

+ 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.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;
+    }
+
+}

+ 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);
+        }
+    }
+}

+ 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();
+    }
 }

+ 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();
+    }
+}

+ 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.ruoyi.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;
+    }
+
+}

+ 49 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/I18nConfig.java

@@ -0,0 +1,49 @@
+package com.lij.himeeter.framework.config;
+
+import com.lij.himeeter.common.constant.Constants;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  I18nConfig
+ * 创建时间 : 2025年01月02日 14:08
+ *
+ * 文件描述 : 资源文件配置加载
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class I18nConfig implements WebMvcConfigurer {
+
+    @Bean
+    public LocaleResolver localeResolver() {
+        SessionLocaleResolver slr = new SessionLocaleResolver();
+        // 默认语言
+        slr.setDefaultLocale(Constants.DEFAULT_LOCALE);
+        return slr;
+    }
+
+    @Bean
+    public LocaleChangeInterceptor localeChangeInterceptor() {
+        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
+        // 参数名
+        lci.setParamName("lang");
+        return lci;
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(localeChangeInterceptor());
+    }
+}

+ 66 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/KaptchaTextCreator.java

@@ -0,0 +1,66 @@
+package com.lij.himeeter.framework.config;
+
+import com.google.code.kaptcha.text.impl.DefaultTextCreator;
+
+import java.util.Random;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  KaptchaTextCreator
+ * 创建时间 : 2025年01月02日 14:10
+ *
+ * 文件描述 : 验证码文本生成器
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class KaptchaTextCreator extends DefaultTextCreator {
+
+    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
+
+    @Override
+    public String getText() {
+        Integer result = 0;
+        Random random = new Random();
+        int x = random.nextInt(10);
+        int y = random.nextInt(10);
+        StringBuilder suChinese = new StringBuilder();
+        int randomoperands = random.nextInt(3);
+        if (randomoperands == 0) {
+            result = x * y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("*");
+            suChinese.append(CNUMBERS[y]);
+        } else if (randomoperands == 1) {
+            if ((x != 0) && y % x == 0) {
+                result = y / x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("/");
+                suChinese.append(CNUMBERS[x]);
+            } else {
+                result = x + y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("+");
+                suChinese.append(CNUMBERS[y]);
+            }
+        } else {
+            if (x >= y) {
+                result = x - y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[y]);
+            } else {
+                result = y - x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[x]);
+            }
+        }
+        suChinese.append("=?@" + result);
+        return suChinese.toString();
+    }
+}

+ 119 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/MyBatisConfig.java

@@ -0,0 +1,119 @@
+package com.lij.himeeter.framework.config;
+
+import com.lij.himeeter.common.utils.StringUtils;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  MyBatisConfig
+ * 创建时间 : 2025年01月02日 14:12
+ *
+ * 文件描述 : Mybatis支持*匹配扫描包
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+@Configuration
+public class MyBatisConfig {
+
+    @Autowired
+    private Environment env;
+
+    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+    public static String setTypeAliasesPackage(String typeAliasesPackage) {
+        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+        List<String> allResult = new ArrayList<String>();
+        try {
+            for (String aliasesPackage : typeAliasesPackage.split(",")) {
+                List<String> result = new ArrayList<String>();
+                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+                Resource[] resources = resolver.getResources(aliasesPackage);
+                if (resources != null && resources.length > 0) {
+                    MetadataReader metadataReader = null;
+                    for (Resource resource : resources) {
+                        if (resource.isReadable()) {
+                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
+                            try {
+                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+                            } catch (ClassNotFoundException e) {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                }
+                if (result.size() > 0) {
+                    HashSet<String> hashResult = new HashSet<String>(result);
+                    allResult.addAll(hashResult);
+                }
+            }
+            if (allResult.size() > 0) {
+                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+            } else {
+                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return typeAliasesPackage;
+    }
+
+    public Resource[] resolveMapperLocations(String[] mapperLocations) {
+        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+        List<Resource> resources = new ArrayList<Resource>();
+        if (mapperLocations != null) {
+            for (String mapperLocation : mapperLocations) {
+                try {
+                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
+                    resources.addAll(Arrays.asList(mappers));
+                } catch (IOException e) {
+                    // ignore
+                }
+            }
+        }
+        return resources.toArray(new Resource[resources.size()]);
+    }
+
+    @Bean
+    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
+        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis.mapperLocations");
+        String configLocation = env.getProperty("mybatis.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 77 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/RedisConfig.java

@@ -0,0 +1,77 @@
+package com.lij.himeeter.framework.config;
+
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  RedisConfig
+ * 创建时间 : 2025年01月02日 14:14
+ *
+ * 文件描述 : redis配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport {
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return tonumber(current);\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return tonumber(current);";
+    }
+}

+ 81 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ResourcesConfig.java

@@ -0,0 +1,81 @@
+package com.lij.himeeter.framework.config;
+
+import com.lij.himeeter.common.config.HimeeterConfig;
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.framework.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.CacheControl;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ResourcesConfig
+ * 创建时间 : 2025年01月02日 14:16
+ *
+ * 文件描述 : 通用配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class ResourcesConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private RepeatSubmitInterceptor repeatSubmitInterceptor;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        /** 本地文件上传路径 */
+        registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**")
+                .addResourceLocations("file:" + HimeeterConfig.getProfile() + "/");
+
+        /** swagger配置 */
+        registry.addResourceHandler("/swagger-ui/**")
+                .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
+                .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());
+        ;
+    }
+
+    /**
+     * 自定义拦截规则
+     */
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+    }
+
+    /**
+     * 跨域配置
+     */
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        // 设置访问源地址
+        config.addAllowedOriginPattern("*");
+        // 设置访问源请求头
+        config.addAllowedHeader("*");
+        // 设置访问源请求方法
+        config.addAllowedMethod("*");
+        // 有效期 1800秒
+        config.setMaxAge(1800L);
+        // 添加映射路径,拦截一切请求
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config);
+        // 返回新的CorsFilter
+        return new CorsFilter(source);
+    }
+}

+ 145 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/SecurityConfig.java

@@ -0,0 +1,145 @@
+package com.lij.himeeter.framework.config;
+
+
+import com.lij.himeeter.framework.config.properties.PermitAllUrlProperties;
+import com.lij.himeeter.framework.security.AuthenticationEntryPointImpl;
+import com.lij.himeeter.framework.security.LogoutSuccessHandlerImpl;
+import com.lij.himeeter.framework.security.filter.JwtAuthenticationTokenFilter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.web.filter.CorsFilter;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SecurityConfig
+ * 创建时间 : 2025年01月02日 14:49
+ *
+ * 文件描述 : spring security配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
+@Configuration
+public class SecurityConfig {
+    /**
+     * 自定义用户认证逻辑
+     */
+    @Autowired
+    private UserDetailsService userDetailsService;
+
+    /**
+     * 认证失败处理类
+     */
+    @Autowired
+    private AuthenticationEntryPointImpl unauthorizedHandler;
+
+    /**
+     * 退出处理类
+     */
+    @Autowired
+    private LogoutSuccessHandlerImpl logoutSuccessHandler;
+
+    /**
+     * token认证过滤器
+     */
+    @Autowired
+    private JwtAuthenticationTokenFilter authenticationTokenFilter;
+
+    /**
+     * 跨域过滤器
+     */
+    @Autowired
+    private CorsFilter corsFilter;
+
+    /**
+     * 允许匿名访问的地址
+     */
+    @Autowired
+    private PermitAllUrlProperties permitAllUrl;
+
+    /**
+     * 身份验证实现
+     */
+    @Bean
+    public AuthenticationManager authenticationManager() {
+        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
+        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
+        daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
+        return new ProviderManager(daoAuthenticationProvider);
+    }
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Bean
+    protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
+        return httpSecurity
+                // CSRF禁用,因为不使用session
+                .csrf(csrf -> csrf.disable())
+                // 禁用HTTP响应标头
+                .headers((headersCustomizer) -> {
+                    headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
+                })
+                // 认证失败处理类
+                .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
+                // 基于token,所以不需要session
+                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                // 注解标记允许匿名访问的url
+                .authorizeHttpRequests((requests) -> {
+                    permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll());
+                    // 对于登录login 注册register 验证码captchaImage 允许匿名访问
+                    requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
+                            // 静态资源,可匿名访问
+                            .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
+                            .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()
+                            // 除上面外的所有请求全部需要鉴权认证
+                            .anyRequest().authenticated();
+                })
+                // 添加Logout filter
+                .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
+                // 添加JWT filter
+                .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
+                // 添加CORS filter
+                .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
+                .addFilterBefore(corsFilter, LogoutFilter.class)
+                .build();
+    }
+
+    /**
+     * 强散列哈希加密实现
+     */
+    @Bean
+    public BCryptPasswordEncoder bCryptPasswordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}

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

@@ -0,0 +1,38 @@
+package com.lij.himeeter.framework.config;
+
+import com.lij.himeeter.common.utils.ServletUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Component;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ServerConfig
+ * 创建时间 : 2025年01月02日 15:26
+ *
+ * 文件描述 : 服务相关配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Component
+public class ServerConfig {
+    /**
+     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+     *
+     * @return 服务地址
+     */
+    public String getUrl() {
+        HttpServletRequest request = ServletUtils.getRequest();
+        return getDomain(request);
+    }
+
+    public static String getDomain(HttpServletRequest request) {
+        StringBuffer url = request.getRequestURL();
+        String contextPath = request.getServletContext().getContextPath();
+        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+    }
+}

+ 67 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,67 @@
+package com.lij.himeeter.framework.config;
+
+import com.lij.himeeter.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ThreadPoolConfig
+ * 创建时间 : 2025年01月02日 15:26
+ *
+ * 文件描述 : 线程池配置
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class ThreadPoolConfig {
+
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 200;
+
+    // 队列最大长度
+    private int queueCapacity = 1000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 300;
+
+    @Bean(name = "threadPoolTaskExecutor")
+    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setCorePoolSize(corePoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setKeepAliveSeconds(keepAliveSeconds);
+        // 线程池对拒绝任务(无线程可用)的处理策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
+    /**
+     * 执行周期性或定时任务
+     */
+    @Bean(name = "scheduledExecutorService")
+    protected ScheduledExecutorService scheduledExecutorService() {
+        return new ScheduledThreadPoolExecutor(corePoolSize, new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), new ThreadPoolExecutor.CallerRunsPolicy()) {
+            @Override
+            protected void afterExecute(Runnable r, Throwable t) {
+                super.afterExecute(r, t);
+                Threads.printException(r, t);
+            }
+        };
+    }
+}

+ 97 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/properties/DruidProperties.java

@@ -0,0 +1,97 @@
+package com.lij.himeeter.framework.config.properties;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  DruidProperties
+ * 创建时间 : 2024年12月30日 18:45
+ *
+ * 文件描述 : druid 配置属性
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class DruidProperties {
+
+    @Value("${spring.datasource.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.druid.connectTimeout}")
+    private int connectTimeout;
+
+    @Value("${spring.datasource.druid.socketTimeout}")
+    private int socketTimeout;
+
+    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.druid.testOnReturn}")
+    private boolean testOnReturn;
+
+    public DruidDataSource dataSource(DruidDataSource datasource) {
+        /** 配置初始化大小、最小、最大 */
+        datasource.setInitialSize(initialSize);
+        datasource.setMaxActive(maxActive);
+        datasource.setMinIdle(minIdle);
+
+        /** 配置获取连接等待超时的时间 */
+        datasource.setMaxWait(maxWait);
+
+        /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */
+        datasource.setConnectTimeout(connectTimeout);
+
+        /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */
+        datasource.setSocketTimeout(socketTimeout);
+
+        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+        /**
+         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+         */
+        datasource.setValidationQuery(validationQuery);
+        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+        datasource.setTestWhileIdle(testWhileIdle);
+        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnBorrow(testOnBorrow);
+        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnReturn(testOnReturn);
+        return datasource;
+    }
+}

+ 73 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/config/properties/PermitAllUrlProperties.java

@@ -0,0 +1,73 @@
+package com.lij.himeeter.framework.config.properties;
+
+import com.lij.himeeter.common.annotation.Anonymous;
+import org.apache.commons.lang3.RegExUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.util.*;
+import java.util.regex.Pattern;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  PermitAllUrlProperties
+ * 创建时间 : 2024年12月30日 18:56
+ *
+ * 文件描述 : 设置Anonymous注解允许匿名访问的url
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware {
+
+    private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
+
+    private ApplicationContext applicationContext;
+
+    private List<String> urls = new ArrayList<>();
+
+    public String ASTERISK = "*";
+
+    @Override
+    public void afterPropertiesSet() {
+        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
+        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
+
+        map.keySet().forEach(info -> {
+            HandlerMethod handlerMethod = map.get(info);
+
+            // 获取方法上边的注解 替代path variable 为 *
+            Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
+            Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues()) //
+                    .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
+
+            // 获取类上边的注解, 替代path variable 为 *
+            Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
+            Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues())
+                    .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
+        });
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext context) throws BeansException {
+        this.applicationContext = context;
+    }
+
+    public List<String> getUrls() {
+        return urls;
+    }
+
+    public void setUrls(List<String> urls) {
+        this.urls = urls;
+    }
+}

+ 35 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/datasource/DynamicDataSource.java

@@ -0,0 +1,35 @@
+package com.lij.himeeter.framework.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  DynamicDataSource
+ * 创建时间 : 2024年12月30日 18:49
+ *
+ * 文件描述 : 无
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class DynamicDataSource extends AbstractRoutingDataSource {
+
+    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
+        super.setDefaultTargetDataSource(defaultTargetDataSource);
+        super.setTargetDataSources(targetDataSources);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    protected Object determineCurrentLookupKey() {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+
+}

+ 52 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,52 @@
+package com.lij.himeeter.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  DynamicDataSourceContextHolder
+ * 创建时间 : 2024年12月30日 18:50
+ *
+ * 文件描述 : 数据源切换处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class DynamicDataSourceContextHolder {
+
+    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源的变量
+     */
+    public static void setDataSourceType(String dsType) {
+        log.info("切换到{}数据源", dsType);
+        CONTEXT_HOLDER.set(dsType);
+    }
+
+    /**
+     * 获得数据源的变量
+     */
+    public static String getDataSourceType() {
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType() {
+        CONTEXT_HOLDER.remove();
+    }
+
+}

+ 59 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/interceptor/RepeatSubmitInterceptor.java

@@ -0,0 +1,59 @@
+package com.lij.himeeter.framework.interceptor;
+
+import com.alibaba.fastjson2.JSON;
+import com.lij.himeeter.common.annotation.RepeatSubmit;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.utils.ServletUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.lang.reflect.Method;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  RepeatSubmitInterceptor
+ * 创建时间 : 2025年01月02日 14:17
+ *
+ * 文件描述 : 无
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+@Component
+public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        if (handler instanceof HandlerMethod) {
+            HandlerMethod handlerMethod = (HandlerMethod) handler;
+            Method method = handlerMethod.getMethod();
+            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
+            if (annotation != null) {
+                if (this.isRepeatSubmit(request, annotation)) {
+                    AjaxResult ajaxResult = AjaxResult.error(annotation.message());
+                    ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 验证是否重复提交由子类实现具体的防重复提交的规则
+     *
+     * @param request    请求信息
+     * @param annotation 防重复注解参数
+     * @return 结果
+     * @throws Exception
+     */
+    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
+}

+ 119 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/interceptor/impl/SameUrlDataInterceptor.java

@@ -0,0 +1,119 @@
+package com.lij.himeeter.framework.interceptor.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.lij.himeeter.common.annotation.RepeatSubmit;
+import com.lij.himeeter.common.constant.CacheConstants;
+import com.lij.himeeter.common.core.RedisCache;
+import com.lij.himeeter.common.filter.RepeatedlyRequestWrapper;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.http.HttpHelper;
+import com.lij.himeeter.framework.interceptor.RepeatSubmitInterceptor;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SameUrlDataInterceptor
+ * 创建时间 : 2025年01月02日 14:37
+ *
+ * 文件描述 : 无
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
+
+    public final String REPEAT_PARAMS = "repeatParams";
+
+    public final String REPEAT_TIME = "repeatTime";
+
+    // 令牌自定义标识
+    @Value("${token.header}")
+    private String header;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 验证是否重复提交由子类实现具体的防重复提交的规则
+     *
+     * @param request    请求信息
+     * @param annotation 防重复注解参数
+     * @return 结果
+     * @throws Exception
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) {
+        String nowParams = "";
+        if (request instanceof RepeatedlyRequestWrapper) {
+            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
+            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
+        }
+
+        // body参数为空,获取Parameter的数据
+        if (StringUtils.isEmpty(nowParams)) {
+            nowParams = JSON.toJSONString(request.getParameterMap());
+        }
+
+        Map<String, Object> nowDataMap = new HashMap<>();
+        nowDataMap.put(REPEAT_PARAMS, nowParams);
+        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+        // 请求地址(作为存放cache的key值)
+        String url = request.getRequestURI();
+
+        // 唯一值(没有消息头则使用请求地址)
+        String submitKey = StringUtils.trimToEmpty(request.getHeader(header));
+
+        // 唯一标识(指定key + url + 消息头)
+        String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
+
+        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
+        if (sessionObj != null) {
+            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+            if (sessionMap.containsKey(url)) {
+                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) {
+                    return true;
+                }
+            }
+        }
+        Map<String, Object> cacheMap = new HashMap<String, Object>();
+        cacheMap.put(url, nowDataMap);
+        redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
+        return false;
+    }
+
+
+    /**
+     * 判断参数是否相同
+     */
+    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
+        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+        String preParams = (String) preMap.get(REPEAT_PARAMS);
+        return nowParams.equals(preParams);
+    }
+
+    /**
+     * 判断两次间隔时间
+     */
+    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {
+        long time1 = (Long) nowMap.get(REPEAT_TIME);
+        long time2 = (Long) preMap.get(REPEAT_TIME);
+        if ((time1 - time2) < interval) {
+            return true;
+        }
+        return false;
+    }
+}

+ 39 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/security/AuthenticationEntryPointImpl.java

@@ -0,0 +1,39 @@
+package com.lij.himeeter.framework.security;
+
+import com.alibaba.fastjson2.JSON;
+import com.lij.himeeter.common.constant.HttpStatus;
+import com.lij.himeeter.common.core.domain.AjaxResult;
+import com.lij.himeeter.common.utils.ServletUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  AuthenticationEntryPointImpl
+ * 创建时间 : 2025年01月02日 14:51
+ *
+ * 文件描述 : 认证失败处理类 返回未授权
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
+        int code = HttpStatus.UNAUTHORIZED;
+        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
+        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
+    }
+}

+ 27 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/security/LogoutSuccessHandlerImpl.java

@@ -0,0 +1,27 @@
+package com.lij.himeeter.framework.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.token.TokenService;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  LogoutSuccessHandlerImpl
+ * 创建时间 : 2025年01月02日 14:53
+ *
+ * 文件描述 : 无
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Configuration
+public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
+
+    @Autowired
+    private TokenService tokenService;
+}

+ 52 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/security/filter/JwtAuthenticationTokenFilter.java

@@ -0,0 +1,52 @@
+package com.lij.himeeter.framework.security.filter;
+
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.framework.web.service.TokenService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  JwtAuthenticationTokenFilter
+ * 创建时间 : 2025年01月02日 15:23
+ *
+ * 文件描述 : token过滤器 验证token有效性
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Component
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        LoginUser loginUser = tokenService.getLoginUser(request);
+        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
+            tokenService.verifyToken(loginUser);
+            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+        }
+        chain.doFilter(request, response);
+    }
+}

+ 220 - 0
himeeter-framework/src/main/java/com/lij/himeeter/framework/web/service/TokenService.java

@@ -0,0 +1,220 @@
+package com.lij.himeeter.framework.web.service;
+
+import com.lij.himeeter.common.constant.CacheConstants;
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.core.RedisCache;
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.utils.ServletUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.ip.AddressUtils;
+import com.lij.himeeter.common.utils.ip.IpUtils;
+import com.lij.himeeter.common.utils.uuid.IdUtils;
+import eu.bitwalker.useragentutils.UserAgent;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  TokenService
+ * 创建时间 : 2025年01月02日 14:54
+ *
+ * 文件描述 : token验证处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Component
+public class TokenService {
+    private static final Logger log = LoggerFactory.getLogger(TokenService.class);
+
+    // 令牌自定义标识
+    @Value("${token.header}")
+    private String header;
+
+    // 令牌秘钥
+    @Value("${token.secret}")
+    private String secret;
+
+    // 令牌有效期(默认30分钟)
+    @Value("${token.expireTime}")
+    private int expireTime;
+
+    protected static final long MILLIS_SECOND = 1000;
+
+    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
+
+    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 获取用户身份信息
+     *
+     * @return 用户信息
+     */
+    public LoginUser getLoginUser(HttpServletRequest request) {
+        // 获取请求携带的令牌
+        String token = getToken(request);
+        if (StringUtils.isNotEmpty(token)) {
+            try {
+                Claims claims = parseToken(token);
+                // 解析对应的权限以及用户信息
+                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
+                String userKey = getTokenKey(uuid);
+                LoginUser user = redisCache.getCacheObject(userKey);
+                return user;
+            } catch (Exception e) {
+                log.error("获取用户信息异常'{}'", e.getMessage());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 设置用户身份信息
+     */
+    public void setLoginUser(LoginUser loginUser) {
+        if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
+            refreshToken(loginUser);
+        }
+    }
+
+    /**
+     * 删除用户身份信息
+     */
+    public void delLoginUser(String token) {
+        if (StringUtils.isNotEmpty(token)) {
+            String userKey = getTokenKey(token);
+            redisCache.deleteObject(userKey);
+        }
+    }
+
+    /**
+     * 创建令牌
+     *
+     * @param loginUser 用户信息
+     * @return 令牌
+     */
+    public String createToken(LoginUser loginUser) {
+        String token = IdUtils.fastUUID();
+        loginUser.setToken(token);
+        setUserAgent(loginUser);
+        refreshToken(loginUser);
+
+        Map<String, Object> claims = new HashMap<>();
+        claims.put(Constants.LOGIN_USER_KEY, token);
+        return createToken(claims);
+    }
+
+    /**
+     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
+     *
+     * @param loginUser
+     * @return 令牌
+     */
+    public void verifyToken(LoginUser loginUser) {
+        long expireTime = loginUser.getExpireTime();
+        long currentTime = System.currentTimeMillis();
+        if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
+            refreshToken(loginUser);
+        }
+    }
+
+    /**
+     * 刷新令牌有效期
+     *
+     * @param loginUser 登录信息
+     */
+    public void refreshToken(LoginUser loginUser) {
+        loginUser.setLoginTime(System.currentTimeMillis());
+        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
+        // 根据uuid将loginUser缓存
+        String userKey = getTokenKey(loginUser.getToken());
+        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
+    }
+
+    /**
+     * 设置用户代理信息
+     *
+     * @param loginUser 登录信息
+     */
+    public void setUserAgent(LoginUser loginUser) {
+        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+        String ip = IpUtils.getIpAddr();
+        loginUser.setIpaddr(ip);
+        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
+        loginUser.setBrowser(userAgent.getBrowser().getName());
+        loginUser.setOs(userAgent.getOperatingSystem().getName());
+    }
+
+    /**
+     * 从数据声明生成令牌
+     *
+     * @param claims 数据声明
+     * @return 令牌
+     */
+    private String createToken(Map<String, Object> claims) {
+        String token = Jwts.builder()
+                .setClaims(claims)
+                .signWith(SignatureAlgorithm.HS512, secret).compact();
+        return token;
+    }
+
+    /**
+     * 从令牌中获取数据声明
+     *
+     * @param token 令牌
+     * @return 数据声明
+     */
+    private Claims parseToken(String token) {
+        return Jwts.parser()
+                .setSigningKey(secret)
+                .parseClaimsJws(token)
+                .getBody();
+    }
+
+    /**
+     * 从令牌中获取用户名
+     *
+     * @param token 令牌
+     * @return 用户名
+     */
+    public String getUsernameFromToken(String token) {
+        Claims claims = parseToken(token);
+        return claims.getSubject();
+    }
+
+    /**
+     * 获取请求token
+     *
+     * @param request
+     * @return token
+     */
+    private String getToken(HttpServletRequest request) {
+        String token = request.getHeader(header);
+        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
+            token = token.replace(Constants.TOKEN_PREFIX, "");
+        }
+        return token;
+    }
+
+    private String getTokenKey(String uuid) {
+        return CacheConstants.LOGIN_TOKEN_KEY + uuid;
+    }
+}

+ 70 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysDictDataService.java

@@ -0,0 +1,70 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.common.core.domain.entity.SysDictData;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysDictDataService
+ * 创建时间 : 2024年12月31日 16:20
+ *
+ * 文件描述 : 字典 业务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysDictDataService {
+
+    /**
+     * 根据条件分页查询字典数据
+     *
+     * @param dictData 字典数据信息
+     * @return 字典数据集合信息
+     */
+    List<SysDictData> selectDictDataList(SysDictData dictData);
+
+    /**
+     * 根据字典类型和字典键值查询字典数据信息
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典键值
+     * @return 字典标签
+     */
+    String selectDictLabel(String dictType, String dictValue);
+
+    /**
+     * 根据字典数据ID查询信息
+     *
+     * @param dictCode 字典数据ID
+     * @return 字典数据
+     */
+    SysDictData selectDictDataById(Long dictCode);
+
+    /**
+     * 批量删除字典数据信息
+     *
+     * @param dictCodes 需要删除的字典数据ID
+     */
+    void deleteDictDataByIds(Long[] dictCodes);
+
+    /**
+     * 新增保存字典数据信息
+     *
+     * @param dictData 字典数据信息
+     * @return 结果
+     */
+    int insertDictData(SysDictData dictData);
+
+    /**
+     * 修改保存字典数据信息
+     *
+     * @param dictData 字典数据信息
+     * @return 结果
+     */
+    int updateDictData(SysDictData dictData);
+}

+ 108 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysDictTypeService.java

@@ -0,0 +1,108 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.common.core.domain.entity.SysDictData;
+import com.lij.himeeter.common.core.domain.entity.SysDictType;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysDictTypeService
+ * 创建时间 : 2024年12月31日 16:20
+ *
+ * 文件描述 : 字典 业务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysDictTypeService {
+
+    /**
+     * 根据条件分页查询字典类型
+     *
+     * @param dictType 字典类型信息
+     * @return 字典类型集合信息
+     */
+    List<SysDictType> selectDictTypeList(SysDictType dictType);
+
+    /**
+     * 根据所有字典类型
+     *
+     * @return 字典类型集合信息
+     */
+    List<SysDictType> selectDictTypeAll();
+
+    /**
+     * 根据字典类型查询字典数据
+     *
+     * @param dictType 字典类型
+     * @return 字典数据集合信息
+     */
+    List<SysDictData> selectDictDataByType(String dictType);
+
+    /**
+     * 根据字典类型ID查询信息
+     *
+     * @param dictId 字典类型ID
+     * @return 字典类型
+     */
+    SysDictType selectDictTypeById(Long dictId);
+
+    /**
+     * 根据字典类型查询信息
+     *
+     * @param dictType 字典类型
+     * @return 字典类型
+     */
+    SysDictType selectDictTypeByType(String dictType);
+
+    /**
+     * 批量删除字典信息
+     *
+     * @param dictIds 需要删除的字典ID
+     */
+    void deleteDictTypeByIds(Long[] dictIds);
+
+    /**
+     * 加载字典缓存数据
+     */
+    void loadingDictCache();
+
+    /**
+     * 清空字典缓存数据
+     */
+    void clearDictCache();
+
+    /**
+     * 重置字典缓存数据
+     */
+    void resetDictCache();
+
+    /**
+     * 新增保存字典类型信息
+     *
+     * @param dictType 字典类型信息
+     * @return 结果
+     */
+    int insertDictType(SysDictType dictType);
+
+    /**
+     * 修改保存字典类型信息
+     *
+     * @param dictType 字典类型信息
+     * @return 结果
+     */
+    int updateDictType(SysDictType dictType);
+
+    /**
+     * 校验字典类型称是否唯一
+     *
+     * @param dictType 字典类型
+     * @return 结果
+     */
+    boolean checkDictTypeUnique(SysDictType dictType);
+}

+ 154 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysMenuService.java

@@ -0,0 +1,154 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.common.core.domain.TreeSelect;
+import com.lij.himeeter.common.core.domain.entity.SysMenu;
+import com.lij.himeeter.system.domin.vo.RouterVO;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysMenuService
+ * 创建时间 : 2024年12月31日 16:21
+ *
+ * 文件描述 : 菜单 业务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysMenuService {
+
+    /**
+     * 根据用户查询系统菜单列表
+     *
+     * @param userId 用户ID
+     * @return 菜单列表
+     */
+    List<SysMenu> selectMenuList(Long userId);
+
+    /**
+     * 根据用户查询系统菜单列表
+     *
+     * @param menu   菜单信息
+     * @param userId 用户ID
+     * @return 菜单列表
+     */
+    List<SysMenu> selectMenuList(SysMenu menu, Long userId);
+
+    /**
+     * 根据用户ID查询权限
+     *
+     * @param userId 用户ID
+     * @return 权限列表
+     */
+    Set<String> selectMenuPermsByUserId(Long userId);
+
+    /**
+     * 根据角色ID查询权限
+     *
+     * @param roleId 角色ID
+     * @return 权限列表
+     */
+    Set<String> selectMenuPermsByRoleId(Long roleId);
+
+    /**
+     * 根据用户ID查询菜单树信息
+     *
+     * @param userId 用户ID
+     * @return 菜单列表
+     */
+    List<SysMenu> selectMenuTreeByUserId(Long userId);
+
+    /**
+     * 根据角色ID查询菜单树信息
+     *
+     * @param roleId 角色ID
+     * @return 选中菜单列表
+     */
+    List<Long> selectMenuListByRoleId(Long roleId);
+
+    /**
+     * 构建前端路由所需要的菜单
+     *
+     * @param menus 菜单列表
+     * @return 路由列表
+     */
+    List<RouterVO> buildMenus(List<SysMenu> menus);
+
+    /**
+     * 构建前端所需要树结构
+     *
+     * @param menus 菜单列表
+     * @return 树结构列表
+     */
+    List<SysMenu> buildMenuTree(List<SysMenu> menus);
+
+    /**
+     * 构建前端所需要下拉树结构
+     *
+     * @param menus 菜单列表
+     * @return 下拉树结构列表
+     */
+    List<TreeSelect> buildMenuTreeSelect(List<SysMenu> menus);
+
+    /**
+     * 根据菜单ID查询信息
+     *
+     * @param menuId 菜单ID
+     * @return 菜单信息
+     */
+    SysMenu selectMenuById(Long menuId);
+
+    /**
+     * 是否存在菜单子节点
+     *
+     * @param menuId 菜单ID
+     * @return 结果 true 存在 false 不存在
+     */
+    boolean hasChildByMenuId(Long menuId);
+
+    /**
+     * 查询菜单是否存在角色
+     *
+     * @param menuId 菜单ID
+     * @return 结果 true 存在 false 不存在
+     */
+    boolean checkMenuExistRole(Long menuId);
+
+    /**
+     * 新增保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    int insertMenu(SysMenu menu);
+
+    /**
+     * 修改保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    int updateMenu(SysMenu menu);
+
+    /**
+     * 删除菜单管理信息
+     *
+     * @param menuId 菜单ID
+     * @return 结果
+     */
+    int deleteMenuById(Long menuId);
+
+    /**
+     * 校验菜单名称是否唯一
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    boolean checkMenuNameUnique(SysMenu menu);
+}

+ 70 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysNoticeService.java

@@ -0,0 +1,70 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.system.domin.SysNotice;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysNoticeService
+ * 创建时间 : 2024年12月31日 16:21
+ *
+ * 文件描述 : 公告 服务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysNoticeService {
+
+    /**
+     * 查询公告信息
+     *
+     * @param noticeId 公告ID
+     * @return 公告信息
+     */
+    SysNotice selectNoticeById(Long noticeId);
+
+    /**
+     * 查询公告列表
+     *
+     * @param notice 公告信息
+     * @return 公告集合
+     */
+    List<SysNotice> selectNoticeList(SysNotice notice);
+
+    /**
+     * 新增公告
+     *
+     * @param notice 公告信息
+     * @return 结果
+     */
+    int insertNotice(SysNotice notice);
+
+    /**
+     * 修改公告
+     *
+     * @param notice 公告信息
+     * @return 结果
+     */
+    int updateNotice(SysNotice notice);
+
+    /**
+     * 删除公告信息
+     *
+     * @param noticeId 公告ID
+     * @return 结果
+     */
+    int deleteNoticeById(Long noticeId);
+
+    /**
+     * 批量删除公告信息
+     *
+     * @param noticeIds 需要删除的公告ID
+     * @return 结果
+     */
+    int deleteNoticeByIds(Long[] noticeIds);
+}

+ 110 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysPostService.java

@@ -0,0 +1,110 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.system.domin.SysPost;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysPostService
+ * 创建时间 : 2024年12月31日 16:22
+ *
+ * 文件描述 : 岗位信息 服务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysPostService {
+
+    /**
+     * 查询岗位信息集合
+     *
+     * @param post 岗位信息
+     * @return 岗位列表
+     */
+    List<SysPost> selectPostList(SysPost post);
+
+    /**
+     * 查询所有岗位
+     *
+     * @return 岗位列表
+     */
+    List<SysPost> selectPostAll();
+
+    /**
+     * 通过岗位ID查询岗位信息
+     *
+     * @param postId 岗位ID
+     * @return 角色对象信息
+     */
+    SysPost selectPostById(Long postId);
+
+    /**
+     * 根据用户ID获取岗位选择框列表
+     *
+     * @param userId 用户ID
+     * @return 选中岗位ID列表
+     */
+    List<Long> selectPostListByUserId(Long userId);
+
+    /**
+     * 校验岗位名称
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    boolean checkPostNameUnique(SysPost post);
+
+    /**
+     * 校验岗位编码
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    boolean checkPostCodeUnique(SysPost post);
+
+    /**
+     * 通过岗位ID查询岗位使用数量
+     *
+     * @param postId 岗位ID
+     * @return 结果
+     */
+    int countUserPostById(Long postId);
+
+    /**
+     * 删除岗位信息
+     *
+     * @param postId 岗位ID
+     * @return 结果
+     */
+    int deletePostById(Long postId);
+
+    /**
+     * 批量删除岗位信息
+     *
+     * @param postIds 需要删除的岗位ID
+     * @return 结果
+     */
+    int deletePostByIds(Long[] postIds);
+
+    /**
+     * 新增保存岗位信息
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    int insertPost(SysPost post);
+
+    /**
+     * 修改保存岗位信息
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    int updatePost(SysPost post);
+
+}

+ 183 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysRoleService.java

@@ -0,0 +1,183 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.common.core.domain.entity.SysRole;
+import com.lij.himeeter.system.domin.SysUserRole;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysRoleService
+ * 创建时间 : 2024年12月31日 16:23
+ *
+ * 文件描述 : 角色业务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysRoleService {
+
+    /**
+     * 根据条件分页查询角色数据
+     *
+     * @param role 角色信息
+     * @return 角色数据集合信息
+     */
+    List<SysRole> selectRoleList(SysRole role);
+
+    /**
+     * 根据用户ID查询角色列表
+     *
+     * @param userId 用户ID
+     * @return 角色列表
+     */
+    List<SysRole> selectRolesByUserId(Long userId);
+
+    /**
+     * 根据用户ID查询角色权限
+     *
+     * @param userId 用户ID
+     * @return 权限列表
+     */
+    Set<String> selectRolePermissionByUserId(Long userId);
+
+    /**
+     * 查询所有角色
+     *
+     * @return 角色列表
+     */
+    List<SysRole> selectRoleAll();
+
+    /**
+     * 根据用户ID获取角色选择框列表
+     *
+     * @param userId 用户ID
+     * @return 选中角色ID列表
+     */
+    List<Long> selectRoleListByUserId(Long userId);
+
+    /**
+     * 通过角色ID查询角色
+     *
+     * @param roleId 角色ID
+     * @return 角色对象信息
+     */
+    SysRole selectRoleById(Long roleId);
+
+    /**
+     * 校验角色名称是否唯一
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    boolean checkRoleNameUnique(SysRole role);
+
+    /**
+     * 校验角色权限是否唯一
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    boolean checkRoleKeyUnique(SysRole role);
+
+    /**
+     * 校验角色是否允许操作
+     *
+     * @param role 角色信息
+     */
+    void checkRoleAllowed(SysRole role);
+
+    /**
+     * 校验角色是否有数据权限
+     *
+     * @param roleIds 角色id
+     */
+    void checkRoleDataScope(Long... roleIds);
+
+    /**
+     * 通过角色ID查询角色使用数量
+     *
+     * @param roleId 角色ID
+     * @return 结果
+     */
+    int countUserRoleByRoleId(Long roleId);
+
+    /**
+     * 新增保存角色信息
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    int insertRole(SysRole role);
+
+    /**
+     * 修改保存角色信息
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    int updateRole(SysRole role);
+
+    /**
+     * 修改角色状态
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    int updateRoleStatus(SysRole role);
+
+    /**
+     * 修改数据权限信息
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    int authDataScope(SysRole role);
+
+    /**
+     * 通过角色ID删除角色
+     *
+     * @param roleId 角色ID
+     * @return 结果
+     */
+    int deleteRoleById(Long roleId);
+
+    /**
+     * 批量删除角色信息
+     *
+     * @param roleIds 需要删除的角色ID
+     * @return 结果
+     */
+    int deleteRoleByIds(Long[] roleIds);
+
+    /**
+     * 取消授权用户角色
+     *
+     * @param userRole 用户和角色关联信息
+     * @return 结果
+     */
+    int deleteAuthUser(SysUserRole userRole);
+
+    /**
+     * 批量取消授权用户角色
+     *
+     * @param roleId  角色ID
+     * @param userIds 需要取消授权的用户数据ID
+     * @return 结果
+     */
+    int deleteAuthUsers(Long roleId, Long[] userIds);
+
+    /**
+     * 批量选择授权用户角色
+     *
+     * @param roleId  角色ID
+     * @param userIds 需要删除的用户数据ID
+     * @return 结果
+     */
+    int insertAuthUsers(Long roleId, Long[] userIds);
+}

+ 58 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysUserOnlineService.java

@@ -0,0 +1,58 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.system.domin.SysUserOnline;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysUserOnlineService
+ * 创建时间 : 2024年12月31日 16:23
+ *
+ * 文件描述 : 在线用户 服务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysUserOnlineService {
+
+    /**
+     * 通过登录地址查询信息
+     *
+     * @param ipaddr 登录地址
+     * @param user   用户信息
+     * @return 在线用户信息
+     */
+    SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user);
+
+    /**
+     * 通过用户名称查询信息
+     *
+     * @param userName 用户名称
+     * @param user     用户信息
+     * @return 在线用户信息
+     */
+    SysUserOnline selectOnlineByUserName(String userName, LoginUser user);
+
+    /**
+     * 通过登录地址/用户名称查询信息
+     *
+     * @param ipaddr   登录地址
+     * @param userName 用户名称
+     * @param user     用户信息
+     * @return 在线用户信息
+     */
+    SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user);
+
+    /**
+     * 设置在线用户信息
+     *
+     * @param user 用户信息
+     * @return 在线用户
+     */
+    SysUserOnline loginUserToUserOnline(LoginUser user);
+
+}

+ 217 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/ISysUserService.java

@@ -0,0 +1,217 @@
+package com.lij.himeeter.system.service;
+
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  ISysUserService
+ * 创建时间 : 2024年12月31日 16:23
+ *
+ * 文件描述 : 用户 业务层
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+public interface ISysUserService {
+
+
+    /**
+     * 根据条件分页查询用户列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    List<SysUser> selectUserList(SysUser user);
+
+    /**
+     * 根据条件分页查询已分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    List<SysUser> selectAllocatedList(SysUser user);
+
+    /**
+     * 根据条件分页查询未分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    List<SysUser> selectUnallocatedList(SysUser user);
+
+    /**
+     * 通过用户名查询用户
+     *
+     * @param userName 用户名
+     * @return 用户对象信息
+     */
+    SysUser selectUserByUserName(String userName);
+
+    /**
+     * 通过用户ID查询用户
+     *
+     * @param userId 用户ID
+     * @return 用户对象信息
+     */
+    SysUser selectUserById(Long userId);
+
+    /**
+     * 根据用户ID查询用户所属角色组
+     *
+     * @param userName 用户名
+     * @return 结果
+     */
+    String selectUserRoleGroup(String userName);
+
+    /**
+     * 根据用户ID查询用户所属岗位组
+     *
+     * @param userName 用户名
+     * @return 结果
+     */
+    String selectUserPostGroup(String userName);
+
+    /**
+     * 校验用户名称是否唯一
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    boolean checkUserNameUnique(SysUser user);
+
+    /**
+     * 校验手机号码是否唯一
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    boolean checkPhoneUnique(SysUser user);
+
+    /**
+     * 校验email是否唯一
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    boolean checkEmailUnique(SysUser user);
+
+    /**
+     * 校验用户是否允许操作
+     *
+     * @param user 用户信息
+     */
+    void checkUserAllowed(SysUser user);
+
+    /**
+     * 校验用户是否有数据权限
+     *
+     * @param userId 用户id
+     */
+    void checkUserDataScope(Long userId);
+
+    /**
+     * 新增用户信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    int insertUser(SysUser user);
+
+    /**
+     * 注册用户信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    boolean registerUser(SysUser user);
+
+    /**
+     * 修改用户信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    int updateUser(SysUser user);
+
+    /**
+     * 用户授权角色
+     *
+     * @param userId  用户ID
+     * @param roleIds 角色组
+     */
+    void insertUserAuth(Long userId, Long[] roleIds);
+
+    /**
+     * 修改用户状态
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    int updateUserStatus(SysUser user);
+
+    /**
+     * 修改用户基本信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    int updateUserProfile(SysUser user);
+
+    /**
+     * 修改用户头像
+     *
+     * @param userName 用户名
+     * @param avatar   头像地址
+     * @return 结果
+     */
+    boolean updateUserAvatar(String userName, String avatar);
+
+    /**
+     * 重置用户密码
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    int resetPwd(SysUser user);
+
+    /**
+     * 重置用户密码
+     *
+     * @param userName 用户名
+     * @param password 密码
+     * @return 结果
+     */
+    int resetUserPwd(String userName, String password);
+
+    /**
+     * 通过用户ID删除用户
+     *
+     * @param userId 用户ID
+     * @return 结果
+     */
+    int deleteUserById(Long userId);
+
+    /**
+     * 批量删除用户信息
+     *
+     * @param userIds 需要删除的用户ID
+     * @return 结果
+     */
+    int deleteUserByIds(Long[] userIds);
+
+    /**
+     * 导入用户数据
+     *
+     * @param userList        用户数据列表
+     * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
+     * @param operName        操作用户
+     * @return 结果
+     */
+    String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName);
+}

+ 112 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysDictDataServiceImpl.java

@@ -0,0 +1,112 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.common.core.domain.entity.SysDictData;
+import com.lij.himeeter.common.utils.DictUtils;
+import com.lij.himeeter.system.mapper.SysDictDataMapper;
+import com.lij.himeeter.system.service.ISysDictDataService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysDictDataServiceImpl
+ * 创建时间 : 2024年12月31日 16:55
+ *
+ * 文件描述 : 字典 业务层处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Service
+public class SysDictDataServiceImpl implements ISysDictDataService {
+
+    @Autowired
+    private SysDictDataMapper dictDataMapper;
+
+    /**
+     * 根据条件分页查询字典数据
+     *
+     * @param dictData 字典数据信息
+     * @return 字典数据集合信息
+     */
+    @Override
+    public List<SysDictData> selectDictDataList(SysDictData dictData) {
+        return dictDataMapper.selectDictDataList(dictData);
+    }
+
+    /**
+     * 根据字典类型和字典键值查询字典数据信息
+     *
+     * @param dictType  字典类型
+     * @param dictValue 字典键值
+     * @return 字典标签
+     */
+    @Override
+    public String selectDictLabel(String dictType, String dictValue) {
+        return dictDataMapper.selectDictLabel(dictType, dictValue);
+    }
+
+    /**
+     * 根据字典数据ID查询信息
+     *
+     * @param dictCode 字典数据ID
+     * @return 字典数据
+     */
+    @Override
+    public SysDictData selectDictDataById(Long dictCode) {
+        return dictDataMapper.selectDictDataById(dictCode);
+    }
+
+    /**
+     * 批量删除字典数据信息
+     *
+     * @param dictCodes 需要删除的字典数据ID
+     */
+    @Override
+    public void deleteDictDataByIds(Long[] dictCodes) {
+        for (Long dictCode : dictCodes) {
+            SysDictData data = selectDictDataById(dictCode);
+            dictDataMapper.deleteDictDataById(dictCode);
+            List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(data.getDictType());
+            DictUtils.setDictCache(data.getDictType(), dictDatas);
+        }
+    }
+
+    /**
+     * 新增保存字典数据信息
+     *
+     * @param data 字典数据信息
+     * @return 结果
+     */
+    @Override
+    public int insertDictData(SysDictData data) {
+        int row = dictDataMapper.insertDictData(data);
+        if (row > 0) {
+            List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(data.getDictType());
+            DictUtils.setDictCache(data.getDictType(), dictDatas);
+        }
+        return row;
+    }
+
+    /**
+     * 修改保存字典数据信息
+     *
+     * @param data 字典数据信息
+     * @return 结果
+     */
+    @Override
+    public int updateDictData(SysDictData data) {
+        int row = dictDataMapper.updateDictData(data);
+        if (row > 0) {
+            List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(data.getDictType());
+            DictUtils.setDictCache(data.getDictType(), dictDatas);
+        }
+        return row;
+    }
+}

+ 211 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysDictTypeServiceImpl.java

@@ -0,0 +1,211 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.common.constant.UserConstants;
+import com.lij.himeeter.common.core.domain.entity.SysDictData;
+import com.lij.himeeter.common.core.domain.entity.SysDictType;
+import com.lij.himeeter.common.exception.ServiceException;
+import com.lij.himeeter.common.utils.DictUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.mapper.SysDictDataMapper;
+import com.lij.himeeter.system.mapper.SysDictTypeMapper;
+import com.lij.himeeter.system.service.ISysDictTypeService;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysDictTypeServiceImpl
+ * 创建时间 : 2024年12月31日 16:55
+ *
+ * 文件描述 : 字典 业务层处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Service
+public class SysDictTypeServiceImpl implements ISysDictTypeService {
+    @Autowired
+    private SysDictTypeMapper dictTypeMapper;
+
+    @Autowired
+    private SysDictDataMapper dictDataMapper;
+
+    /**
+     * 项目启动时,初始化字典到缓存
+     */
+    @PostConstruct
+    public void init() {
+        loadingDictCache();
+    }
+
+    /**
+     * 根据条件分页查询字典类型
+     *
+     * @param dictType 字典类型信息
+     * @return 字典类型集合信息
+     */
+    @Override
+    public List<SysDictType> selectDictTypeList(SysDictType dictType) {
+        return dictTypeMapper.selectDictTypeList(dictType);
+    }
+
+    /**
+     * 根据所有字典类型
+     *
+     * @return 字典类型集合信息
+     */
+    @Override
+    public List<SysDictType> selectDictTypeAll() {
+        return dictTypeMapper.selectDictTypeAll();
+    }
+
+    /**
+     * 根据字典类型查询字典数据
+     *
+     * @param dictType 字典类型
+     * @return 字典数据集合信息
+     */
+    @Override
+    public List<SysDictData> selectDictDataByType(String dictType) {
+        List<SysDictData> dictDatas = DictUtils.getDictCache(dictType);
+        if (StringUtils.isNotEmpty(dictDatas)) {
+            return dictDatas;
+        }
+        dictDatas = dictDataMapper.selectDictDataByType(dictType);
+        if (StringUtils.isNotEmpty(dictDatas)) {
+            DictUtils.setDictCache(dictType, dictDatas);
+            return dictDatas;
+        }
+        return null;
+    }
+
+    /**
+     * 根据字典类型ID查询信息
+     *
+     * @param dictId 字典类型ID
+     * @return 字典类型
+     */
+    @Override
+    public SysDictType selectDictTypeById(Long dictId) {
+        return dictTypeMapper.selectDictTypeById(dictId);
+    }
+
+    /**
+     * 根据字典类型查询信息
+     *
+     * @param dictType 字典类型
+     * @return 字典类型
+     */
+    @Override
+    public SysDictType selectDictTypeByType(String dictType) {
+        return dictTypeMapper.selectDictTypeByType(dictType);
+    }
+
+    /**
+     * 批量删除字典类型信息
+     *
+     * @param dictIds 需要删除的字典ID
+     */
+    @Override
+    public void deleteDictTypeByIds(Long[] dictIds) {
+        for (Long dictId : dictIds) {
+            SysDictType dictType = selectDictTypeById(dictId);
+            if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0) {
+                throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName()));
+            }
+            dictTypeMapper.deleteDictTypeById(dictId);
+            DictUtils.removeDictCache(dictType.getDictType());
+        }
+    }
+
+    /**
+     * 加载字典缓存数据
+     */
+    @Override
+    public void loadingDictCache() {
+        SysDictData dictData = new SysDictData();
+        dictData.setStatus("0");
+        Map<String, List<SysDictData>> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType));
+        for (Map.Entry<String, List<SysDictData>> entry : dictDataMap.entrySet()) {
+            DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList()));
+        }
+    }
+
+    /**
+     * 清空字典缓存数据
+     */
+    @Override
+    public void clearDictCache() {
+        DictUtils.clearDictCache();
+    }
+
+    /**
+     * 重置字典缓存数据
+     */
+    @Override
+    public void resetDictCache() {
+        clearDictCache();
+        loadingDictCache();
+    }
+
+    /**
+     * 新增保存字典类型信息
+     *
+     * @param dict 字典类型信息
+     * @return 结果
+     */
+    @Override
+    public int insertDictType(SysDictType dict) {
+        int row = dictTypeMapper.insertDictType(dict);
+        if (row > 0) {
+            DictUtils.setDictCache(dict.getDictType(), null);
+        }
+        return row;
+    }
+
+    /**
+     * 修改保存字典类型信息
+     *
+     * @param dict 字典类型信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int updateDictType(SysDictType dict) {
+        SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId());
+        dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType());
+        int row = dictTypeMapper.updateDictType(dict);
+        if (row > 0) {
+            List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType());
+            DictUtils.setDictCache(dict.getDictType(), dictDatas);
+        }
+        return row;
+    }
+
+    /**
+     * 校验字典类型称是否唯一
+     *
+     * @param dict 字典类型
+     * @return 结果
+     */
+    @Override
+    public boolean checkDictTypeUnique(SysDictType dict) {
+        Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId();
+        SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType());
+        if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+}

+ 483 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysMenuServiceImpl.java

@@ -0,0 +1,483 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.common.constant.Constants;
+import com.lij.himeeter.common.constant.UserConstants;
+import com.lij.himeeter.common.core.domain.TreeSelect;
+import com.lij.himeeter.common.core.domain.entity.SysMenu;
+import com.lij.himeeter.common.core.domain.entity.SysRole;
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.domin.vo.MetaVO;
+import com.lij.himeeter.system.domin.vo.RouterVO;
+import com.lij.himeeter.system.mapper.SysMenuMapper;
+import com.lij.himeeter.system.mapper.SysRoleMapper;
+import com.lij.himeeter.system.mapper.SysRoleMenuMapper;
+import com.lij.himeeter.system.service.ISysMenuService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysMenuServiceImpl
+ * 创建时间 : 2024年12月31日 16:56
+ *
+ * 文件描述 : 菜单 业务层处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Service
+public class SysMenuServiceImpl implements ISysMenuService {
+    public static final String PREMISSION_STRING = "perms[\"{0}\"]";
+
+    @Autowired
+    private SysMenuMapper menuMapper;
+
+    @Autowired
+    private SysRoleMapper roleMapper;
+
+    @Autowired
+    private SysRoleMenuMapper roleMenuMapper;
+
+    /**
+     * 根据用户查询系统菜单列表
+     *
+     * @param userId 用户ID
+     * @return 菜单列表
+     */
+    @Override
+    public List<SysMenu> selectMenuList(Long userId) {
+        return selectMenuList(new SysMenu(), userId);
+    }
+
+    /**
+     * 查询系统菜单列表
+     *
+     * @param menu 菜单信息
+     * @return 菜单列表
+     */
+    @Override
+    public List<SysMenu> selectMenuList(SysMenu menu, Long userId) {
+        List<SysMenu> menuList = null;
+        // 管理员显示所有菜单信息
+        if (SysUser.isAdmin(userId)) {
+            menuList = menuMapper.selectMenuList(menu);
+        } else {
+            menu.getParams().put("userId", userId);
+            menuList = menuMapper.selectMenuListByUserId(menu);
+        }
+        return menuList;
+    }
+
+    /**
+     * 根据用户ID查询权限
+     *
+     * @param userId 用户ID
+     * @return 权限列表
+     */
+    @Override
+    public Set<String> selectMenuPermsByUserId(Long userId) {
+        List<String> perms = menuMapper.selectMenuPermsByUserId(userId);
+        Set<String> permsSet = new HashSet<>();
+        for (String perm : perms) {
+            if (StringUtils.isNotEmpty(perm)) {
+                permsSet.addAll(Arrays.asList(perm.trim().split(",")));
+            }
+        }
+        return permsSet;
+    }
+
+    /**
+     * 根据角色ID查询权限
+     *
+     * @param roleId 角色ID
+     * @return 权限列表
+     */
+    @Override
+    public Set<String> selectMenuPermsByRoleId(Long roleId) {
+        List<String> perms = menuMapper.selectMenuPermsByRoleId(roleId);
+        Set<String> permsSet = new HashSet<>();
+        for (String perm : perms) {
+            if (StringUtils.isNotEmpty(perm)) {
+                permsSet.addAll(Arrays.asList(perm.trim().split(",")));
+            }
+        }
+        return permsSet;
+    }
+
+    /**
+     * 根据用户ID查询菜单
+     *
+     * @param userId 用户名称
+     * @return 菜单列表
+     */
+    @Override
+    public List<SysMenu> selectMenuTreeByUserId(Long userId) {
+        List<SysMenu> menus = null;
+        if (SecurityUtils.isAdmin(userId)) {
+            menus = menuMapper.selectMenuTreeAll();
+        } else {
+            menus = menuMapper.selectMenuTreeByUserId(userId);
+        }
+        return getChildPerms(menus, 0);
+    }
+
+    /**
+     * 根据角色ID查询菜单树信息
+     *
+     * @param roleId 角色ID
+     * @return 选中菜单列表
+     */
+    @Override
+    public List<Long> selectMenuListByRoleId(Long roleId) {
+        SysRole role = roleMapper.selectRoleById(roleId);
+        return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly());
+    }
+
+    /**
+     * 构建前端路由所需要的菜单
+     *
+     * @param menus 菜单列表
+     * @return 路由列表
+     */
+    @Override
+    public List<RouterVO> buildMenus(List<SysMenu> menus) {
+        List<RouterVO> routers = new LinkedList<RouterVO>();
+        for (SysMenu menu : menus) {
+            RouterVO router = new RouterVO();
+            router.setHidden("1".equals(menu.getVisible()));
+            router.setName(getRouteName(menu));
+            router.setPath(getRouterPath(menu));
+            router.setComponent(getComponent(menu));
+            router.setQuery(menu.getQuery());
+            router.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
+            List<SysMenu> cMenus = menu.getChildren();
+            if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) {
+                router.setAlwaysShow(true);
+                router.setRedirect("noRedirect");
+                router.setChildren(buildMenus(cMenus));
+            } else if (isMenuFrame(menu)) {
+                router.setMeta(null);
+                List<RouterVO> childrenList = new ArrayList<RouterVO>();
+                RouterVO children = new RouterVO();
+                children.setPath(menu.getPath());
+                children.setComponent(menu.getComponent());
+                children.setName(getRouteName(menu.getRouteName(), menu.getPath()));
+                children.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
+                children.setQuery(menu.getQuery());
+                childrenList.add(children);
+                router.setChildren(childrenList);
+            } else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
+                router.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon()));
+                router.setPath("/");
+                List<RouterVO> childrenList = new ArrayList<RouterVO>();
+                RouterVO children = new RouterVO();
+                String routerPath = innerLinkReplaceEach(menu.getPath());
+                children.setPath(routerPath);
+                children.setComponent(UserConstants.INNER_LINK);
+                children.setName(getRouteName(menu.getRouteName(), routerPath));
+                children.setMeta(new MetaVO(menu.getMenuName(), menu.getIcon(), menu.getPath()));
+                childrenList.add(children);
+                router.setChildren(childrenList);
+            }
+            routers.add(router);
+        }
+        return routers;
+    }
+
+    /**
+     * 构建前端所需要树结构
+     *
+     * @param menus 菜单列表
+     * @return 树结构列表
+     */
+    @Override
+    public List<SysMenu> buildMenuTree(List<SysMenu> menus) {
+        List<SysMenu> returnList = new ArrayList<SysMenu>();
+        List<Long> tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList());
+        for (Iterator<SysMenu> iterator = menus.iterator(); iterator.hasNext(); ) {
+            SysMenu menu = (SysMenu) iterator.next();
+            // 如果是顶级节点, 遍历该父节点的所有子节点
+            if (!tempList.contains(menu.getParentId())) {
+                recursionFn(menus, menu);
+                returnList.add(menu);
+            }
+        }
+        if (returnList.isEmpty()) {
+            returnList = menus;
+        }
+        return returnList;
+    }
+
+    /**
+     * 构建前端所需要下拉树结构
+     *
+     * @param menus 菜单列表
+     * @return 下拉树结构列表
+     */
+    @Override
+    public List<TreeSelect> buildMenuTreeSelect(List<SysMenu> menus) {
+        List<SysMenu> menuTrees = buildMenuTree(menus);
+        return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
+    }
+
+    /**
+     * 根据菜单ID查询信息
+     *
+     * @param menuId 菜单ID
+     * @return 菜单信息
+     */
+    @Override
+    public SysMenu selectMenuById(Long menuId) {
+        return menuMapper.selectMenuById(menuId);
+    }
+
+    /**
+     * 是否存在菜单子节点
+     *
+     * @param menuId 菜单ID
+     * @return 结果
+     */
+    @Override
+    public boolean hasChildByMenuId(Long menuId) {
+        int result = menuMapper.hasChildByMenuId(menuId);
+        return result > 0;
+    }
+
+    /**
+     * 查询菜单使用数量
+     *
+     * @param menuId 菜单ID
+     * @return 结果
+     */
+    @Override
+    public boolean checkMenuExistRole(Long menuId) {
+        int result = roleMenuMapper.checkMenuExistRole(menuId);
+        return result > 0;
+    }
+
+    /**
+     * 新增保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    @Override
+    public int insertMenu(SysMenu menu) {
+        return menuMapper.insertMenu(menu);
+    }
+
+    /**
+     * 修改保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    @Override
+    public int updateMenu(SysMenu menu) {
+        return menuMapper.updateMenu(menu);
+    }
+
+    /**
+     * 删除菜单管理信息
+     *
+     * @param menuId 菜单ID
+     * @return 结果
+     */
+    @Override
+    public int deleteMenuById(Long menuId) {
+        return menuMapper.deleteMenuById(menuId);
+    }
+
+    /**
+     * 校验菜单名称是否唯一
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkMenuNameUnique(SysMenu menu) {
+        Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
+        SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId());
+        if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 获取路由名称
+     *
+     * @param menu 菜单信息
+     * @return 路由名称
+     */
+    public String getRouteName(SysMenu menu) {
+        // 非外链并且是一级目录(类型为目录)
+        if (isMenuFrame(menu)) {
+            return StringUtils.EMPTY;
+        }
+        return getRouteName(menu.getRouteName(), menu.getPath());
+    }
+
+    /**
+     * 获取路由名称,如没有配置路由名称则取路由地址
+     *
+     * @param name 路由名称
+     * @param path 路由地址
+     * @return 路由名称(驼峰格式)
+     */
+    public String getRouteName(String name, String path) {
+        String routerName = StringUtils.isNotEmpty(name) ? name : path;
+        return StringUtils.capitalize(routerName);
+    }
+
+    /**
+     * 获取路由地址
+     *
+     * @param menu 菜单信息
+     * @return 路由地址
+     */
+    public String getRouterPath(SysMenu menu) {
+        String routerPath = menu.getPath();
+        // 内链打开外网方式
+        if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
+            routerPath = innerLinkReplaceEach(routerPath);
+        }
+        // 非外链并且是一级目录(类型为目录)
+        if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
+                && UserConstants.NO_FRAME.equals(menu.getIsFrame())) {
+            routerPath = "/" + menu.getPath();
+        }
+        // 非外链并且是一级目录(类型为菜单)
+        else if (isMenuFrame(menu)) {
+            routerPath = "/";
+        }
+        return routerPath;
+    }
+
+    /**
+     * 获取组件信息
+     *
+     * @param menu 菜单信息
+     * @return 组件信息
+     */
+    public String getComponent(SysMenu menu) {
+        String component = UserConstants.LAYOUT;
+        if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) {
+            component = menu.getComponent();
+        } else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
+            component = UserConstants.INNER_LINK;
+        } else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) {
+            component = UserConstants.PARENT_VIEW;
+        }
+        return component;
+    }
+
+    /**
+     * 是否为菜单内部跳转
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isMenuFrame(SysMenu menu) {
+        return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType())
+                && menu.getIsFrame().equals(UserConstants.NO_FRAME);
+    }
+
+    /**
+     * 是否为内链组件
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isInnerLink(SysMenu menu) {
+        return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath());
+    }
+
+    /**
+     * 是否为parent_view组件
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isParentView(SysMenu menu) {
+        return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType());
+    }
+
+    /**
+     * 根据父节点的ID获取所有子节点
+     *
+     * @param list     分类表
+     * @param parentId 传入的父节点ID
+     * @return String
+     */
+    public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId) {
+        List<SysMenu> returnList = new ArrayList<SysMenu>();
+        for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext(); ) {
+            SysMenu t = (SysMenu) iterator.next();
+            // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
+            if (t.getParentId() == parentId) {
+                recursionFn(list, t);
+                returnList.add(t);
+            }
+        }
+        return returnList;
+    }
+
+    /**
+     * 递归列表
+     *
+     * @param list 分类表
+     * @param t    子节点
+     */
+    private void recursionFn(List<SysMenu> list, SysMenu t) {
+        // 得到子节点列表
+        List<SysMenu> childList = getChildList(list, t);
+        t.setChildren(childList);
+        for (SysMenu tChild : childList) {
+            if (hasChild(list, tChild)) {
+                recursionFn(list, tChild);
+            }
+        }
+    }
+
+    /**
+     * 得到子节点列表
+     */
+    private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t) {
+        List<SysMenu> tlist = new ArrayList<SysMenu>();
+        Iterator<SysMenu> it = list.iterator();
+        while (it.hasNext()) {
+            SysMenu n = (SysMenu) it.next();
+            if (n.getParentId().longValue() == t.getMenuId().longValue()) {
+                tlist.add(n);
+            }
+        }
+        return tlist;
+    }
+
+    /**
+     * 判断是否有子节点
+     */
+    private boolean hasChild(List<SysMenu> list, SysMenu t) {
+        return getChildList(list, t).size() > 0;
+    }
+
+    /**
+     * 内链域名特殊字符替换
+     *
+     * @return 替换后的内链域名
+     */
+    public String innerLinkReplaceEach(String path) {
+        return StringUtils.replaceEach(path, new String[]{Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":"},
+                new String[]{"", "", "", "/", "/"});
+    }
+}

+ 102 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysNoticeServiceImpl.java

@@ -0,0 +1,102 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.system.domin.SysNotice;
+import com.lij.himeeter.system.mapper.SysNoticeMapper;
+import com.lij.himeeter.system.service.ISysNoticeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysNoticeServiceImpl
+ * 创建时间 : 2024年12月31日 16:56
+ *
+ * 文件描述 : 公告 服务层实现
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Service
+public class SysNoticeServiceImpl implements ISysNoticeService
+{
+    @Autowired
+    private SysNoticeMapper noticeMapper;
+
+    /**
+     * 查询公告信息
+     *
+     * @param noticeId 公告ID
+     * @return 公告信息
+     */
+    @Override
+    public SysNotice selectNoticeById(Long noticeId)
+    {
+        return noticeMapper.selectNoticeById(noticeId);
+    }
+
+    /**
+     * 查询公告列表
+     *
+     * @param notice 公告信息
+     * @return 公告集合
+     */
+    @Override
+    public List<SysNotice> selectNoticeList(SysNotice notice)
+    {
+        return noticeMapper.selectNoticeList(notice);
+    }
+
+    /**
+     * 新增公告
+     *
+     * @param notice 公告信息
+     * @return 结果
+     */
+    @Override
+    public int insertNotice(SysNotice notice)
+    {
+        return noticeMapper.insertNotice(notice);
+    }
+
+    /**
+     * 修改公告
+     *
+     * @param notice 公告信息
+     * @return 结果
+     */
+    @Override
+    public int updateNotice(SysNotice notice)
+    {
+        return noticeMapper.updateNotice(notice);
+    }
+
+    /**
+     * 删除公告对象
+     *
+     * @param noticeId 公告ID
+     * @return 结果
+     */
+    @Override
+    public int deleteNoticeById(Long noticeId)
+    {
+        return noticeMapper.deleteNoticeById(noticeId);
+    }
+
+    /**
+     * 批量删除公告信息
+     *
+     * @param noticeIds 需要删除的公告ID
+     * @return 结果
+     */
+    @Override
+    public int deleteNoticeByIds(Long[] noticeIds)
+    {
+        return noticeMapper.deleteNoticeByIds(noticeIds);
+    }
+}

+ 172 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysPostServiceImpl.java

@@ -0,0 +1,172 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.common.constant.UserConstants;
+import com.lij.himeeter.common.exception.ServiceException;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.domin.SysPost;
+import com.lij.himeeter.system.mapper.SysPostMapper;
+import com.lij.himeeter.system.mapper.SysUserPostMapper;
+import com.lij.himeeter.system.service.ISysPostService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysPostServiceImpl
+ * 创建时间 : 2024年12月31日 16:56
+ *
+ * 文件描述 : 岗位信息 服务层处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Service
+public class SysPostServiceImpl implements ISysPostService {
+    @Autowired
+    private SysPostMapper postMapper;
+
+    @Autowired
+    private SysUserPostMapper userPostMapper;
+
+    /**
+     * 查询岗位信息集合
+     *
+     * @param post 岗位信息
+     * @return 岗位信息集合
+     */
+    @Override
+    public List<SysPost> selectPostList(SysPost post) {
+        return postMapper.selectPostList(post);
+    }
+
+    /**
+     * 查询所有岗位
+     *
+     * @return 岗位列表
+     */
+    @Override
+    public List<SysPost> selectPostAll() {
+        return postMapper.selectPostAll();
+    }
+
+    /**
+     * 通过岗位ID查询岗位信息
+     *
+     * @param postId 岗位ID
+     * @return 角色对象信息
+     */
+    @Override
+    public SysPost selectPostById(Long postId) {
+        return postMapper.selectPostById(postId);
+    }
+
+    /**
+     * 根据用户ID获取岗位选择框列表
+     *
+     * @param userId 用户ID
+     * @return 选中岗位ID列表
+     */
+    @Override
+    public List<Long> selectPostListByUserId(Long userId) {
+        return postMapper.selectPostListByUserId(userId);
+    }
+
+    /**
+     * 校验岗位名称是否唯一
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkPostNameUnique(SysPost post) {
+        Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
+        SysPost info = postMapper.checkPostNameUnique(post.getPostName());
+        if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 校验岗位编码是否唯一
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkPostCodeUnique(SysPost post) {
+        Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId();
+        SysPost info = postMapper.checkPostCodeUnique(post.getPostCode());
+        if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 通过岗位ID查询岗位使用数量
+     *
+     * @param postId 岗位ID
+     * @return 结果
+     */
+    @Override
+    public int countUserPostById(Long postId) {
+        return userPostMapper.countUserPostById(postId);
+    }
+
+    /**
+     * 删除岗位信息
+     *
+     * @param postId 岗位ID
+     * @return 结果
+     */
+    @Override
+    public int deletePostById(Long postId) {
+        return postMapper.deletePostById(postId);
+    }
+
+    /**
+     * 批量删除岗位信息
+     *
+     * @param postIds 需要删除的岗位ID
+     * @return 结果
+     */
+    @Override
+    public int deletePostByIds(Long[] postIds) {
+        for (Long postId : postIds) {
+            SysPost post = selectPostById(postId);
+            if (countUserPostById(postId) > 0) {
+                throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName()));
+            }
+        }
+        return postMapper.deletePostByIds(postIds);
+    }
+
+    /**
+     * 新增保存岗位信息
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    @Override
+    public int insertPost(SysPost post) {
+        return postMapper.insertPost(post);
+    }
+
+    /**
+     * 修改保存岗位信息
+     *
+     * @param post 岗位信息
+     * @return 结果
+     */
+    @Override
+    public int updatePost(SysPost post) {
+        return postMapper.updatePost(post);
+    }
+}

+ 392 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysRoleServiceImpl.java

@@ -0,0 +1,392 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.common.annotation.DataScope;
+import com.lij.himeeter.common.constant.UserConstants;
+import com.lij.himeeter.common.core.domain.entity.SysRole;
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+import com.lij.himeeter.common.exception.ServiceException;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.spring.SpringUtils;
+import com.lij.himeeter.system.domin.SysRoleDept;
+import com.lij.himeeter.system.domin.SysRoleMenu;
+import com.lij.himeeter.system.domin.SysUserRole;
+import com.lij.himeeter.system.mapper.SysRoleDeptMapper;
+import com.lij.himeeter.system.mapper.SysRoleMapper;
+import com.lij.himeeter.system.mapper.SysRoleMenuMapper;
+import com.lij.himeeter.system.mapper.SysUserRoleMapper;
+import com.lij.himeeter.system.service.ISysRoleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysRoleServiceImpl
+ * 创建时间 : 2024年12月31日 16:56
+ *
+ * 文件描述 : 角色 业务层处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Service
+public class SysRoleServiceImpl implements ISysRoleService {
+    @Autowired
+    private SysRoleMapper roleMapper;
+
+    @Autowired
+    private SysRoleMenuMapper roleMenuMapper;
+
+    @Autowired
+    private SysUserRoleMapper userRoleMapper;
+
+    @Autowired
+    private SysRoleDeptMapper roleDeptMapper;
+
+    /**
+     * 根据条件分页查询角色数据
+     *
+     * @param role 角色信息
+     * @return 角色数据集合信息
+     */
+    @Override
+    @DataScope(deptAlias = "d")
+    public List<SysRole> selectRoleList(SysRole role) {
+        return roleMapper.selectRoleList(role);
+    }
+
+    /**
+     * 根据用户ID查询角色
+     *
+     * @param userId 用户ID
+     * @return 角色列表
+     */
+    @Override
+    public List<SysRole> selectRolesByUserId(Long userId) {
+        List<SysRole> userRoles = roleMapper.selectRolePermissionByUserId(userId);
+        List<SysRole> roles = selectRoleAll();
+        for (SysRole role : roles) {
+            for (SysRole userRole : userRoles) {
+                if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) {
+                    role.setFlag(true);
+                    break;
+                }
+            }
+        }
+        return roles;
+    }
+
+    /**
+     * 根据用户ID查询权限
+     *
+     * @param userId 用户ID
+     * @return 权限列表
+     */
+    @Override
+    public Set<String> selectRolePermissionByUserId(Long userId) {
+        List<SysRole> perms = roleMapper.selectRolePermissionByUserId(userId);
+        Set<String> permsSet = new HashSet<>();
+        for (SysRole perm : perms) {
+            if (StringUtils.isNotNull(perm)) {
+                permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));
+            }
+        }
+        return permsSet;
+    }
+
+    /**
+     * 查询所有角色
+     *
+     * @return 角色列表
+     */
+    @Override
+    public List<SysRole> selectRoleAll() {
+        return SpringUtils.getAopProxy(this).selectRoleList(new SysRole());
+    }
+
+    /**
+     * 根据用户ID获取角色选择框列表
+     *
+     * @param userId 用户ID
+     * @return 选中角色ID列表
+     */
+    @Override
+    public List<Long> selectRoleListByUserId(Long userId) {
+        return roleMapper.selectRoleListByUserId(userId);
+    }
+
+    /**
+     * 通过角色ID查询角色
+     *
+     * @param roleId 角色ID
+     * @return 角色对象信息
+     */
+    @Override
+    public SysRole selectRoleById(Long roleId) {
+        return roleMapper.selectRoleById(roleId);
+    }
+
+    /**
+     * 校验角色名称是否唯一
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkRoleNameUnique(SysRole role) {
+        Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId();
+        SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName());
+        if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 校验角色权限是否唯一
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkRoleKeyUnique(SysRole role) {
+        Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId();
+        SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey());
+        if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 校验角色是否允许操作
+     *
+     * @param role 角色信息
+     */
+    @Override
+    public void checkRoleAllowed(SysRole role) {
+        if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) {
+            throw new ServiceException("不允许操作超级管理员角色");
+        }
+    }
+
+    /**
+     * 校验角色是否有数据权限
+     *
+     * @param roleIds 角色id
+     */
+    @Override
+    public void checkRoleDataScope(Long... roleIds) {
+        if (!SysUser.isAdmin(SecurityUtils.getUserId())) {
+            for (Long roleId : roleIds) {
+                SysRole role = new SysRole();
+                role.setRoleId(roleId);
+                List<SysRole> roles = SpringUtils.getAopProxy(this).selectRoleList(role);
+                if (StringUtils.isEmpty(roles)) {
+                    throw new ServiceException("没有权限访问角色数据!");
+                }
+            }
+        }
+    }
+
+    /**
+     * 通过角色ID查询角色使用数量
+     *
+     * @param roleId 角色ID
+     * @return 结果
+     */
+    @Override
+    public int countUserRoleByRoleId(Long roleId) {
+        return userRoleMapper.countUserRoleByRoleId(roleId);
+    }
+
+    /**
+     * 新增保存角色信息
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int insertRole(SysRole role) {
+        // 新增角色信息
+        roleMapper.insertRole(role);
+        return insertRoleMenu(role);
+    }
+
+    /**
+     * 修改保存角色信息
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int updateRole(SysRole role) {
+        // 修改角色信息
+        roleMapper.updateRole(role);
+        // 删除角色与菜单关联
+        roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId());
+        return insertRoleMenu(role);
+    }
+
+    /**
+     * 修改角色状态
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    @Override
+    public int updateRoleStatus(SysRole role) {
+        return roleMapper.updateRole(role);
+    }
+
+    /**
+     * 修改数据权限信息
+     *
+     * @param role 角色信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int authDataScope(SysRole role) {
+        // 修改角色信息
+        roleMapper.updateRole(role);
+        // 删除角色与部门关联
+        roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId());
+        // 新增角色和部门信息(数据权限)
+        return insertRoleDept(role);
+    }
+
+    /**
+     * 新增角色菜单信息
+     *
+     * @param role 角色对象
+     */
+    public int insertRoleMenu(SysRole role) {
+        int rows = 1;
+        // 新增用户与角色管理
+        List<SysRoleMenu> list = new ArrayList<SysRoleMenu>();
+        for (Long menuId : role.getMenuIds()) {
+            SysRoleMenu rm = new SysRoleMenu();
+            rm.setRoleId(role.getRoleId());
+            rm.setMenuId(menuId);
+            list.add(rm);
+        }
+        if (list.size() > 0) {
+            rows = roleMenuMapper.batchRoleMenu(list);
+        }
+        return rows;
+    }
+
+    /**
+     * 新增角色部门信息(数据权限)
+     *
+     * @param role 角色对象
+     */
+    public int insertRoleDept(SysRole role) {
+        int rows = 1;
+        // 新增角色与部门(数据权限)管理
+        List<SysRoleDept> list = new ArrayList<SysRoleDept>();
+        for (Long deptId : role.getDeptIds()) {
+            SysRoleDept rd = new SysRoleDept();
+            rd.setRoleId(role.getRoleId());
+            rd.setDeptId(deptId);
+            list.add(rd);
+        }
+        if (list.size() > 0) {
+            rows = roleDeptMapper.batchRoleDept(list);
+        }
+        return rows;
+    }
+
+    /**
+     * 通过角色ID删除角色
+     *
+     * @param roleId 角色ID
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteRoleById(Long roleId) {
+        // 删除角色与菜单关联
+        roleMenuMapper.deleteRoleMenuByRoleId(roleId);
+        // 删除角色与部门关联
+        roleDeptMapper.deleteRoleDeptByRoleId(roleId);
+        return roleMapper.deleteRoleById(roleId);
+    }
+
+    /**
+     * 批量删除角色信息
+     *
+     * @param roleIds 需要删除的角色ID
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteRoleByIds(Long[] roleIds) {
+        for (Long roleId : roleIds) {
+            checkRoleAllowed(new SysRole(roleId));
+            checkRoleDataScope(roleId);
+            SysRole role = selectRoleById(roleId);
+            if (countUserRoleByRoleId(roleId) > 0) {
+                throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName()));
+            }
+        }
+        // 删除角色与菜单关联
+        roleMenuMapper.deleteRoleMenu(roleIds);
+        // 删除角色与部门关联
+        roleDeptMapper.deleteRoleDept(roleIds);
+        return roleMapper.deleteRoleByIds(roleIds);
+    }
+
+    /**
+     * 取消授权用户角色
+     *
+     * @param userRole 用户和角色关联信息
+     * @return 结果
+     */
+    @Override
+    public int deleteAuthUser(SysUserRole userRole) {
+        return userRoleMapper.deleteUserRoleInfo(userRole);
+    }
+
+    /**
+     * 批量取消授权用户角色
+     *
+     * @param roleId  角色ID
+     * @param userIds 需要取消授权的用户数据ID
+     * @return 结果
+     */
+    @Override
+    public int deleteAuthUsers(Long roleId, Long[] userIds) {
+        return userRoleMapper.deleteUserRoleInfos(roleId, userIds);
+    }
+
+    /**
+     * 批量选择授权用户角色
+     *
+     * @param roleId  角色ID
+     * @param userIds 需要授权的用户数据ID
+     * @return 结果
+     */
+    @Override
+    public int insertAuthUsers(Long roleId, Long[] userIds) {
+        // 新增用户与角色管理
+        List<SysUserRole> list = new ArrayList<SysUserRole>();
+        for (Long userId : userIds) {
+            SysUserRole ur = new SysUserRole();
+            ur.setUserId(userId);
+            ur.setRoleId(roleId);
+            list.add(ur);
+        }
+        return userRoleMapper.batchUserRole(list);
+    }
+}

+ 105 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysUserOnlineServiceImpl.java

@@ -0,0 +1,105 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.common.core.domain.model.LoginUser;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.system.domin.SysUserOnline;
+import com.lij.himeeter.system.service.ISysUserOnlineService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysUserOnlineServiceImpl
+ * 创建时间 : 2024年12月31日 16:57
+ *
+ * 文件描述 : 在线用户 服务层处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+
+@Service
+public class SysUserOnlineServiceImpl implements ISysUserOnlineService
+{
+    /**
+     * 通过登录地址查询信息
+     *
+     * @param ipaddr 登录地址
+     * @param user 用户信息
+     * @return 在线用户信息
+     */
+    @Override
+    public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user)
+    {
+        if (StringUtils.equals(ipaddr, user.getIpaddr()))
+        {
+            return loginUserToUserOnline(user);
+        }
+        return null;
+    }
+
+    /**
+     * 通过用户名称查询信息
+     *
+     * @param userName 用户名称
+     * @param user 用户信息
+     * @return 在线用户信息
+     */
+    @Override
+    public SysUserOnline selectOnlineByUserName(String userName, LoginUser user)
+    {
+        if (StringUtils.equals(userName, user.getUsername()))
+        {
+            return loginUserToUserOnline(user);
+        }
+        return null;
+    }
+
+    /**
+     * 通过登录地址/用户名称查询信息
+     *
+     * @param ipaddr 登录地址
+     * @param userName 用户名称
+     * @param user 用户信息
+     * @return 在线用户信息
+     */
+    @Override
+    public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user)
+    {
+        if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername()))
+        {
+            return loginUserToUserOnline(user);
+        }
+        return null;
+    }
+
+    /**
+     * 设置在线用户信息
+     *
+     * @param user 用户信息
+     * @return 在线用户
+     */
+    @Override
+    public SysUserOnline loginUserToUserOnline(LoginUser user)
+    {
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser()))
+        {
+            return null;
+        }
+        SysUserOnline sysUserOnline = new SysUserOnline();
+        sysUserOnline.setTokenId(user.getToken());
+        sysUserOnline.setUserName(user.getUsername());
+        sysUserOnline.setIpaddr(user.getIpaddr());
+        sysUserOnline.setLoginLocation(user.getLoginLocation());
+        sysUserOnline.setBrowser(user.getBrowser());
+        sysUserOnline.setOs(user.getOs());
+        sysUserOnline.setLoginTime(user.getLoginTime());
+        if (StringUtils.isNotNull(user.getUser().getDept()))
+        {
+            sysUserOnline.setDeptName(user.getUser().getDept().getDeptName());
+        }
+        return sysUserOnline;
+    }
+}

+ 502 - 0
himeeter-system/src/main/java/com/lij/himeeter/system/service/impl/SysUserServiceImpl.java

@@ -0,0 +1,502 @@
+package com.lij.himeeter.system.service.impl;
+
+import com.lij.himeeter.common.annotation.DataScope;
+import com.lij.himeeter.common.constant.UserConstants;
+import com.lij.himeeter.common.core.domain.entity.SysRole;
+import com.lij.himeeter.common.core.domain.entity.SysUser;
+import com.lij.himeeter.common.exception.ServiceException;
+import com.lij.himeeter.common.utils.SecurityUtils;
+import com.lij.himeeter.common.utils.StringUtils;
+import com.lij.himeeter.common.utils.bean.BeanValidators;
+import com.lij.himeeter.common.utils.spring.SpringUtils;
+import com.lij.himeeter.system.domin.SysPost;
+import com.lij.himeeter.system.domin.SysUserPost;
+import com.lij.himeeter.system.domin.SysUserRole;
+import com.lij.himeeter.system.mapper.*;
+import com.lij.himeeter.system.service.ISysConfigService;
+import com.lij.himeeter.system.service.ISysDeptService;
+import com.lij.himeeter.system.service.ISysUserService;
+import jakarta.validation.Validator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * <pre>
+ *
+ * 文件名 :  SysUserServiceImpl
+ * 创建时间 : 2024年12月31日 16:57
+ *
+ * 文件描述 : 用户 业务层处理
+ *
+ * </pre>
+ *
+ * @author Nobita
+ * @version 1.0.0
+ */
+@Service
+public class SysUserServiceImpl implements ISysUserService {
+
+    private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);
+
+    @Autowired
+    private SysUserMapper userMapper;
+
+    @Autowired
+    private SysRoleMapper roleMapper;
+
+    @Autowired
+    private SysPostMapper postMapper;
+
+    @Autowired
+    private SysUserRoleMapper userRoleMapper;
+
+    @Autowired
+    private SysUserPostMapper userPostMapper;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @Autowired
+    protected Validator validator;
+
+    /**
+     * 根据条件分页查询用户列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    @Override
+    @DataScope(deptAlias = "d", userAlias = "u")
+    public List<SysUser> selectUserList(SysUser user) {
+        return userMapper.selectUserList(user);
+    }
+
+    /**
+     * 根据条件分页查询已分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    @Override
+    @DataScope(deptAlias = "d", userAlias = "u")
+    public List<SysUser> selectAllocatedList(SysUser user) {
+        return userMapper.selectAllocatedList(user);
+    }
+
+    /**
+     * 根据条件分页查询未分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    @Override
+    @DataScope(deptAlias = "d", userAlias = "u")
+    public List<SysUser> selectUnallocatedList(SysUser user) {
+        return userMapper.selectUnallocatedList(user);
+    }
+
+    /**
+     * 通过用户名查询用户
+     *
+     * @param userName 用户名
+     * @return 用户对象信息
+     */
+    @Override
+    public SysUser selectUserByUserName(String userName) {
+        return userMapper.selectUserByUserName(userName);
+    }
+
+    /**
+     * 通过用户ID查询用户
+     *
+     * @param userId 用户ID
+     * @return 用户对象信息
+     */
+    @Override
+    public SysUser selectUserById(Long userId) {
+        return userMapper.selectUserById(userId);
+    }
+
+    /**
+     * 查询用户所属角色组
+     *
+     * @param userName 用户名
+     * @return 结果
+     */
+    @Override
+    public String selectUserRoleGroup(String userName) {
+        List<SysRole> list = roleMapper.selectRolesByUserName(userName);
+        if (CollectionUtils.isEmpty(list)) {
+            return StringUtils.EMPTY;
+        }
+        return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(","));
+    }
+
+    /**
+     * 查询用户所属岗位组
+     *
+     * @param userName 用户名
+     * @return 结果
+     */
+    @Override
+    public String selectUserPostGroup(String userName) {
+        List<SysPost> list = postMapper.selectPostsByUserName(userName);
+        if (CollectionUtils.isEmpty(list)) {
+            return StringUtils.EMPTY;
+        }
+        return list.stream().map(SysPost::getPostName).collect(Collectors.joining(","));
+    }
+
+    /**
+     * 校验用户名称是否唯一
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkUserNameUnique(SysUser user) {
+        Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+        SysUser info = userMapper.checkUserNameUnique(user.getUserName());
+        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 校验手机号码是否唯一
+     *
+     * @param user 用户信息
+     * @return
+     */
+    @Override
+    public boolean checkPhoneUnique(SysUser user) {
+        Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+        SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber());
+        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 校验email是否唯一
+     *
+     * @param user 用户信息
+     * @return
+     */
+    @Override
+    public boolean checkEmailUnique(SysUser user) {
+        Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+        SysUser info = userMapper.checkEmailUnique(user.getEmail());
+        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 校验用户是否允许操作
+     *
+     * @param user 用户信息
+     */
+    @Override
+    public void checkUserAllowed(SysUser user) {
+        if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) {
+            throw new ServiceException("不允许操作超级管理员用户");
+        }
+    }
+
+    /**
+     * 校验用户是否有数据权限
+     *
+     * @param userId 用户id
+     */
+    @Override
+    public void checkUserDataScope(Long userId) {
+        if (!SysUser.isAdmin(SecurityUtils.getUserId())) {
+            SysUser user = new SysUser();
+            user.setUserId(userId);
+            List<SysUser> users = SpringUtils.getAopProxy(this).selectUserList(user);
+            if (StringUtils.isEmpty(users)) {
+                throw new ServiceException("没有权限访问用户数据!");
+            }
+        }
+    }
+
+    /**
+     * 新增保存用户信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int insertUser(SysUser user) {
+        // 新增用户信息
+        int rows = userMapper.insertUser(user);
+        // 新增用户岗位关联
+        insertUserPost(user);
+        // 新增用户与角色管理
+        insertUserRole(user);
+        return rows;
+    }
+
+    /**
+     * 注册用户信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    @Override
+    public boolean registerUser(SysUser user) {
+        return userMapper.insertUser(user) > 0;
+    }
+
+    /**
+     * 修改保存用户信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int updateUser(SysUser user) {
+        Long userId = user.getUserId();
+        // 删除用户与角色关联
+        userRoleMapper.deleteUserRoleByUserId(userId);
+        // 新增用户与角色管理
+        insertUserRole(user);
+        // 删除用户与岗位关联
+        userPostMapper.deleteUserPostByUserId(userId);
+        // 新增用户与岗位管理
+        insertUserPost(user);
+        return userMapper.updateUser(user);
+    }
+
+    /**
+     * 用户授权角色
+     *
+     * @param userId  用户ID
+     * @param roleIds 角色组
+     */
+    @Override
+    @Transactional
+    public void insertUserAuth(Long userId, Long[] roleIds) {
+        userRoleMapper.deleteUserRoleByUserId(userId);
+        insertUserRole(userId, roleIds);
+    }
+
+    /**
+     * 修改用户状态
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    @Override
+    public int updateUserStatus(SysUser user) {
+        return userMapper.updateUser(user);
+    }
+
+    /**
+     * 修改用户基本信息
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    @Override
+    public int updateUserProfile(SysUser user) {
+        return userMapper.updateUser(user);
+    }
+
+    /**
+     * 修改用户头像
+     *
+     * @param userName 用户名
+     * @param avatar   头像地址
+     * @return 结果
+     */
+    @Override
+    public boolean updateUserAvatar(String userName, String avatar) {
+        return userMapper.updateUserAvatar(userName, avatar) > 0;
+    }
+
+    /**
+     * 重置用户密码
+     *
+     * @param user 用户信息
+     * @return 结果
+     */
+    @Override
+    public int resetPwd(SysUser user) {
+        return userMapper.updateUser(user);
+    }
+
+    /**
+     * 重置用户密码
+     *
+     * @param userName 用户名
+     * @param password 密码
+     * @return 结果
+     */
+    @Override
+    public int resetUserPwd(String userName, String password) {
+        return userMapper.resetUserPwd(userName, password);
+    }
+
+    /**
+     * 新增用户角色信息
+     *
+     * @param user 用户对象
+     */
+    public void insertUserRole(SysUser user) {
+        this.insertUserRole(user.getUserId(), user.getRoleIds());
+    }
+
+    /**
+     * 新增用户岗位信息
+     *
+     * @param user 用户对象
+     */
+    public void insertUserPost(SysUser user) {
+        Long[] posts = user.getPostIds();
+        if (StringUtils.isNotEmpty(posts)) {
+            // 新增用户与岗位管理
+            List<SysUserPost> list = new ArrayList<SysUserPost>(posts.length);
+            for (Long postId : posts) {
+                SysUserPost up = new SysUserPost();
+                up.setUserId(user.getUserId());
+                up.setPostId(postId);
+                list.add(up);
+            }
+            userPostMapper.batchUserPost(list);
+        }
+    }
+
+    /**
+     * 新增用户角色信息
+     *
+     * @param userId  用户ID
+     * @param roleIds 角色组
+     */
+    public void insertUserRole(Long userId, Long[] roleIds) {
+        if (StringUtils.isNotEmpty(roleIds)) {
+            // 新增用户与角色管理
+            List<SysUserRole> list = new ArrayList<SysUserRole>(roleIds.length);
+            for (Long roleId : roleIds) {
+                SysUserRole ur = new SysUserRole();
+                ur.setUserId(userId);
+                ur.setRoleId(roleId);
+                list.add(ur);
+            }
+            userRoleMapper.batchUserRole(list);
+        }
+    }
+
+    /**
+     * 通过用户ID删除用户
+     *
+     * @param userId 用户ID
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteUserById(Long userId) {
+        // 删除用户与角色关联
+        userRoleMapper.deleteUserRoleByUserId(userId);
+        // 删除用户与岗位表
+        userPostMapper.deleteUserPostByUserId(userId);
+        return userMapper.deleteUserById(userId);
+    }
+
+    /**
+     * 批量删除用户信息
+     *
+     * @param userIds 需要删除的用户ID
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteUserByIds(Long[] userIds) {
+        for (Long userId : userIds) {
+            checkUserAllowed(new SysUser(userId));
+            checkUserDataScope(userId);
+        }
+        // 删除用户与角色关联
+        userRoleMapper.deleteUserRole(userIds);
+        // 删除用户与岗位关联
+        userPostMapper.deleteUserPost(userIds);
+        return userMapper.deleteUserByIds(userIds);
+    }
+
+    /**
+     * 导入用户数据
+     *
+     * @param userList        用户数据列表
+     * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
+     * @param operName        操作用户
+     * @return 结果
+     */
+    @Override
+    public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName) {
+        if (StringUtils.isNull(userList) || userList.size() == 0) {
+            throw new ServiceException("导入用户数据不能为空!");
+        }
+        int successNum = 0;
+        int failureNum = 0;
+        StringBuilder successMsg = new StringBuilder();
+        StringBuilder failureMsg = new StringBuilder();
+        for (SysUser user : userList) {
+            try {
+                // 验证是否存在这个用户
+                SysUser u = userMapper.selectUserByUserName(user.getUserName());
+                if (StringUtils.isNull(u)) {
+                    BeanValidators.validateWithException(validator, user);
+                    deptService.checkDeptDataScope(user.getDeptId());
+                    String password = configService.selectConfigByKey("sys.user.initPassword");
+                    user.setPassword(SecurityUtils.encryptPassword(password));
+                    user.setCreateBy(operName);
+                    userMapper.insertUser(user);
+                    successNum++;
+                    successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
+                } else if (isUpdateSupport) {
+                    BeanValidators.validateWithException(validator, user);
+                    checkUserAllowed(u);
+                    checkUserDataScope(u.getUserId());
+                    deptService.checkDeptDataScope(user.getDeptId());
+                    user.setUserId(u.getUserId());
+                    user.setUpdateBy(operName);
+                    userMapper.updateUser(user);
+                    successNum++;
+                    successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
+                } else {
+                    failureNum++;
+                    failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
+                }
+            } catch (Exception e) {
+                failureNum++;
+                String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
+                failureMsg.append(msg + e.getMessage());
+                log.error(msg, e);
+            }
+        }
+        if (failureNum > 0) {
+            failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+            throw new ServiceException(failureMsg.toString());
+        } else {
+            successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+        }
+        return successMsg.toString();
+    }
+}