Browse Source

1、增加敏感词管理
2、增加错误码管理
3、增加数据库文档

rayson 1 year ago
parent
commit
7e032746f3
45 changed files with 3351 additions and 486 deletions
  1. 16 0
      citu-dependencies/pom.xml
  2. 51 2
      citu-framework/citu-common/src/main/java/com/citu/framework/common/exception/util/ServiceExceptionUtil.java
  3. 39 0
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/config/CituErrorCodeAutoConfiguration.java
  4. 30 0
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/config/ErrorCodeProperties.java
  5. 15 0
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java
  6. 108 0
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java
  7. 35 0
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/loader/ErrorCodeLoader.java
  8. 82 0
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java
  9. 10 0
      citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/package-info.java
  10. 1 0
      citu-framework/citu-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  11. 5 0
      citu-module-infra/citu-module-infra-biz/pom.xml
  12. 153 0
      citu-module-infra/citu-module-infra-biz/src/main/java/com/citu/module/infra/controller/admin/db/DatabaseDocController.java
  13. 8 0
      citu-module-infra/citu-module-infra-biz/src/main/resources/application.yaml
  14. 36 0
      citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/errorcode/ErrorCodeApi.java
  15. 35 0
      citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java
  16. 28 0
      citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/errorcode/dto/ErrorCodeRespDTO.java
  17. 30 0
      citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/sensitiveword/SensitiveWordApi.java
  18. 8 0
      citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/enums/ErrorCodeConstants.java
  19. 39 0
      citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/enums/errorcode/ErrorCodeTypeEnum.java
  20. 33 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/api/errorcode/ErrorCodeApiImpl.java
  21. 30 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/api/sensitiveword/SensitiveWordApiImpl.java
  22. 13 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/ErrorCodeController.http
  23. 95 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/ErrorCodeController.java
  24. 37 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java
  25. 48 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java
  26. 31 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/vo/ErrorCodeSaveReqVO.java
  27. 4 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/SensitiveWordController.http
  28. 110 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/SensitiveWordController.java
  29. 34 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java
  30. 46 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java
  31. 32 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/vo/SensitiveWordSaveVO.java
  32. 53 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/dataobject/errorcode/ErrorCodeDO.java
  33. 58 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java
  34. 41 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/mysql/errorcode/ErrorCodeMapper.java
  35. 37 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java
  36. 78 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/errorcode/ErrorCodeService.java
  37. 170 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/errorcode/ErrorCodeServiceImpl.java
  38. 90 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/sensitiveword/SensitiveWordService.java
  39. 265 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/sensitiveword/SensitiveWordServiceImpl.java
  40. 152 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/util/collection/SimpleTrie.java
  41. 2 0
      citu-module-system/citu-module-system-biz/src/main/resources/application-local.yaml
  42. 308 0
      citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/service/errorcode/ErrorCodeServiceTest.java
  43. 304 0
      citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java
  44. 2 0
      citu-module-system/citu-module-system-biz/src/test/resources/sql/clean.sql
  45. 549 484
      citu-module-system/citu-module-system-biz/src/test/resources/sql/create_tables.sql

+ 16 - 0
citu-dependencies/pom.xml

@@ -525,6 +525,22 @@
                 </exclusions>
                 </exclusions>
             </dependency>
             </dependency>
 
 
+            <dependency>
+                <groupId>cn.smallbun.screw</groupId>
+                <artifactId>screw-core</artifactId> <!-- 实现数据库文档 -->
+                <version>${screw.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.freemarker</groupId>
+                        <artifactId>freemarker</artifactId> <!-- 移除 Freemarker 依赖,采用 Velocity 作为模板引擎 -->
+                    </exclusion>
+                    <exclusion>
+                        <groupId>com.alibaba</groupId>
+                        <artifactId>fastjson</artifactId> <!-- 最新版screw-core1.0.5依赖fastjson1.2.73存在漏洞,移除。 -->
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
             <dependency>
             <dependency>
                 <groupId>com.google.guava</groupId>
                 <groupId>com.google.guava</groupId>
                 <artifactId>guava</artifactId>
                 <artifactId>guava</artifactId>

+ 51 - 2
citu-framework/citu-common/src/main/java/com/citu/framework/common/exception/util/ServiceExceptionUtil.java

@@ -6,24 +6,73 @@ import com.citu.framework.common.exception.enums.GlobalErrorCodeConstants;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
 /**
 /**
  * {@link ServiceException} 工具类
  * {@link ServiceException} 工具类
  *
  *
  * 目的在于,格式化异常信息提示。
  * 目的在于,格式化异常信息提示。
  * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
  * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
+ * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式:
  *
  *
+ * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration
+ * 2. 异常提示信息,写在 .properties 等等配置文件
+ * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新
+ * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新
  */
  */
 @Slf4j
 @Slf4j
 public class ServiceExceptionUtil {
 public class ServiceExceptionUtil {
 
 
+    /**
+     * 错误码提示模板
+     */
+    private static final ConcurrentMap<Integer, String> MESSAGES = new ConcurrentHashMap<>();
+
+    public static void putAll(Map<Integer, String> messages) {
+        ServiceExceptionUtil.MESSAGES.putAll(messages);
+    }
+
+    public static void put(Integer code, String message) {
+        ServiceExceptionUtil.MESSAGES.put(code, message);
+    }
+
+    public static void delete(Integer code, String message) {
+        ServiceExceptionUtil.MESSAGES.remove(code, message);
+    }
+
     // ========== 和 ServiceException 的集成 ==========
     // ========== 和 ServiceException 的集成 ==========
 
 
     public static ServiceException exception(ErrorCode errorCode) {
     public static ServiceException exception(ErrorCode errorCode) {
-        return exception0(errorCode.getCode(), errorCode.getMsg());
+        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
+        return exception0(errorCode.getCode(), messagePattern);
     }
     }
 
 
     public static ServiceException exception(ErrorCode errorCode, Object... params) {
     public static ServiceException exception(ErrorCode errorCode, Object... params) {
-        return exception0(errorCode.getCode(), errorCode.getMsg(), params);
+        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
+        return exception0(errorCode.getCode(), messagePattern, params);
+    }
+
+    /**
+     * 创建指定编号的 ServiceException 的异常
+     *
+     * @param code 编号
+     * @return 异常
+     */
+    public static ServiceException exception(Integer code) {
+        return exception0(code, MESSAGES.get(code));
+    }
+
+    /**
+     * 创建指定编号的 ServiceException 的异常
+     *
+     * @param code 编号
+     * @param params 消息提示的占位符对应的参数
+     * @return 异常
+     */
+    public static ServiceException exception(Integer code, Object... params) {
+        return exception0(code, MESSAGES.get(code), params);
     }
     }
 
 
     public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
     public static ServiceException exception0(Integer code, String messagePattern, Object... params) {

+ 39 - 0
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/config/CituErrorCodeAutoConfiguration.java

@@ -0,0 +1,39 @@
+package com.citu.framework.errorcode.config;
+
+import com.citu.framework.errorcode.core.generator.ErrorCodeAutoGenerator;
+import com.citu.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl;
+import com.citu.framework.errorcode.core.loader.ErrorCodeLoader;
+import com.citu.framework.errorcode.core.loader.ErrorCodeLoaderImpl;
+import com.citu.module.system.api.errorcode.ErrorCodeApi;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 错误码配置类
+ *
+ * @author 芋道源码
+ */
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "citu.error-code", value = "enable", matchIfMissing = true) // 允许使用 citu.error-code.enable=false 禁用访问日志
+@EnableConfigurationProperties(ErrorCodeProperties.class)
+@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码
+public class CituErrorCodeAutoConfiguration {
+
+    @Bean
+    public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName,
+                                                         ErrorCodeProperties errorCodeProperties,
+                                                         ErrorCodeApi errorCodeApi) {
+        return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), errorCodeApi);
+    }
+
+    @Bean
+    public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName,
+                                           ErrorCodeApi errorCodeApi) {
+        return new ErrorCodeLoaderImpl(applicationName, errorCodeApi);
+    }
+
+}

+ 30 - 0
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/config/ErrorCodeProperties.java

@@ -0,0 +1,30 @@
+package com.citu.framework.errorcode.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 错误码的配置属性类
+ *
+ * @author dlyan
+ */
+@ConfigurationProperties("citu.error-code")
+@Data
+@Validated
+public class ErrorCodeProperties {
+
+    /**
+     * 是否开启
+     */
+    private Boolean enable = true;
+    /**
+     * 错误码枚举类
+     */
+    @NotNull(message = "错误码枚举类不能为空")
+    private List<String> constantsClassList;
+
+}

+ 15 - 0
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java

@@ -0,0 +1,15 @@
+package com.citu.framework.errorcode.core.generator;
+
+/**
+ * 错误码的自动生成器
+ *
+ * @author dylan
+ */
+public interface ErrorCodeAutoGenerator {
+
+    /**
+     * 将配置类到错误码写入数据库
+     */
+    void execute();
+
+}

+ 108 - 0
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java

@@ -0,0 +1,108 @@
+package com.citu.framework.errorcode.core.generator;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.ClassUtil;
+import cn.hutool.core.util.ReflectUtil;
+import com.citu.framework.common.exception.ErrorCode;
+import com.citu.module.system.api.errorcode.ErrorCodeApi;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * ErrorCodeAutoGenerator 的实现类
+ * 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中
+ *
+ * @author dylan
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator {
+
+    /**
+     * 应用分组
+     */
+    private final String applicationName;
+    /**
+     * 错误码枚举类
+     */
+    private final List<String> constantsClassList;
+    /**
+     * 错误码 Api
+     */
+    private final ErrorCodeApi errorCodeApi;
+
+    @Override
+    @EventListener(ApplicationReadyEvent.class)
+    @Async // 异步,保证项目的启动过程,毕竟非关键流程
+    public void execute() {
+        // 第一步,解析错误码
+        List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = parseErrorCode();
+        log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size());
+
+        // 第二步,写入到 system 服务
+        try {
+            errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs);
+            log.info("[execute][写入到 system 组件完成]");
+        } catch (Exception ex) {
+            log.error("[execute][写入到 system 组件失败({})]", ExceptionUtil.getRootCauseMessage(ex));
+        }
+    }
+
+    /**
+     * 解析 constantsClassList 变量,转换成错误码数组
+     *
+     * @return 错误码数组
+     */
+    private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode() {
+        // 校验 errorCodeConstantsClass 参数
+        if (CollUtil.isEmpty(constantsClassList)) {
+            log.info("[execute][未配置 citu.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]");
+            return new ArrayList<>();
+        }
+
+        // 解析错误码
+        List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
+        constantsClassList.forEach(constantsClass -> {
+            try {
+                // 解析错误码枚举类
+                Class<?> errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass);
+                // 解析错误码
+                autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz));
+            } catch (Exception ex) {
+                log.warn("[parseErrorCode][constantsClass({}) 加载失败({})]", constantsClass,
+                        ExceptionUtil.getRootCauseMessage(ex));
+            }
+        });
+        return autoGenerateDTOs;
+    }
+
+    /**
+     * 解析错误码类,获得错误码数组
+     *
+     * @return 错误码数组
+     */
+    private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode(Class<?> constantsClass) {
+        List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
+        Arrays.stream(constantsClass.getFields()).forEach(field -> {
+            if (field.getType() != ErrorCode.class) {
+                return;
+            }
+            // 转换成 ErrorCodeAutoGenerateReqDTO 对象
+            ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field);
+            autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName)
+                    .setCode(errorCode.getCode()).setMessage(errorCode.getMsg()));
+        });
+        return autoGenerateDTOs;
+    }
+
+}
+

+ 35 - 0
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/loader/ErrorCodeLoader.java

@@ -0,0 +1,35 @@
+package com.citu.framework.errorcode.core.loader;
+
+
+import com.citu.framework.common.exception.util.ServiceExceptionUtil;
+
+/**
+ * 错误码加载器
+ *
+ * 注意,错误码最终加载到 {@link ServiceExceptionUtil} 的 MESSAGES 变量中!
+ *
+ * @author dlyan
+ */
+public interface ErrorCodeLoader {
+
+    /**
+     * 添加错误码
+     *
+     * @param code 错误码的编号
+     * @param msg 错误码的提示
+     */
+    default void putErrorCode(Integer code, String msg) {
+        ServiceExceptionUtil.put(code, msg);
+    }
+
+    /**
+     * 刷新错误码
+     */
+    void refreshErrorCodes();
+
+    /**
+     * 加载错误码
+     */
+    void loadErrorCodes();
+
+}

+ 82 - 0
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java

@@ -0,0 +1,82 @@
+package com.citu.framework.errorcode.core.loader;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import com.citu.framework.common.util.date.DateUtils;
+import com.citu.module.system.api.errorcode.ErrorCodeApi;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeRespDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。
+ * <p>
+ * 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。
+ *
+ * @author dlyan
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class ErrorCodeLoaderImpl implements ErrorCodeLoader {
+
+    /**
+     * 刷新错误码的频率,单位:毫秒
+     */
+    private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000;
+
+    /**
+     * 应用分组
+     */
+    private final String applicationName;
+    /**
+     * 错误码 Api
+     */
+    private final ErrorCodeApi errorCodeApi;
+
+    /**
+     * 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新
+     */
+    private LocalDateTime maxUpdateTime;
+
+    @Override
+    @EventListener(ApplicationReadyEvent.class)
+    @Async // 异步,保证项目的启动过程,毕竟非关键流程
+    public void loadErrorCodes() {
+        loadErrorCodes0();
+    }
+
+    @Override
+    @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD)
+    public void refreshErrorCodes() {
+        loadErrorCodes0();
+    }
+
+    private void loadErrorCodes0() {
+        try {
+            // 加载错误码
+            List<ErrorCodeRespDTO> errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime);
+            if (CollUtil.isEmpty(errorCodeRespDTOs)) {
+                return;
+            }
+            log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
+
+            // 刷新错误码的缓存
+            errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
+                // 写入到错误码的缓存
+                putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
+                // 记录下更新时间,方便增量更新
+                maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
+            });
+        } catch (Exception ex) {
+            log.error("[loadErrorCodes0][加载错误码失败({})]", ExceptionUtil.getRootCauseMessage(ex));
+        }
+    }
+
+}

+ 10 - 0
citu-framework/citu-spring-boot-starter-web/src/main/java/com/citu/framework/errorcode/package-info.java

@@ -0,0 +1,10 @@
+/**
+ * 错误码 ErrorCode 的自动配置功能,提供如下功能:
+ *
+ * 1. 远程读取:项目启动时,从 system-service 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置;
+ * 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-service 服务加载最新的 ErrorCode 错误码;
+ * 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑;
+ *
+ * @author 芋道源码
+ */
+package com.citu.framework.errorcode;

+ 1 - 0
citu-framework/citu-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -3,4 +3,5 @@ com.citu.framework.jackson.config.CituJacksonAutoConfiguration
 com.citu.framework.swagger.config.CituSwaggerAutoConfiguration
 com.citu.framework.swagger.config.CituSwaggerAutoConfiguration
 com.citu.framework.web.config.CituWebAutoConfiguration
 com.citu.framework.web.config.CituWebAutoConfiguration
 com.citu.framework.apilog.config.CituApiLogRpcAutoConfiguration
 com.citu.framework.apilog.config.CituApiLogRpcAutoConfiguration
+com.citu.framework.errorcode.config.CituErrorCodeAutoConfiguration
 com.citu.framework.banner.config.CituBannerAutoConfiguration
 com.citu.framework.banner.config.CituBannerAutoConfiguration

+ 5 - 0
citu-module-infra/citu-module-infra-biz/pom.xml

@@ -117,6 +117,11 @@
             <artifactId>velocity-engine-core</artifactId> <!-- 实现代码生成 -->
             <artifactId>velocity-engine-core</artifactId> <!-- 实现代码生成 -->
         </dependency>
         </dependency>
 
 
+        <dependency>
+            <groupId>cn.smallbun.screw</groupId>
+            <artifactId>screw-core</artifactId> <!-- 实现数据库文档 -->
+        </dependency>
+
         <!-- 监控相关 -->
         <!-- 监控相关 -->
         <dependency>
         <dependency>
             <groupId>com.citu</groupId>
             <groupId>com.citu</groupId>

+ 153 - 0
citu-module-infra/citu-module-infra-biz/src/main/java/com/citu/module/infra/controller/admin/db/DatabaseDocController.java

@@ -0,0 +1,153 @@
+package com.citu.module.infra.controller.admin.db;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.smallbun.screw.core.Configuration;
+import cn.smallbun.screw.core.engine.EngineConfig;
+import cn.smallbun.screw.core.engine.EngineFileType;
+import cn.smallbun.screw.core.engine.EngineTemplateType;
+import cn.smallbun.screw.core.execute.DocumentationExecute;
+import cn.smallbun.screw.core.process.ProcessConfig;
+import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
+import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
+import com.citu.framework.common.util.servlet.ServletUtils;
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+@Tag(name = "管理后台 - 数据库文档")
+@RestController
+@RequestMapping("/infra/db-doc")
+public class DatabaseDocController {
+
+    private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator
+            + "db-doc";
+    private static final String DOC_FILE_NAME = "数据库文档";
+    private static final String DOC_VERSION = "1.0.0";
+    private static final String DOC_DESCRIPTION = "文档描述";
+    @Resource
+    private DynamicDataSourceProperties dynamicDataSourceProperties;
+
+    /**
+     * 创建 screw 的引擎配置
+     */
+    private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) {
+        return EngineConfig.builder()
+                .fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径
+                .openOutputDir(false) // 打开目录
+                .fileType(fileOutputType) // 文件类型
+                .produceType(EngineTemplateType.velocity) // 文件类型
+                .fileName(docFileName) // 自定义文件名称
+                .build();
+    }
+
+    /**
+     * 创建 screw 的处理配置,一般可忽略
+     * 指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置
+     */
+    private static ProcessConfig buildProcessConfig() {
+        return ProcessConfig.builder()
+                .ignoreTablePrefix(Arrays.asList("QRTZ_", "ACT_", "FLW_")) // 忽略表前缀
+                .build();
+    }
+
+    @GetMapping("/export-html")
+    @Operation(summary = "导出 html 格式的数据文档")
+    @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true")
+    public void exportHtml(@RequestParam(defaultValue = "true") Boolean deleteFile,
+                           HttpServletResponse response) throws IOException {
+        doExportFile(EngineFileType.HTML, deleteFile, response);
+    }
+
+    @GetMapping("/export-word")
+    @Operation(summary = "导出 word 格式的数据文档")
+    @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true")
+    public void exportWord(@RequestParam(defaultValue = "true") Boolean deleteFile,
+                           HttpServletResponse response) throws IOException {
+        doExportFile(EngineFileType.WORD, deleteFile, response);
+    }
+
+    @GetMapping("/export-markdown")
+    @Operation(summary = "导出 markdown 格式的数据文档")
+    @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true")
+    public void exportMarkdown(@RequestParam(defaultValue = "true") Boolean deleteFile,
+                               HttpServletResponse response) throws IOException {
+        doExportFile(EngineFileType.MD, deleteFile, response);
+    }
+
+    private void doExportFile(EngineFileType fileOutputType, Boolean deleteFile,
+                              HttpServletResponse response) throws IOException {
+        String docFileName = DOC_FILE_NAME + "_" + IdUtil.fastSimpleUUID();
+        String filePath = doExportFile(fileOutputType, docFileName);
+        String downloadFileName = DOC_FILE_NAME + fileOutputType.getFileSuffix(); //下载后的文件名
+        try {
+            // 读取,返回
+            ServletUtils.writeAttachment(response, downloadFileName, FileUtil.readBytes(filePath));
+        } finally {
+            handleDeleteFile(deleteFile, filePath);
+        }
+    }
+
+    /**
+     * 输出文件,返回文件路径
+     *
+     * @param fileOutputType 文件类型
+     * @param fileName       文件名, 无需 ".docx" 等文件后缀
+     * @return 生成的文件所在路径
+     */
+    private String doExportFile(EngineFileType fileOutputType, String fileName) {
+        try (HikariDataSource dataSource = buildDataSource()) {
+            // 创建 screw 的配置
+            Configuration config = Configuration.builder()
+                    .version(DOC_VERSION)  // 版本
+                    .description(DOC_DESCRIPTION) // 描述
+                    .dataSource(dataSource) // 数据源
+                    .engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置
+                    .produceConfig(buildProcessConfig()) // 处理配置
+                    .build();
+
+            // 执行 screw,生成数据库文档
+            new DocumentationExecute(config).execute();
+
+            return FILE_OUTPUT_DIR + File.separator + fileName + fileOutputType.getFileSuffix();
+        }
+    }
+
+    private void handleDeleteFile(Boolean deleteFile, String filePath) {
+        if (!deleteFile) {
+            return;
+        }
+        FileUtil.del(filePath);
+    }
+
+    /**
+     * 创建数据源
+     */
+    // TODO 芋艿:screw 暂时不支持 druid,尴尬
+    private HikariDataSource buildDataSource() {
+        // 获得 DataSource 数据源,目前只支持首个
+        String primary = dynamicDataSourceProperties.getPrimary();
+        DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary);
+        // 创建 HikariConfig 配置类
+        HikariConfig hikariConfig = new HikariConfig();
+        hikariConfig.setJdbcUrl(dataSourceProperty.getUrl());
+        hikariConfig.setUsername(dataSourceProperty.getUsername());
+        hikariConfig.setPassword(dataSourceProperty.getPassword());
+        hikariConfig.addDataSourceProperty("useInformationSchema", "true"); // 设置可以获取 tables remarks 信息
+        // 创建数据源
+        return new HikariDataSource(hikariConfig);
+    }
+
+}

+ 8 - 0
citu-module-infra/citu-module-infra-biz/src/main/resources/application.yaml

@@ -143,6 +143,14 @@ citu:
     base-package: com.citu
     base-package: com.citu
     db-schemas: ${spring.datasource.dynamic.datasource.master.name}
     db-schemas: ${spring.datasource.dynamic.datasource.master.name}
     front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类
     front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类
+  error-code: # 错误码相关配置项
+    constants-class-list:
+      - com.citu.module.bpm.enums.ErrorCodeConstants
+      - com.citu.module.infra.enums.ErrorCodeConstants
+      - com.citu.module.member.enums.ErrorCodeConstants
+      - com.citu.module.pay.enums.ErrorCodeConstants
+      - com.citu.module.system.enums.ErrorCodeConstants
+      - com.citu.module.mp.enums.ErrorCodeConstants
   tenant: # 多租户相关配置项
   tenant: # 多租户相关配置项
     enable: true
     enable: true
     ignore-urls:
     ignore-urls:

+ 36 - 0
citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/errorcode/ErrorCodeApi.java

@@ -0,0 +1,36 @@
+package com.citu.module.system.api.errorcode;
+
+
+import com.citu.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeRespDTO;
+
+import javax.validation.Valid;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 错误码 Api 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErrorCodeApi {
+
+    /**
+     * 自动创建错误码
+     *
+     * @param autoGenerateDTOs 错误码信息
+     */
+    void autoGenerateErrorCodeList(@Valid List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs);
+
+    /**
+     * 增量获得错误码数组
+     * <p>
+     * 如果 minUpdateTime 为空时,则获取所有错误码
+     *
+     * @param applicationName 应用名
+     * @param minUpdateTime   最小更新时间
+     * @return 错误码数组
+     */
+    List<ErrorCodeRespDTO> getErrorCodeList(String applicationName, LocalDateTime minUpdateTime);
+
+}

+ 35 - 0
citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java

@@ -0,0 +1,35 @@
+package com.citu.module.system.api.errorcode.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+
+/**
+ * 错误码自动生成 DTO
+ *
+ * @author dylan
+ */
+@Data
+@Accessors(chain = true)
+public class ErrorCodeAutoGenerateReqDTO {
+
+    /**
+     * 应用名
+     */
+    @NotNull(message = "应用名不能为空")
+    private String applicationName;
+    /**
+     * 错误码编码
+     */
+    @NotNull(message = "错误码编码不能为空")
+    private Integer code;
+    /**
+     * 错误码错误提示
+     */
+    @NotEmpty(message = "错误码错误提示不能为空")
+    private String message;
+
+}

+ 28 - 0
citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/errorcode/dto/ErrorCodeRespDTO.java

@@ -0,0 +1,28 @@
+package com.citu.module.system.api.errorcode.dto;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 错误码的 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ErrorCodeRespDTO {
+
+    /**
+     * 错误码编码
+     */
+    private Integer code;
+    /**
+     * 错误码错误提示
+     */
+    private String message;
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+}

+ 30 - 0
citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/api/sensitiveword/SensitiveWordApi.java

@@ -0,0 +1,30 @@
+package com.citu.module.system.api.sensitiveword;
+
+import java.util.List;
+
+/**
+ * 敏感词 API 接口
+ *
+ * @author 永不言败
+ */
+public interface SensitiveWordApi {
+
+    /**
+     * 获得文本所包含的不合法的敏感词数组
+     *
+     * @param text 文本
+     * @param tags 标签数组
+     * @return 不合法的敏感词数组
+     */
+    List<String> validateText(String text, List<String> tags);
+
+    /**
+     * 判断文本是否包含敏感词
+     *
+     * @param text 文本
+     * @param tags 表述数组
+     * @return 是否包含
+     */
+    boolean isTextValid(String text, List<String> tags);
+
+}

+ 8 - 0
citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/enums/ErrorCodeConstants.java

@@ -115,6 +115,10 @@ public interface ErrorCodeConstants {
     ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1_002_016_001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除");
     ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1_002_016_001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除");
     ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1_002_016_002, "名字为【{}】的租户套餐已被禁用");
     ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1_002_016_002, "名字为【{}】的租户套餐已被禁用");
 
 
+    // ========== 错误码模块 1-002-017-000 ==========
+    ErrorCode ERROR_CODE_NOT_EXISTS = new ErrorCode(1_002_017_000, "错误码不存在");
+    ErrorCode ERROR_CODE_DUPLICATE = new ErrorCode(1_002_017_001, "已经存在编码为【{}】的错误码");
+
     // ========== 社交用户 1-002-018-000 ==========
     // ========== 社交用户 1-002-018-000 ==========
     ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1_002_018_000, "社交授权失败,原因是:{}");
     ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1_002_018_000, "社交授权失败,原因是:{}");
     ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1_002_018_001, "社交授权失败,找不到对应的用户");
     ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1_002_018_001, "社交授权失败,找不到对应的用户");
@@ -123,6 +127,10 @@ public interface ErrorCodeConstants {
     ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_201, "社交客户端不存在");
     ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_201, "社交客户端不存在");
     ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_202, "社交客户端已存在配置");
     ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_202, "社交客户端已存在配置");
 
 
+    // ========== 系统敏感词 1-002-019-000 =========
+    ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_002_019_000, "系统敏感词在所有标签中都不存在");
+    ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1_002_019_001, "系统敏感词已在标签中存在");
+
     // ========== OAuth2 客户端 1-002-020-000 =========
     // ========== OAuth2 客户端 1-002-020-000 =========
     ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在");
     ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在");
     ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在");
     ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在");

+ 39 - 0
citu-module-system/citu-module-system-api/src/main/java/com/citu/module/system/enums/errorcode/ErrorCodeTypeEnum.java

@@ -0,0 +1,39 @@
+package com.citu.module.system.enums.errorcode;
+
+import com.citu.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 错误码的类型枚举
+ *
+ * @author dylan
+ */
+@AllArgsConstructor
+@Getter
+public enum ErrorCodeTypeEnum implements IntArrayValuable {
+
+    /**
+     * 自动生成
+     */
+    AUTO_GENERATION(1),
+    /**
+     * 手动编辑
+     */
+    MANUAL_OPERATION(2);
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ErrorCodeTypeEnum::getType).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}

+ 33 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/api/errorcode/ErrorCodeApiImpl.java

@@ -0,0 +1,33 @@
+package com.citu.module.system.api.errorcode;
+
+import com.citu.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeRespDTO;
+import com.citu.module.system.service.errorcode.ErrorCodeService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 错误码 Api 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+public class ErrorCodeApiImpl implements ErrorCodeApi {
+
+    @Resource
+    private ErrorCodeService errorCodeService;
+
+    @Override
+    public void autoGenerateErrorCodeList(List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs) {
+        errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs);
+    }
+
+    @Override
+    public List<ErrorCodeRespDTO> getErrorCodeList(String applicationName, LocalDateTime minUpdateTime) {
+        return errorCodeService.getErrorCodeList(applicationName, minUpdateTime);
+    }
+
+}

+ 30 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/api/sensitiveword/SensitiveWordApiImpl.java

@@ -0,0 +1,30 @@
+package com.citu.module.system.api.sensitiveword;
+
+
+import com.citu.module.system.service.sensitiveword.SensitiveWordService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 敏感词 API 实现类
+ *
+ * @author 永不言败
+ */
+@Service
+public class SensitiveWordApiImpl implements SensitiveWordApi {
+
+    @Resource
+    private SensitiveWordService sensitiveWordService;
+
+    @Override
+    public List<String> validateText(String text, List<String> tags) {
+        return sensitiveWordService.validateText(text, tags);
+    }
+
+    @Override
+    public boolean isTextValid(String text, List<String> tags) {
+        return sensitiveWordService.isTextValid(text, tags);
+    }
+}

+ 13 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/ErrorCodeController.http

@@ -0,0 +1,13 @@
+### 创建错误码
+POST {{baseUrl}}/inra/error-code/create
+Authorization: Bearer {{token}}
+Content-Type: application/json
+tenant-id: {{adminTenentId}}
+
+{
+  "code": 200,
+  "message": "成功",
+  "group": "test",
+  "type": 1
+}
+

+ 95 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/ErrorCodeController.java

@@ -0,0 +1,95 @@
+package com.citu.module.system.controller.admin.errorcode;
+
+import com.citu.framework.apilog.core.annotation.ApiAccessLog;
+import com.citu.framework.common.pojo.CommonResult;
+import com.citu.framework.common.pojo.PageParam;
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.object.BeanUtils;
+import com.citu.framework.excel.core.util.ExcelUtils;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodeRespVO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodeSaveReqVO;
+import com.citu.module.system.dal.dataobject.errorcode.ErrorCodeDO;
+import com.citu.module.system.service.errorcode.ErrorCodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static com.citu.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static com.citu.framework.common.pojo.CommonResult.success;
+
+
+@Tag(name = "管理后台 - 错误码")
+@RestController
+@RequestMapping("/system/error-code")
+@Validated
+public class ErrorCodeController {
+
+    @Resource
+    private ErrorCodeService errorCodeService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建错误码")
+    @PreAuthorize("@ss.hasPermission('system:error-code:create')")
+    public CommonResult<Long> createErrorCode(@Valid @RequestBody ErrorCodeSaveReqVO createReqVO) {
+        return success(errorCodeService.createErrorCode(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新错误码")
+    @PreAuthorize("@ss.hasPermission('system:error-code:update')")
+    public CommonResult<Boolean> updateErrorCode(@Valid @RequestBody ErrorCodeSaveReqVO updateReqVO) {
+        errorCodeService.updateErrorCode(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除错误码")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('system:error-code:delete')")
+    public CommonResult<Boolean> deleteErrorCode(@RequestParam("id") Long id) {
+        errorCodeService.deleteErrorCode(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得错误码")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('system:error-code:query')")
+    public CommonResult<ErrorCodeRespVO> getErrorCode(@RequestParam("id") Long id) {
+        ErrorCodeDO errorCode = errorCodeService.getErrorCode(id);
+        return success(BeanUtils.toBean(errorCode, ErrorCodeRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得错误码分页")
+    @PreAuthorize("@ss.hasPermission('system:error-code:query')")
+    public CommonResult<PageResult<ErrorCodeRespVO>> getErrorCodePage(@Valid ErrorCodePageReqVO pageVO) {
+        PageResult<ErrorCodeDO> pageResult = errorCodeService.getErrorCodePage(pageVO);
+        return success(BeanUtils.toBean(pageResult, ErrorCodeRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出错误码 Excel")
+    @PreAuthorize("@ss.hasPermission('system:error-code:export')")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportErrorCodeExcel(@Valid ErrorCodePageReqVO exportReqVO,
+                                     HttpServletResponse response) throws IOException {
+        exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErrorCodeDO> list = errorCodeService.getErrorCodePage(exportReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "错误码.xls", "数据", ErrorCodeRespVO.class,
+                BeanUtils.toBean(list, ErrorCodeRespVO.class));
+    }
+
+}

+ 37 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java

@@ -0,0 +1,37 @@
+package com.citu.module.system.controller.admin.errorcode.vo;
+
+import com.citu.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.citu.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+
+@Schema(description = "管理后台 - 错误码分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErrorCodePageReqVO extends PageParam {
+
+    @Schema(description = "错误码类型,参见 ErrorCodeTypeEnum 枚举类", example = "1")
+    private Integer type;
+
+    @Schema(description = "应用名", example = "dashboard")
+    private String applicationName;
+
+    @Schema(description = "错误码编码", example = "1234")
+    private Integer code;
+
+    @Schema(description = "错误码错误提示", example = "帅气")
+    private String message;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @Schema(description = "创建时间")
+    private LocalDateTime[] createTime;
+
+}

+ 48 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java

@@ -0,0 +1,48 @@
+package com.citu.module.system.controller.admin.errorcode.vo;
+
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.citu.framework.excel.core.annotations.DictFormat;
+import com.citu.framework.excel.core.convert.DictConvert;
+import com.citu.module.system.enums.DictTypeConstants;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 错误码 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErrorCodeRespVO {
+
+    @Schema(description = "错误码编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("错误码编号")
+    private Long id;
+
+    @Schema(description = "错误码类型,参见 ErrorCodeTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "错误码类型", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.ERROR_CODE_TYPE)
+    private Integer type;
+
+    @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard")
+    @ExcelProperty("应用名")
+    private String applicationName;
+
+    @Schema(description = "错误码编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234")
+    @ExcelProperty("错误码编码")
+    private Integer code;
+
+    @Schema(description = "错误码错误提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "帅气")
+    @ExcelProperty("错误码错误提示")
+    private String message;
+
+    @Schema(description = "备注", example = "哈哈哈")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 31 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/errorcode/vo/ErrorCodeSaveReqVO.java

@@ -0,0 +1,31 @@
+package com.citu.module.system.controller.admin.errorcode.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+
+@Schema(description = "管理后台 - 错误码创建/修改 Request VO")
+@Data
+public class ErrorCodeSaveReqVO {
+
+    @Schema(description = "错误码编号", example = "1024")
+    private Long id;
+
+    @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard")
+    @NotNull(message = "应用名不能为空")
+    private String applicationName;
+
+    @Schema(description = "错误码编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234")
+    @NotNull(message = "错误码编码不能为空")
+    private Integer code;
+
+    @Schema(description = "错误码错误提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "帅气")
+    @NotNull(message = "错误码错误提示不能为空")
+    private String message;
+
+    @Schema(description = "备注", example = "哈哈哈")
+    private String memo;
+
+}

+ 4 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/SensitiveWordController.http

@@ -0,0 +1,4 @@
+### 请求 /system/sensitive-word/validate-text 接口 => 成功
+GET {{baseUrl}}/system/sensitive-word/validate-text?text=XXX&tags=短信&tags=蔬菜
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}

+ 110 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/SensitiveWordController.java

@@ -0,0 +1,110 @@
+package com.citu.module.system.controller.admin.sensitiveword;
+
+import com.citu.framework.apilog.core.annotation.ApiAccessLog;
+import com.citu.framework.common.pojo.CommonResult;
+import com.citu.framework.common.pojo.PageParam;
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.object.BeanUtils;
+import com.citu.framework.excel.core.util.ExcelUtils;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordRespVO;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordSaveVO;
+
+import com.citu.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
+import com.citu.module.system.service.sensitiveword.SensitiveWordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+import static com.citu.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static com.citu.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 敏感词")
+@RestController
+@RequestMapping("/system/sensitive-word")
+@Validated
+public class SensitiveWordController {
+
+    @Resource
+    private SensitiveWordService sensitiveWordService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建敏感词")
+    @PreAuthorize("@ss.hasPermission('system:sensitive-word:create')")
+    public CommonResult<Long> createSensitiveWord(@Valid @RequestBody SensitiveWordSaveVO createReqVO) {
+        return success(sensitiveWordService.createSensitiveWord(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新敏感词")
+    @PreAuthorize("@ss.hasPermission('system:sensitive-word:update')")
+    public CommonResult<Boolean> updateSensitiveWord(@Valid @RequestBody SensitiveWordSaveVO updateReqVO) {
+        sensitiveWordService.updateSensitiveWord(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除敏感词")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('system:sensitive-word:delete')")
+    public CommonResult<Boolean> deleteSensitiveWord(@RequestParam("id") Long id) {
+        sensitiveWordService.deleteSensitiveWord(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得敏感词")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')")
+    public CommonResult<SensitiveWordRespVO> getSensitiveWord(@RequestParam("id") Long id) {
+        SensitiveWordDO sensitiveWord = sensitiveWordService.getSensitiveWord(id);
+        return success(BeanUtils.toBean(sensitiveWord, SensitiveWordRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得敏感词分页")
+    @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')")
+    public CommonResult<PageResult<SensitiveWordRespVO>> getSensitiveWordPage(@Valid SensitiveWordPageReqVO pageVO) {
+        PageResult<SensitiveWordDO> pageResult = sensitiveWordService.getSensitiveWordPage(pageVO);
+        return success(BeanUtils.toBean(pageResult, SensitiveWordRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出敏感词 Excel")
+    @PreAuthorize("@ss.hasPermission('system:sensitive-word:export')")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportSensitiveWordExcel(@Valid SensitiveWordPageReqVO exportReqVO,
+                                         HttpServletResponse response) throws IOException {
+        exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<SensitiveWordDO> list = sensitiveWordService.getSensitiveWordPage(exportReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "敏感词.xls", "数据", SensitiveWordRespVO.class,
+                BeanUtils.toBean(list, SensitiveWordRespVO.class));
+    }
+
+    @GetMapping("/get-tags")
+    @Operation(summary = "获取所有敏感词的标签数组")
+    @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')")
+    public CommonResult<Set<String>> getSensitiveWordTagSet() {
+        return success(sensitiveWordService.getSensitiveWordTagSet());
+    }
+
+    @GetMapping("/validate-text")
+    @Operation(summary = "获得文本所包含的不合法的敏感词数组")
+    public CommonResult<List<String>> validateText(@RequestParam("text") String text,
+                                                   @RequestParam(value = "tags", required = false) List<String> tags) {
+        return success(sensitiveWordService.validateText(text, tags));
+    }
+
+}

+ 34 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java

@@ -0,0 +1,34 @@
+package com.citu.module.system.controller.admin.sensitiveword.vo;
+
+
+import com.citu.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.citu.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 敏感词分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class SensitiveWordPageReqVO extends PageParam {
+
+    @Schema(description = "敏感词", example = "敏感词")
+    private String name;
+
+    @Schema(description = "标签", example = "短信,评论")
+    private String tag;
+
+    @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1")
+    private Integer status;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @Schema(description = "创建时间")
+    private LocalDateTime[] createTime;
+
+}

+ 46 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java

@@ -0,0 +1,46 @@
+package com.citu.module.system.controller.admin.sensitiveword.vo;
+
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.citu.framework.excel.core.annotations.DictFormat;
+import com.citu.framework.excel.core.convert.DictConvert;
+import com.citu.framework.excel.core.convert.JsonConvert;
+import com.citu.module.system.enums.DictTypeConstants;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 敏感词 Response VO")
+@Data
+public class SensitiveWordRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词")
+    @ExcelProperty("敏感词")
+    private String name;
+
+    @Schema(description = "标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "短信,评论")
+    @ExcelProperty(value = "标签", converter = JsonConvert.class)
+    private List<String> tags;
+
+    @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat(DictTypeConstants.COMMON_STATUS)
+    private Integer status;
+
+    @Schema(description = "描述", example = "污言秽语")
+    @ExcelProperty("描述")
+    private String description;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 32 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/controller/admin/sensitiveword/vo/SensitiveWordSaveVO.java

@@ -0,0 +1,32 @@
+package com.citu.module.system.controller.admin.sensitiveword.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 敏感词创建/修改 Request VO")
+@Data
+public class SensitiveWordSaveVO {
+
+    @Schema(description = "编号", example = "1")
+    private Long id;
+
+    @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词")
+    @NotNull(message = "敏感词不能为空")
+    private String name;
+
+    @Schema(description = "标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "短信,评论")
+    @NotNull(message = "标签不能为空")
+    private List<String> tags;
+
+    @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "状态不能为空")
+    private Integer status;
+
+    @Schema(description = "描述", example = "污言秽语")
+    private String description;
+
+}

+ 53 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/dataobject/errorcode/ErrorCodeDO.java

@@ -0,0 +1,53 @@
+package com.citu.module.system.dal.dataobject.errorcode;
+
+
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.citu.framework.mybatis.core.dataobject.BaseDO;
+import com.citu.module.system.enums.errorcode.ErrorCodeTypeEnum;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * 错误码表
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "system_error_code")
+@KeySequence("system_error_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErrorCodeDO extends BaseDO {
+
+    /**
+     * 错误码编号,自增
+     */
+    @TableId
+    private Long id;
+    /**
+     * 错误码类型
+     *
+     * 枚举 {@link ErrorCodeTypeEnum}
+     */
+    private Integer type;
+    /**
+     * 应用名
+     */
+    private String applicationName;
+    /**
+     * 错误码编码
+     */
+    private Integer code;
+    /**
+     * 错误码错误提示
+     */
+    private String message;
+    /**
+     * 错误码备注
+     */
+    private String memo;
+
+}

+ 58 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java

@@ -0,0 +1,58 @@
+package com.citu.module.system.dal.dataobject.sensitiveword;
+
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.citu.framework.common.enums.CommonStatusEnum;
+import com.citu.framework.mybatis.core.dataobject.BaseDO;
+import com.citu.framework.mybatis.core.type.StringListTypeHandler;
+import lombok.*;
+
+import java.util.List;
+
+/**
+ * 敏感词 DO
+ *
+ * @author 永不言败
+ */
+@TableName(value = "system_sensitive_word", autoResultMap = true)
+@KeySequence("system_sensitive_word_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SensitiveWordDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 敏感词
+     */
+    private String name;
+    /**
+     * 描述
+     */
+    private String description;
+    /**
+     * 标签数组
+     *
+     * 用于实现不同的业务场景下,需要使用不同标签的敏感词。
+     * 例如说,tag 有短信、论坛两种,敏感词 "推广" 在短信下是敏感词,在论坛下不是敏感词。
+     * 此时,我们会存储一条敏感词记录,它的 name 为"推广",tag 为短信。
+     */
+    @TableField(typeHandler = StringListTypeHandler.class)
+    private List<String> tags;
+    /**
+     * 状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+
+}

+ 41 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/mysql/errorcode/ErrorCodeMapper.java

@@ -0,0 +1,41 @@
+package com.citu.module.system.dal.mysql.errorcode;
+
+
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.mybatis.core.mapper.BaseMapperX;
+import com.citu.framework.mybatis.core.query.LambdaQueryWrapperX;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO;
+import com.citu.module.system.dal.dataobject.errorcode.ErrorCodeDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+@Mapper
+public interface ErrorCodeMapper extends BaseMapperX<ErrorCodeDO> {
+
+    default PageResult<ErrorCodeDO> selectPage(ErrorCodePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ErrorCodeDO>()
+                .eqIfPresent(ErrorCodeDO::getType, reqVO.getType())
+                .likeIfPresent(ErrorCodeDO::getApplicationName, reqVO.getApplicationName())
+                .eqIfPresent(ErrorCodeDO::getCode, reqVO.getCode())
+                .likeIfPresent(ErrorCodeDO::getMessage, reqVO.getMessage())
+                .betweenIfPresent(ErrorCodeDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ErrorCodeDO::getCode));
+    }
+
+    default List<ErrorCodeDO> selectListByCodes(Collection<Integer> codes) {
+        return selectList(ErrorCodeDO::getCode, codes);
+    }
+
+    default ErrorCodeDO selectByCode(Integer code) {
+        return selectOne(ErrorCodeDO::getCode, code);
+    }
+
+    default List<ErrorCodeDO> selectListByApplicationNameAndUpdateTimeGt(String applicationName, LocalDateTime minUpdateTime) {
+        return selectList(new LambdaQueryWrapperX<ErrorCodeDO>().eq(ErrorCodeDO::getApplicationName, applicationName)
+                .gtIfPresent(ErrorCodeDO::getUpdateTime, minUpdateTime));
+    }
+
+}

+ 37 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java

@@ -0,0 +1,37 @@
+package com.citu.module.system.dal.mysql.sensitiveword;
+
+
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.mybatis.core.mapper.BaseMapperX;
+import com.citu.framework.mybatis.core.query.LambdaQueryWrapperX;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
+import com.citu.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.time.LocalDateTime;
+
+/**
+ * 敏感词 Mapper
+ *
+ * @author 永不言败
+ */
+@Mapper
+public interface SensitiveWordMapper extends BaseMapperX<SensitiveWordDO> {
+
+    default PageResult<SensitiveWordDO> selectPage(SensitiveWordPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<SensitiveWordDO>()
+                .likeIfPresent(SensitiveWordDO::getName, reqVO.getName())
+                .likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag())
+                .eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(SensitiveWordDO::getId));
+    }
+    default SensitiveWordDO selectByName(String name) {
+        return selectOne(SensitiveWordDO::getName, name);
+    }
+
+    @Select("SELECT COUNT(*) FROM system_sensitive_word WHERE update_time > #{maxUpdateTime}")
+    Long selectCountByUpdateTimeGt(LocalDateTime maxTime);
+
+}

+ 78 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/errorcode/ErrorCodeService.java

@@ -0,0 +1,78 @@
+package com.citu.module.system.service.errorcode;
+
+
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeRespDTO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodeSaveReqVO;
+import com.citu.module.system.dal.dataobject.errorcode.ErrorCodeDO;
+
+import javax.validation.Valid;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 错误码 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErrorCodeService {
+
+    /**
+     * 自动创建错误码
+     *
+     * @param autoGenerateDTOs 错误码信息
+     */
+    void autoGenerateErrorCodes(@Valid List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs);
+
+    /**
+     * 增量获得错误码数组
+     *
+     * 如果 minUpdateTime 为空时,则获取所有错误码
+     *
+     * @param applicationName 应用名
+     * @param minUpdateTime 最小更新时间
+     * @return 错误码数组
+     */
+    List<ErrorCodeRespDTO> getErrorCodeList(String applicationName, LocalDateTime minUpdateTime);
+
+    /**
+     * 创建错误码
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createErrorCode(@Valid ErrorCodeSaveReqVO createReqVO);
+
+    /**
+     * 更新错误码
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateErrorCode(@Valid ErrorCodeSaveReqVO updateReqVO);
+
+    /**
+     * 删除错误码
+     *
+     * @param id 编号
+     */
+    void deleteErrorCode(Long id);
+
+    /**
+     * 获得错误码
+     *
+     * @param id 编号
+     * @return 错误码
+     */
+    ErrorCodeDO getErrorCode(Long id);
+
+    /**
+     * 获得错误码分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 错误码分页
+     */
+    PageResult<ErrorCodeDO> getErrorCodePage(ErrorCodePageReqVO pageReqVO);
+
+}

+ 170 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/errorcode/ErrorCodeServiceImpl.java

@@ -0,0 +1,170 @@
+package com.citu.module.system.service.errorcode;
+
+import cn.hutool.core.collection.CollUtil;
+
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.object.BeanUtils;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeRespDTO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodeSaveReqVO;
+import com.citu.module.system.dal.dataobject.errorcode.ErrorCodeDO;
+import com.citu.module.system.dal.mysql.errorcode.ErrorCodeMapper;
+import com.citu.module.system.enums.errorcode.ErrorCodeTypeEnum;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+import static com.citu.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static com.citu.framework.common.util.collection.CollectionUtils.convertMap;
+import static com.citu.framework.common.util.collection.CollectionUtils.convertSet;
+import static com.citu.module.system.enums.ErrorCodeConstants.ERROR_CODE_DUPLICATE;
+import static com.citu.module.system.enums.ErrorCodeConstants.ERROR_CODE_NOT_EXISTS;
+
+
+/**
+ * 错误码 Service 实现类
+ *
+ * @author dlyan
+ */
+@Service
+@Validated
+@Slf4j
+public class ErrorCodeServiceImpl implements ErrorCodeService {
+
+    @Resource
+    private ErrorCodeMapper errorCodeMapper;
+
+    @Override
+    public Long createErrorCode(ErrorCodeSaveReqVO createReqVO) {
+        // 校验 code 重复
+        validateCodeDuplicate(createReqVO.getCode(), null);
+
+        // 插入
+        ErrorCodeDO errorCode = BeanUtils.toBean(createReqVO, ErrorCodeDO.class)
+                .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType());
+        errorCodeMapper.insert(errorCode);
+        // 返回
+        return errorCode.getId();
+    }
+
+    @Override
+    public void updateErrorCode(ErrorCodeSaveReqVO updateReqVO) {
+        // 校验存在
+        validateErrorCodeExists(updateReqVO.getId());
+        // 校验 code 重复
+        validateCodeDuplicate(updateReqVO.getCode(), updateReqVO.getId());
+
+        // 更新
+        ErrorCodeDO updateObj = BeanUtils.toBean(updateReqVO, ErrorCodeDO.class)
+                .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType());
+        errorCodeMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteErrorCode(Long id) {
+        // 校验存在
+        validateErrorCodeExists(id);
+        // 删除
+        errorCodeMapper.deleteById(id);
+    }
+
+    /**
+     * 校验错误码的唯一字段是否重复
+     *
+     * 是否存在相同编码的错误码
+     *
+     * @param code 错误码编码
+     * @param id 错误码编号
+     */
+    @VisibleForTesting
+    public void validateCodeDuplicate(Integer code, Long id) {
+        ErrorCodeDO errorCodeDO = errorCodeMapper.selectByCode(code);
+        if (errorCodeDO == null) {
+            return;
+        }
+        // 如果 id 为空,说明不用比较是否为相同 id 的错误码
+        if (id == null) {
+            throw exception(ERROR_CODE_DUPLICATE);
+        }
+        if (!errorCodeDO.getId().equals(id)) {
+            throw exception(ERROR_CODE_DUPLICATE);
+        }
+    }
+
+    @VisibleForTesting
+    void validateErrorCodeExists(Long id) {
+        if (errorCodeMapper.selectById(id) == null) {
+            throw exception(ERROR_CODE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ErrorCodeDO getErrorCode(Long id) {
+        return errorCodeMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ErrorCodeDO> getErrorCodePage(ErrorCodePageReqVO pageReqVO) {
+        return errorCodeMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    @Transactional
+    public void autoGenerateErrorCodes(List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs) {
+        if (CollUtil.isEmpty(autoGenerateDTOs)) {
+            return;
+        }
+        // 获得错误码
+        List<ErrorCodeDO> errorCodeDOs = errorCodeMapper.selectListByCodes(
+                convertSet(autoGenerateDTOs, ErrorCodeAutoGenerateReqDTO::getCode));
+        Map<Integer, ErrorCodeDO> errorCodeDOMap = convertMap(errorCodeDOs, ErrorCodeDO::getCode);
+
+        // 遍历 autoGenerateBOs 数组,逐个插入或更新。考虑到每次量级不大,就不走批量了
+        autoGenerateDTOs.forEach(autoGenerateDTO -> {
+            ErrorCodeDO errorCode = errorCodeDOMap.get(autoGenerateDTO.getCode());
+            // 不存在,则进行新增
+            if (errorCode == null) {
+                errorCode = BeanUtils.toBean(autoGenerateDTO, ErrorCodeDO.class)
+                        .setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType());
+                errorCodeMapper.insert(errorCode);
+                return;
+            }
+            // 存在,则进行更新。更新有三个前置条件:
+            // 条件 1. 只更新自动生成的错误码,即 Type 为 ErrorCodeTypeEnum.AUTO_GENERATION
+            if (!ErrorCodeTypeEnum.AUTO_GENERATION.getType().equals(errorCode.getType())) {
+                return;
+            }
+            // 条件 2. 分组 applicationName 必须匹配,避免存在错误码冲突的情况
+            if (!autoGenerateDTO.getApplicationName().equals(errorCode.getApplicationName())) {
+                log.error("[autoGenerateErrorCodes][自动创建({}/{}) 错误码失败,数据库中已经存在({}/{})]",
+                        autoGenerateDTO.getCode(), autoGenerateDTO.getApplicationName(),
+                        errorCode.getCode(), errorCode.getApplicationName());
+                return;
+            }
+            // 条件 3. 错误提示语存在差异
+            if (autoGenerateDTO.getMessage().equals(errorCode.getMessage())) {
+                return;
+            }
+            // 最终匹配,进行更新
+            errorCodeMapper.updateById(new ErrorCodeDO().setId(errorCode.getId()).setMessage(autoGenerateDTO.getMessage()));
+        });
+    }
+
+    @Override
+    public List<ErrorCodeRespDTO> getErrorCodeList(String applicationName, LocalDateTime minUpdateTime) {
+        List<ErrorCodeDO> list = errorCodeMapper.selectListByApplicationNameAndUpdateTimeGt(
+                applicationName, minUpdateTime);
+        return BeanUtils.toBean(list, ErrorCodeRespDTO.class);
+    }
+
+}
+

+ 90 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/sensitiveword/SensitiveWordService.java

@@ -0,0 +1,90 @@
+package com.citu.module.system.service.sensitiveword;
+
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordSaveVO;
+import com.citu.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
+
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 敏感词 Service 接口
+ *
+ * @author 永不言败
+ */
+public interface SensitiveWordService {
+
+    /**
+     * 创建敏感词
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createSensitiveWord(@Valid SensitiveWordSaveVO createReqVO);
+
+    /**
+     * 更新敏感词
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateSensitiveWord(@Valid SensitiveWordSaveVO updateReqVO);
+
+    /**
+     * 删除敏感词
+     *
+     * @param id 编号
+     */
+    void deleteSensitiveWord(Long id);
+
+    /**
+     * 获得敏感词
+     *
+     * @param id 编号
+     * @return 敏感词
+     */
+    SensitiveWordDO getSensitiveWord(Long id);
+
+    /**
+     * 获得敏感词列表
+     *
+     * @return 敏感词列表
+     */
+    List<SensitiveWordDO> getSensitiveWordList();
+
+    /**
+     * 获得敏感词分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 敏感词分页
+     */
+    PageResult<SensitiveWordDO> getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO);
+
+    /**
+     * 获得所有敏感词的标签数组
+     *
+     * @return 标签数组
+     */
+    Set<String> getSensitiveWordTagSet();
+
+    /**
+     * 获得文本所包含的不合法的敏感词数组
+     *
+     * @param text 文本
+     * @param tags 标签数组
+     * @return 不合法的敏感词数组
+     */
+    List<String> validateText(String text, List<String> tags);
+
+    /**
+     * 判断文本是否包含敏感词
+     *
+     * @param text 文本
+     * @param tags 标签数组
+     * @return 是否包含敏感词
+     */
+    boolean isTextValid(String text, List<String> tags);
+
+}

+ 265 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/service/sensitiveword/SensitiveWordServiceImpl.java

@@ -0,0 +1,265 @@
+package com.citu.module.system.service.sensitiveword;
+
+import cn.hutool.core.collection.CollUtil;
+import com.citu.framework.common.enums.CommonStatusEnum;
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.collection.CollectionUtils;
+import com.citu.framework.common.util.object.BeanUtils;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordSaveVO;
+
+import com.citu.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
+import com.citu.module.system.dal.mysql.sensitiveword.SensitiveWordMapper;
+
+import com.citu.module.system.util.collection.SimpleTrie;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import org.springframework.validation.annotation.Validated;
+
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import static com.citu.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static com.citu.framework.common.util.collection.CollectionUtils.filterList;
+import static com.citu.framework.common.util.collection.CollectionUtils.getMaxValue;
+import static com.citu.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_EXISTS;
+import static com.citu.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
+
+/**
+ * 敏感词 Service 实现类
+ *
+ * @author 永不言败
+ */
+@Service
+@Slf4j
+@Validated
+public class SensitiveWordServiceImpl implements SensitiveWordService {
+
+    /**
+     * 是否开启敏感词功能
+     */
+    public static Boolean ENABLED = false;
+
+    /**
+     * 敏感词列表缓存
+     */
+    @Getter
+    private volatile List<SensitiveWordDO> sensitiveWordCache = Collections.emptyList();
+    /**
+     * 敏感词标签缓存
+     * key:敏感词编号 {@link SensitiveWordDO#getId()}
+     * <p>
+     * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
+     */
+    @Getter
+    private volatile Set<String> sensitiveWordTagsCache = Collections.emptySet();
+
+    @Resource
+    private SensitiveWordMapper sensitiveWordMapper;
+
+    /**
+     * 默认的敏感词的字典树,包含所有敏感词
+     */
+    @Getter
+    private volatile SimpleTrie defaultSensitiveWordTrie = new SimpleTrie(Collections.emptySet());
+    /**
+     * 标签与敏感词的字段数的映射
+     */
+    @Getter
+    private volatile Map<String, SimpleTrie> tagSensitiveWordTries = Collections.emptyMap();
+
+    /**
+     * 初始化缓存
+     */
+    @PostConstruct
+    public void initLocalCache() {
+        if (!ENABLED) {
+            return;
+        }
+
+        // 第一步:查询数据
+        List<SensitiveWordDO> sensitiveWords = sensitiveWordMapper.selectList();
+        log.info("[initLocalCache][缓存敏感词,数量为:{}]", sensitiveWords.size());
+
+        // 第二步:构建缓存
+        // 写入 sensitiveWordTagsCache 缓存
+        Set<String> tags = new HashSet<>();
+        sensitiveWords.forEach(word -> tags.addAll(word.getTags()));
+        sensitiveWordTagsCache = tags;
+        sensitiveWordCache = sensitiveWords;
+        // 写入 defaultSensitiveWordTrie、tagSensitiveWordTries 缓存
+        initSensitiveWordTrie(sensitiveWords);
+    }
+
+    private void initSensitiveWordTrie(List<SensitiveWordDO> wordDOs) {
+        // 过滤禁用的敏感词
+        wordDOs = filterList(wordDOs, word -> word.getStatus().equals(CommonStatusEnum.ENABLE.getStatus()));
+
+        // 初始化默认的 defaultSensitiveWordTrie
+        this.defaultSensitiveWordTrie = new SimpleTrie(CollectionUtils.convertList(wordDOs, SensitiveWordDO::getName));
+
+        // 初始化 tagSensitiveWordTries
+        Multimap<String, String> tagWords = HashMultimap.create();
+        for (SensitiveWordDO word : wordDOs) {
+            if (CollUtil.isEmpty(word.getTags())) {
+                continue;
+            }
+            word.getTags().forEach(tag -> tagWords.put(tag, word.getName()));
+        }
+        // 添加到 tagSensitiveWordTries 中
+        Map<String, SimpleTrie> tagSensitiveWordTries = new HashMap<>();
+        tagWords.asMap().forEach((tag, words) -> tagSensitiveWordTries.put(tag, new SimpleTrie(words)));
+        this.tagSensitiveWordTries = tagSensitiveWordTries;
+    }
+
+    /**
+     * 通过定时任务轮询,刷新缓存
+     *
+     * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新
+     */
+    @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
+    public void refreshLocalCache() {
+        // 情况一:如果缓存里没有数据,则直接刷新缓存
+        if (CollUtil.isEmpty(sensitiveWordCache)) {
+            initLocalCache();
+            return;
+        }
+
+        // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存
+        LocalDateTime maxTime = getMaxValue(sensitiveWordCache, SensitiveWordDO::getUpdateTime);
+        if (sensitiveWordMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
+            initLocalCache();
+        }
+    }
+
+    @Override
+    public Long createSensitiveWord(SensitiveWordSaveVO createReqVO) {
+        // 校验唯一性
+        validateSensitiveWordNameUnique(null, createReqVO.getName());
+
+        // 插入
+        SensitiveWordDO sensitiveWord = BeanUtils.toBean(createReqVO, SensitiveWordDO.class);
+        sensitiveWordMapper.insert(sensitiveWord);
+
+        // 刷新缓存
+        initLocalCache();
+        return sensitiveWord.getId();
+    }
+
+    @Override
+    public void updateSensitiveWord(SensitiveWordSaveVO updateReqVO) {
+        // 校验唯一性
+        validateSensitiveWordExists(updateReqVO.getId());
+        validateSensitiveWordNameUnique(updateReqVO.getId(), updateReqVO.getName());
+
+        // 更新
+        SensitiveWordDO updateObj = BeanUtils.toBean(updateReqVO, SensitiveWordDO.class);
+        sensitiveWordMapper.updateById(updateObj);
+
+        // 刷新缓存
+        initLocalCache();
+    }
+
+    @Override
+    public void deleteSensitiveWord(Long id) {
+        // 校验存在
+        validateSensitiveWordExists(id);
+        // 删除
+        sensitiveWordMapper.deleteById(id);
+
+        // 刷新缓存
+        initLocalCache();
+    }
+
+    private void validateSensitiveWordNameUnique(Long id, String name) {
+        SensitiveWordDO word = sensitiveWordMapper.selectByName(name);
+        if (word == null) {
+            return;
+        }
+        // 如果 id 为空,说明不用比较是否为相同 id 的敏感词
+        if (id == null) {
+            throw exception(SENSITIVE_WORD_EXISTS);
+        }
+        if (!word.getId().equals(id)) {
+            throw exception(SENSITIVE_WORD_EXISTS);
+        }
+    }
+
+    private void validateSensitiveWordExists(Long id) {
+        if (sensitiveWordMapper.selectById(id) == null) {
+            throw exception(SENSITIVE_WORD_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public SensitiveWordDO getSensitiveWord(Long id) {
+        return sensitiveWordMapper.selectById(id);
+    }
+
+    @Override
+    public List<SensitiveWordDO> getSensitiveWordList() {
+        return sensitiveWordMapper.selectList();
+    }
+
+    @Override
+    public PageResult<SensitiveWordDO> getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO) {
+        return sensitiveWordMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public Set<String> getSensitiveWordTagSet() {
+        return sensitiveWordTagsCache;
+    }
+
+    @Override
+    public List<String> validateText(String text, List<String> tags) {
+        Assert.isTrue(ENABLED, "敏感词功能未开启,请将 ENABLED 设置为 true");
+
+        // 无标签时,默认所有
+        if (CollUtil.isEmpty(tags)) {
+            return defaultSensitiveWordTrie.validate(text);
+        }
+        // 有标签的情况
+        Set<String> result = new HashSet<>();
+        tags.forEach(tag -> {
+            SimpleTrie trie = tagSensitiveWordTries.get(tag);
+            if (trie == null) {
+                return;
+            }
+            result.addAll(trie.validate(text));
+        });
+        return new ArrayList<>(result);
+    }
+
+    @Override
+    public boolean isTextValid(String text, List<String> tags) {
+        Assert.isTrue(ENABLED, "敏感词功能未开启,请将 ENABLED 设置为 true");
+
+        // 无标签时,默认所有
+        if (CollUtil.isEmpty(tags)) {
+            return defaultSensitiveWordTrie.isValid(text);
+        }
+        // 有标签的情况
+        for (String tag : tags) {
+            SimpleTrie trie = tagSensitiveWordTries.get(tag);
+            if (trie == null) {
+                continue;
+            }
+            // 如果有一个标签不合法,则返回 false 不合法
+            if (!trie.isValid(text)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+}

+ 152 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/util/collection/SimpleTrie.java

@@ -0,0 +1,152 @@
+package com.citu.module.system.util.collection;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.*;
+
+/**
+ * 基于前缀树,实现敏感词的校验
+ * <p>
+ * 相比 Apache Common 提供的 PatriciaTrie 来说,性能可能会更加好一些。
+ *
+ * @author 芋道源码
+ */
+@SuppressWarnings("unchecked")
+public class SimpleTrie {
+
+    /**
+     * 一个敏感词结束后对应的 key
+     */
+    private static final Character CHARACTER_END = '\0';
+
+    /**
+     * 使用敏感词,构建的前缀树
+     */
+    private final Map<Character, Object> children;
+
+    /**
+     * 基于字符串,构建前缀树
+     *
+     * @param strs 字符串数组
+     */
+    public SimpleTrie(Collection<String> strs) {
+        // 排序,优先使用较短的前缀
+        strs = CollUtil.sort(strs, String::compareTo);
+        // 构建树
+        children = new HashMap<>();
+        for (String str : strs) {
+            Map<Character, Object> child = children;
+            // 遍历每个字符
+            for (Character c : str.toCharArray()) {
+                // 如果已经到达结束,就没必要在添加更长的敏感词。
+                // 例如说,有两个敏感词是:吃饭啊、吃饭。输入一句话是 “我要吃饭啊”,则只要匹配到 “吃饭” 这个敏感词即可。
+                if (child.containsKey(CHARACTER_END)) {
+                    break;
+                }
+                if (!child.containsKey(c)) {
+                    child.put(c, new HashMap<>());
+                }
+                child = (Map<Character, Object>) child.get(c);
+            }
+            // 结束
+            child.put(CHARACTER_END, null);
+        }
+    }
+
+    /**
+     * 验证文本是否合法,即不包含敏感词
+     *
+     * @param text 文本
+     * @return 是否 true-合法 false-不合法
+     */
+    public boolean isValid(String text) {
+        // 遍历 text,使用每一个 [i, n) 段的字符串,使用 children 前缀树匹配,是否包含敏感词
+        for (int i = 0; i < text.length(); i++) {
+            Map<Character, Object> child = (Map<Character, Object>) children.get(text.charAt(i));
+            if (child == null) {
+                continue;
+            }
+            boolean ok = recursion(text, i + 1, child);
+            if (!ok) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 验证文本从指定位置开始,是否不包含某个敏感词
+     *
+     * @param text  文本
+     * @param index 开始位置
+     * @param child 节点(当前遍历到的)
+     * @return 是否不包含 true-不包含 false-包含
+     */
+    private boolean recursion(String text, int index, Map<Character, Object> child) {
+        if (child.containsKey(CHARACTER_END)) {
+            return false;
+        }
+        if (index == text.length()) {
+            return true;
+        }
+        child = (Map<Character, Object>) child.get(text.charAt(index));
+        return child == null || !child.containsKey(CHARACTER_END) && recursion(text, ++index, child);
+    }
+
+    /**
+     * 获得文本所包含的不合法的敏感词
+     *
+     * 注意,才当即最短匹配原则。例如说:当敏感词存在 “煞笔”,“煞笔二货 ”时,只会返回 “煞笔”。
+     *
+     * @param text 文本
+     * @return 匹配的敏感词
+     */
+    public List<String> validate(String text) {
+        Set<String> results = new HashSet<>();
+        for (int i = 0; i < text.length(); i++) {
+            Character c = text.charAt(i);
+            Map<Character, Object> child = (Map<Character, Object>) children.get(c);
+            if (child == null) {
+                continue;
+            }
+            StringBuilder result = new StringBuilder().append(c);
+            boolean ok = recursionWithResult(text, i + 1, child, result);
+            if (!ok) {
+                results.add(result.toString());
+            }
+        }
+        return new ArrayList<>(results);
+    }
+
+    /**
+     * 返回文本从 index 开始的敏感词,并使用 StringBuilder 参数进行返回
+     *
+     * 逻辑和 {@link #recursion(String, int, Map)} 是一致,只是多了 result 返回结果
+     *
+     * @param text   文本
+     * @param index  开始未知
+     * @param child  节点(当前遍历到的)
+     * @param result 返回敏感词
+     * @return 是否有敏感词
+     */
+    @SuppressWarnings("unchecked")
+    private static boolean recursionWithResult(String text, int index, Map<Character, Object> child, StringBuilder result) {
+        if (child.containsKey(CHARACTER_END)) {
+            return false;
+        }
+        if (index == text.length()) {
+            return true;
+        }
+        Character c = text.charAt(index);
+        child = (Map<Character, Object>) child.get(c);
+        if (child == null) {
+            return true;
+        }
+        if (child.containsKey(CHARACTER_END)) {
+            result.append(c);
+            return false;
+        }
+        return recursionWithResult(text, ++index, child, result.append(c));
+    }
+
+}

+ 2 - 0
citu-module-system/citu-module-system-biz/src/main/resources/application-local.yaml

@@ -175,6 +175,8 @@ citu:
     refund-notify-url: http://niubi.natapp1.cc/api/pay/refund/notify
     refund-notify-url: http://niubi.natapp1.cc/api/pay/refund/notify
   access-log: # 访问日志的配置项
   access-log: # 访问日志的配置项
     enable: false
     enable: false
+  error-code: # 错误码相关配置项
+    enable: false
   demo: false # 关闭演示模式
   demo: false # 关闭演示模式
 
 
 justauth:
 justauth:

+ 308 - 0
citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/service/errorcode/ErrorCodeServiceTest.java

@@ -0,0 +1,308 @@
+package com.citu.module.system.service.errorcode;
+
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.collection.ArrayUtils;
+import com.citu.framework.test.core.ut.BaseDbUnitTest;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO;
+import com.citu.module.system.api.errorcode.dto.ErrorCodeRespDTO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO;
+import com.citu.module.system.controller.admin.errorcode.vo.ErrorCodeSaveReqVO;
+import com.citu.module.system.dal.dataobject.errorcode.ErrorCodeDO;
+import com.citu.module.system.dal.mysql.errorcode.ErrorCodeMapper;
+import com.citu.module.system.enums.errorcode.ErrorCodeTypeEnum;
+import org.assertj.core.util.Lists;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static cn.hutool.core.util.RandomUtil.randomEle;
+import static com.citu.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static com.citu.framework.common.util.date.LocalDateTimeUtils.buildTime;
+import static com.citu.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static com.citu.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static com.citu.framework.test.core.util.AssertUtils.assertServiceException;
+import static com.citu.framework.test.core.util.RandomUtils.*;
+import static com.citu.module.system.enums.ErrorCodeConstants.ERROR_CODE_DUPLICATE;
+import static com.citu.module.system.enums.ErrorCodeConstants.ERROR_CODE_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+@Import(ErrorCodeServiceImpl.class)
+public class ErrorCodeServiceTest extends BaseDbUnitTest {
+
+    @Resource
+    private ErrorCodeServiceImpl errorCodeService;
+
+    @Resource
+    private ErrorCodeMapper errorCodeMapper;
+
+    @Test
+    public void testCreateErrorCode_success() {
+        // 准备参数
+        ErrorCodeSaveReqVO reqVO = randomPojo(ErrorCodeSaveReqVO.class)
+                .setId(null); // 防止 id 被赋值
+
+        // 调用
+        Long errorCodeId = errorCodeService.createErrorCode(reqVO);
+        // 断言
+        assertNotNull(errorCodeId);
+        // 校验记录的属性是否正确
+        ErrorCodeDO errorCode = errorCodeMapper.selectById(errorCodeId);
+        assertPojoEquals(reqVO, errorCode, "id");
+        assertEquals(ErrorCodeTypeEnum.MANUAL_OPERATION.getType(), errorCode.getType());
+    }
+
+    @Test
+    public void testUpdateErrorCode_success() {
+        // mock 数据
+        ErrorCodeDO dbErrorCode = randomErrorCodeDO();
+        errorCodeMapper.insert(dbErrorCode);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        ErrorCodeSaveReqVO reqVO = randomPojo(ErrorCodeSaveReqVO.class, o -> {
+            o.setId(dbErrorCode.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        errorCodeService.updateErrorCode(reqVO);
+        // 校验是否更新正确
+        ErrorCodeDO errorCode = errorCodeMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, errorCode);
+        assertEquals(ErrorCodeTypeEnum.MANUAL_OPERATION.getType(), errorCode.getType());
+    }
+
+    @Test
+    public void testDeleteErrorCode_success() {
+        // mock 数据
+        ErrorCodeDO dbErrorCode = randomErrorCodeDO();
+        errorCodeMapper.insert(dbErrorCode);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbErrorCode.getId();
+
+        // 调用
+        errorCodeService.deleteErrorCode(id);
+        // 校验数据不存在了
+        assertNull(errorCodeMapper.selectById(id));
+    }
+
+    @Test
+    public void testGetErrorCodePage() {
+        // mock 数据
+        ErrorCodeDO dbErrorCode = initGetErrorCodePage();
+        // 准备参数
+        ErrorCodePageReqVO reqVO = new ErrorCodePageReqVO();
+        reqVO.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType());
+        reqVO.setApplicationName("tu");
+        reqVO.setCode(1);
+        reqVO.setMessage("ma");
+        reqVO.setCreateTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30));
+
+        // 调用
+        PageResult<ErrorCodeDO> pageResult = errorCodeService.getErrorCodePage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbErrorCode, pageResult.getList().get(0));
+    }
+
+    /**
+     * 初始化 getErrorCodePage 方法的测试数据
+     */
+    private ErrorCodeDO initGetErrorCodePage() {
+        ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> { // 等会查询到
+            o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType());
+            o.setApplicationName("tudou");
+            o.setCode(1);
+            o.setMessage("yuanma");
+            o.setCreateTime(buildTime(2020, 11, 11));
+        });
+        errorCodeMapper.insert(dbErrorCode);
+        // 测试 type 不匹配
+        errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType())));
+        // 测试 applicationName 不匹配
+        errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setApplicationName("yuan")));
+        // 测试 code 不匹配
+        errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setCode(2)));
+        // 测试 message 不匹配
+        errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setMessage("nai")));
+        // 测试 createTime 不匹配
+        errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setCreateTime(buildTime(2020, 12, 12))));
+        return dbErrorCode;
+    }
+
+    @Test
+    public void testValidateCodeDuplicate_codeDuplicateForCreate() {
+        // 准备参数
+        Integer code = randomInteger();
+        // mock 数据
+        errorCodeMapper.insert(randomErrorCodeDO(o -> o.setCode(code)));
+
+        // 调用,校验异常
+        assertServiceException(() -> errorCodeService.validateCodeDuplicate(code, null),
+                ERROR_CODE_DUPLICATE);
+    }
+
+    @Test
+    public void testValidateCodeDuplicate_codeDuplicateForUpdate() {
+        // 准备参数
+        Long id = randomLongId();
+        Integer code = randomInteger();
+        // mock 数据
+        errorCodeMapper.insert(randomErrorCodeDO(o -> o.setCode(code)));
+
+        // 调用,校验异常
+        assertServiceException(() -> errorCodeService.validateCodeDuplicate(code, id),
+                ERROR_CODE_DUPLICATE);
+    }
+
+    @Test
+    public void testValidateErrorCodeExists_notExists() {
+        assertServiceException(() -> errorCodeService.validateErrorCodeExists(null),
+                ERROR_CODE_NOT_EXISTS);
+    }
+
+    /**
+     * 情况 1,错误码不存在的情况
+     */
+    @Test
+    public void testAutoGenerateErrorCodes_01() {
+        // 准备参数
+        ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class);
+        // mock 方法
+
+        // 调用
+        errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO));
+        // 断言
+        ErrorCodeDO errorCode = errorCodeMapper.selectOne(null);
+        assertPojoEquals(generateReqDTO, errorCode);
+        assertEquals(ErrorCodeTypeEnum.AUTO_GENERATION.getType(), errorCode.getType());
+    }
+
+    /**
+     * 情况 2.1,错误码存在,但是是 ErrorCodeTypeEnum.MANUAL_OPERATION 类型
+     */
+    @Test
+    public void testAutoGenerateErrorCodes_021() {
+        // mock 数据
+        ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()));
+        errorCodeMapper.insert(dbErrorCode);
+        // 准备参数
+        ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class,
+                o -> o.setCode(dbErrorCode.getCode()));
+        // mock 方法
+
+        // 调用
+        errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO));
+        // 断言,相等,说明不会更新
+        ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId());
+        assertPojoEquals(dbErrorCode, errorCode);
+    }
+
+    /**
+     * 情况 2.2,错误码存在,但是是 applicationName 不匹配
+     */
+    @Test
+    public void testAutoGenerateErrorCodes_022() {
+        // mock 数据
+        ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()));
+        errorCodeMapper.insert(dbErrorCode);
+        // 准备参数
+        ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class,
+                o -> o.setCode(dbErrorCode.getCode()).setApplicationName(randomString()));
+        // mock 方法
+
+        // 调用
+        errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO));
+        // 断言,相等,说明不会更新
+        ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId());
+        assertPojoEquals(dbErrorCode, errorCode);
+    }
+
+    /**
+     * 情况 2.3,错误码存在,但是是 message 相同
+     */
+    @Test
+    public void testAutoGenerateErrorCodes_023() {
+        // mock 数据
+        ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()));
+        errorCodeMapper.insert(dbErrorCode);
+        // 准备参数
+        ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class,
+                o -> o.setCode(dbErrorCode.getCode()).setApplicationName(dbErrorCode.getApplicationName())
+                        .setMessage(dbErrorCode.getMessage()));
+        // mock 方法
+
+        // 调用
+        errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO));
+        // 断言,相等,说明不会更新
+        ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId());
+        assertPojoEquals(dbErrorCode, errorCode);
+    }
+
+    /**
+     * 情况 2.3,错误码存在,但是是 message 不同,则进行更新
+     */
+    @Test
+    public void testAutoGenerateErrorCodes_024() {
+        // mock 数据
+        ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()));
+        errorCodeMapper.insert(dbErrorCode);
+        // 准备参数
+        ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class,
+                o -> o.setCode(dbErrorCode.getCode()).setApplicationName(dbErrorCode.getApplicationName()));
+        // mock 方法
+
+        // 调用
+        errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO));
+        // 断言,匹配
+        ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId());
+        assertPojoEquals(generateReqDTO, errorCode);
+    }
+
+    @Test
+    public void testGetErrorCode() {
+        // 准备参数
+        ErrorCodeDO errorCodeDO = randomErrorCodeDO();
+        errorCodeMapper.insert(errorCodeDO);
+        // mock 方法
+        Long id = errorCodeDO.getId();
+
+        // 调用
+        ErrorCodeDO dbErrorCode = errorCodeService.getErrorCode(id);
+        // 断言
+        assertPojoEquals(errorCodeDO, dbErrorCode);
+    }
+
+    @Test
+    public void testGetErrorCodeList() {
+        // 准备参数
+        ErrorCodeDO errorCodeDO01 = randomErrorCodeDO(
+                o -> o.setApplicationName("yunai_server").setUpdateTime(buildTime(2022, 1, 10)));
+        errorCodeMapper.insert(errorCodeDO01);
+        ErrorCodeDO errorCodeDO02 = randomErrorCodeDO(
+                o -> o.setApplicationName("yunai_server").setUpdateTime(buildTime(2022, 1, 12)));
+        errorCodeMapper.insert(errorCodeDO02);
+        // mock 方法
+        String applicationName = "yunai_server";
+        LocalDateTime minUpdateTime = buildTime(2022, 1, 11);
+
+        // 调用
+        List<ErrorCodeRespDTO> errorCodeList = errorCodeService.getErrorCodeList(applicationName, minUpdateTime);
+        // 断言
+        assertEquals(1, errorCodeList.size());
+        assertPojoEquals(errorCodeDO02, errorCodeList.get(0));
+    }
+
+    // ========== 随机对象 ==========
+
+    @SafeVarargs
+    private static ErrorCodeDO randomErrorCodeDO(Consumer<ErrorCodeDO>... consumers) {
+        Consumer<ErrorCodeDO> consumer = (o) -> {
+            o.setType(randomEle(ErrorCodeTypeEnum.values()).getType()); // 保证 key 的范围
+        };
+        return randomPojo(ErrorCodeDO.class, ArrayUtils.append(consumer, consumers));
+    }
+
+}

+ 304 - 0
citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java

@@ -0,0 +1,304 @@
+package com.citu.module.system.service.sensitiveword;
+
+import com.citu.framework.common.enums.CommonStatusEnum;
+import com.citu.framework.common.pojo.PageResult;
+import com.citu.framework.common.util.collection.SetUtils;
+import com.citu.framework.common.util.date.LocalDateTimeUtils;
+import com.citu.framework.test.core.ut.BaseDbUnitTest;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
+import com.citu.module.system.controller.admin.sensitiveword.vo.SensitiveWordSaveVO;
+import com.citu.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
+import com.citu.module.system.dal.mysql.sensitiveword.SensitiveWordMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+
+import javax.annotation.Resource;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.citu.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static com.citu.framework.common.util.date.LocalDateTimeUtils.buildTime;
+import static com.citu.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static com.citu.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static com.citu.framework.test.core.util.AssertUtils.assertServiceException;
+import static com.citu.framework.test.core.util.RandomUtils.randomLongId;
+import static com.citu.framework.test.core.util.RandomUtils.randomPojo;
+import static com.citu.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link SensitiveWordServiceImpl} 的单元测试类
+ *
+ * @author 永不言败
+ */
+@Import(SensitiveWordServiceImpl.class)
+public class SensitiveWordServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private SensitiveWordServiceImpl sensitiveWordService;
+
+    @Resource
+    private SensitiveWordMapper sensitiveWordMapper;
+
+    @BeforeEach
+    public void setUp() {
+        SensitiveWordServiceImpl.ENABLED = true;
+    }
+
+    @Test
+    public void testInitLocalCache() {
+        SensitiveWordDO wordDO1 = randomPojo(SensitiveWordDO.class, o -> o.setName("傻瓜")
+                .setTags(singletonList("论坛")).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        sensitiveWordMapper.insert(wordDO1);
+        SensitiveWordDO wordDO2 = randomPojo(SensitiveWordDO.class, o -> o.setName("笨蛋")
+                .setTags(singletonList("蔬菜")).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        sensitiveWordMapper.insert(wordDO2);
+        SensitiveWordDO wordDO3 = randomPojo(SensitiveWordDO.class, o -> o.setName("白")
+                .setTags(singletonList("测试")).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        sensitiveWordMapper.insert(wordDO3);
+        SensitiveWordDO wordDO4 = randomPojo(SensitiveWordDO.class, o -> o.setName("白痴")
+                .setTags(singletonList("测试")).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        sensitiveWordMapper.insert(wordDO4);
+
+        // 调用
+        sensitiveWordService.initLocalCache();
+        // 断言 sensitiveWordTagsCache 缓存
+        assertEquals(SetUtils.asSet("论坛", "蔬菜", "测试"), sensitiveWordService.getSensitiveWordTagSet());
+        // 断言 sensitiveWordCache
+        assertEquals(4, sensitiveWordService.getSensitiveWordCache().size());
+        assertPojoEquals(wordDO1, sensitiveWordService.getSensitiveWordCache().get(0));
+        assertPojoEquals(wordDO2, sensitiveWordService.getSensitiveWordCache().get(1));
+        assertPojoEquals(wordDO3, sensitiveWordService.getSensitiveWordCache().get(2));
+        // 断言 tagSensitiveWordTries 缓存
+        assertNotNull(sensitiveWordService.getDefaultSensitiveWordTrie());
+        assertEquals(3, sensitiveWordService.getTagSensitiveWordTries().size());
+        assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("论坛"));
+        assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("蔬菜"));
+        assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("测试"));
+    }
+
+    @Test
+    public void testRefreshLocalCache() {
+        // mock 数据
+        SensitiveWordDO wordDO1 = randomPojo(SensitiveWordDO.class, o -> o.setName("傻瓜")
+                .setTags(singletonList("论坛")).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        wordDO1.setUpdateTime(LocalDateTime.now());
+        sensitiveWordMapper.insert(wordDO1);
+        sensitiveWordService.initLocalCache();
+        // mock 数据 ②
+        SensitiveWordDO wordDO2 = randomPojo(SensitiveWordDO.class, o -> o.setName("笨蛋")
+                .setTags(singletonList("蔬菜")).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        wordDO2.setUpdateTime(LocalDateTimeUtils.addTime(Duration.ofMinutes(1))); // 避免时间相同
+        sensitiveWordMapper.insert(wordDO2);
+
+        // 调用
+        sensitiveWordService.refreshLocalCache();
+        // 断言 sensitiveWordTagsCache 缓存
+        assertEquals(SetUtils.asSet("论坛", "蔬菜"), sensitiveWordService.getSensitiveWordTagSet());
+        // 断言 sensitiveWordCache
+        assertEquals(2, sensitiveWordService.getSensitiveWordCache().size());
+        assertPojoEquals(wordDO1, sensitiveWordService.getSensitiveWordCache().get(0));
+        assertPojoEquals(wordDO2, sensitiveWordService.getSensitiveWordCache().get(1));
+        // 断言 tagSensitiveWordTries 缓存
+        assertNotNull(sensitiveWordService.getDefaultSensitiveWordTrie());
+        assertEquals(2, sensitiveWordService.getTagSensitiveWordTries().size());
+        assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("论坛"));
+        assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("蔬菜"));
+    }
+
+    @Test
+    public void testCreateSensitiveWord_success() {
+        // 准备参数
+        SensitiveWordSaveVO reqVO = randomPojo(SensitiveWordSaveVO.class)
+                .setId(null); // 防止 id 被赋值
+
+        // 调用
+        Long sensitiveWordId = sensitiveWordService.createSensitiveWord(reqVO);
+        // 断言
+        assertNotNull(sensitiveWordId);
+        // 校验记录的属性是否正确
+        SensitiveWordDO sensitiveWord = sensitiveWordMapper.selectById(sensitiveWordId);
+        assertPojoEquals(reqVO, sensitiveWord, "id");
+    }
+
+    @Test
+    public void testUpdateSensitiveWord_success() {
+        // mock 数据
+        SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class);
+        sensitiveWordMapper.insert(dbSensitiveWord);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        SensitiveWordSaveVO reqVO = randomPojo(SensitiveWordSaveVO.class, o -> {
+            o.setId(dbSensitiveWord.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        sensitiveWordService.updateSensitiveWord(reqVO);
+        // 校验是否更新正确
+        SensitiveWordDO sensitiveWord = sensitiveWordMapper.selectById(reqVO.getId()); // 获取最新的
+        assertPojoEquals(reqVO, sensitiveWord);
+    }
+
+    @Test
+    public void testUpdateSensitiveWord_notExists() {
+        // 准备参数
+        SensitiveWordSaveVO reqVO = randomPojo(SensitiveWordSaveVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> sensitiveWordService.updateSensitiveWord(reqVO), SENSITIVE_WORD_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteSensitiveWord_success() {
+        // mock 数据
+        SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class);
+        sensitiveWordMapper.insert(dbSensitiveWord);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbSensitiveWord.getId();
+
+        // 调用
+        sensitiveWordService.deleteSensitiveWord(id);
+        // 校验数据不存在了
+        assertNull(sensitiveWordMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteSensitiveWord_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> sensitiveWordService.deleteSensitiveWord(id), SENSITIVE_WORD_NOT_EXISTS);
+    }
+
+    @Test
+    public void testGetSensitiveWord() {
+        // mock 数据
+        SensitiveWordDO sensitiveWord = randomPojo(SensitiveWordDO.class);
+        sensitiveWordMapper.insert(sensitiveWord);
+        // 准备参数
+        Long id = sensitiveWord.getId();
+
+        // 调用
+        SensitiveWordDO dbSensitiveWord = sensitiveWordService.getSensitiveWord(id);
+        // 断言
+        assertPojoEquals(sensitiveWord, dbSensitiveWord);
+    }
+
+    @Test
+    public void testGetSensitiveWordList() {
+        // mock 数据
+        SensitiveWordDO sensitiveWord01 = randomPojo(SensitiveWordDO.class);
+        sensitiveWordMapper.insert(sensitiveWord01);
+        SensitiveWordDO sensitiveWord02 = randomPojo(SensitiveWordDO.class);
+        sensitiveWordMapper.insert(sensitiveWord02);
+
+        // 调用
+        List<SensitiveWordDO> list = sensitiveWordService.getSensitiveWordList();
+        // 断言
+        assertEquals(2, list.size());
+        assertEquals(sensitiveWord01, list.get(0));
+        assertEquals(sensitiveWord02, list.get(1));
+    }
+
+    @Test
+    public void testGetSensitiveWordPage() {
+        // mock 数据
+        SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class, o -> { // 等会查询到
+            o.setName("笨蛋");
+            o.setTags(Arrays.asList("论坛", "蔬菜"));
+            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
+            o.setCreateTime(buildTime(2022, 2, 8));
+        });
+        sensitiveWordMapper.insert(dbSensitiveWord);
+        // 测试 name 不匹配
+        sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setName("傻瓜")));
+        // 测试 tags 不匹配
+        sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setTags(Arrays.asList("短信", "日用品"))));
+        // 测试 createTime 不匹配
+        sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setCreateTime(buildTime(2022, 2, 16))));
+        // 准备参数
+        SensitiveWordPageReqVO reqVO = new SensitiveWordPageReqVO();
+        reqVO.setName("笨");
+        reqVO.setTag("论坛");
+        reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 12));
+
+        // 调用
+        PageResult<SensitiveWordDO> pageResult = sensitiveWordService.getSensitiveWordPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbSensitiveWord, pageResult.getList().get(0));
+    }
+
+    @Test
+    public void testValidateText_noTag() {
+        testInitLocalCache();
+        // 准备参数
+        String text = "你是傻瓜,你是笨蛋";
+        // 调用
+        List<String> result = sensitiveWordService.validateText(text, null);
+        // 断言
+        assertEquals(Arrays.asList("傻瓜", "笨蛋"), result);
+
+        // 准备参数
+        String text2 = "你是傻瓜,你是笨蛋,你是白";
+        // 调用
+        List<String> result2 = sensitiveWordService.validateText(text2, null);
+        // 断言
+        assertEquals(Arrays.asList("傻瓜", "笨蛋","白"), result2);
+    }
+
+    @Test
+    public void testValidateText_hasTag() {
+        testInitLocalCache();
+        // 准备参数
+        String text = "你是傻瓜,你是笨蛋";
+        // 调用
+        List<String> result = sensitiveWordService.validateText(text, singletonList("论坛"));
+        // 断言
+        assertEquals(singletonList("傻瓜"), result);
+
+
+        // 准备参数
+        String text2 = "你是白";
+        // 调用
+        List<String> result2 = sensitiveWordService.validateText(text2, singletonList("测试"));
+        // 断言
+        assertEquals(singletonList("白"), result2);
+    }
+
+    @Test
+    public void testIsTestValid_noTag() {
+        testInitLocalCache();
+        // 准备参数
+        String text = "你是傻瓜,你是笨蛋";
+        // 调用,断言
+        assertFalse(sensitiveWordService.isTextValid(text, null));
+
+        // 准备参数
+        String text2 = "你是白";
+        // 调用,断言
+        assertFalse(sensitiveWordService.isTextValid(text2, null));
+    }
+
+    @Test
+    public void testIsTestValid_hasTag() {
+        testInitLocalCache();
+        // 准备参数
+        String text = "你是傻瓜,你是笨蛋";
+        // 调用,断言
+        assertFalse(sensitiveWordService.isTextValid(text, singletonList("论坛")));
+
+        // 准备参数
+        String text2 = "你是白";
+        // 调用,断言
+        assertFalse(sensitiveWordService.isTextValid(text2, singletonList("测试")));
+    }
+
+}

+ 2 - 0
citu-module-system/citu-module-system-biz/src/test/resources/sql/clean.sql

@@ -16,11 +16,13 @@ DELETE FROM "system_sms_channel";
 DELETE FROM "system_sms_template";
 DELETE FROM "system_sms_template";
 DELETE FROM "system_sms_log";
 DELETE FROM "system_sms_log";
 DELETE FROM "system_sms_code";
 DELETE FROM "system_sms_code";
+DELETE FROM "system_error_code";
 DELETE FROM "system_social_client";
 DELETE FROM "system_social_client";
 DELETE FROM "system_social_user";
 DELETE FROM "system_social_user";
 DELETE FROM "system_social_user_bind";
 DELETE FROM "system_social_user_bind";
 DELETE FROM "system_tenant";
 DELETE FROM "system_tenant";
 DELETE FROM "system_tenant_package";
 DELETE FROM "system_tenant_package";
+DELETE FROM "system_sensitive_word";
 DELETE FROM "system_oauth2_client";
 DELETE FROM "system_oauth2_client";
 DELETE FROM "system_oauth2_approve";
 DELETE FROM "system_oauth2_approve";
 DELETE FROM "system_oauth2_access_token";
 DELETE FROM "system_oauth2_access_token";

+ 549 - 484
citu-module-system/citu-module-system-biz/src/test/resources/sql/create_tables.sql

@@ -1,141 +1,150 @@
-CREATE TABLE IF NOT EXISTS "system_dept" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar(30) NOT NULL DEFAULT '',
-    "parent_id" bigint NOT NULL DEFAULT '0',
-    "sort" int NOT NULL DEFAULT '0',
-    "leader_user_id" bigint DEFAULT NULL,
-    "phone" varchar(11) DEFAULT NULL,
-    "email" varchar(50) DEFAULT NULL,
-    "status" tinyint NOT NULL,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
-    "tenant_id" bigint not null default  '0',
+CREATE TABLE IF NOT EXISTS "system_dept"
+(
+    "id"             bigint      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"           varchar(30) NOT NULL DEFAULT '',
+    "parent_id"      bigint      NOT NULL DEFAULT '0',
+    "sort"           int         NOT NULL DEFAULT '0',
+    "leader_user_id" bigint               DEFAULT NULL,
+    "phone"          varchar(11)          DEFAULT NULL,
+    "email"          varchar(50)          DEFAULT NULL,
+    "status"         tinyint     NOT NULL,
+    "creator"        varchar(64)          DEFAULT '',
+    "create_time"    timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"        varchar(64)          DEFAULT '',
+    "update_time"    timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"        bit         NOT NULL DEFAULT FALSE,
+    "tenant_id"      bigint      not null default '0',
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '部门表';
 ) COMMENT '部门表';
 
 
-CREATE TABLE IF NOT EXISTS "system_dict_data" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "sort" int NOT NULL DEFAULT '0',
-    "label" varchar(100) NOT NULL DEFAULT '',
-    "value" varchar(100) NOT NULL DEFAULT '',
-    "dict_type" varchar(100) NOT NULL DEFAULT '',
-    "status" tinyint NOT NULL DEFAULT '0',
-    "color_type" varchar(100) NOT NULL DEFAULT '',
-    "css_class" varchar(100) NOT NULL DEFAULT '',
-    "remark" varchar(500) DEFAULT NULL,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+CREATE TABLE IF NOT EXISTS "system_dict_data"
+(
+    "id"          bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "sort"        int          NOT NULL DEFAULT '0',
+    "label"       varchar(100) NOT NULL DEFAULT '',
+    "value"       varchar(100) NOT NULL DEFAULT '',
+    "dict_type"   varchar(100) NOT NULL DEFAULT '',
+    "status"      tinyint      NOT NULL DEFAULT '0',
+    "color_type"  varchar(100) NOT NULL DEFAULT '',
+    "css_class"   varchar(100) NOT NULL DEFAULT '',
+    "remark"      varchar(500)          DEFAULT NULL,
+    "creator"     varchar(64)           DEFAULT '',
+    "create_time" timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"     varchar(64)           DEFAULT '',
+    "update_time" timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"     bit          NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '字典数据表';
 ) COMMENT '字典数据表';
 
 
-CREATE TABLE IF NOT EXISTS "system_role" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar(30) NOT NULL,
-    "code" varchar(100) NOT NULL,
-    "sort" int NOT NULL,
-    "data_scope" tinyint NOT NULL DEFAULT '1',
+CREATE TABLE IF NOT EXISTS "system_role"
+(
+    "id"                  bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"                varchar(30)  NOT NULL,
+    "code"                varchar(100) NOT NULL,
+    "sort"                int          NOT NULL,
+    "data_scope"          tinyint      NOT NULL DEFAULT '1',
     "data_scope_dept_ids" varchar(500) NOT NULL DEFAULT '',
     "data_scope_dept_ids" varchar(500) NOT NULL DEFAULT '',
-    "status" tinyint NOT NULL,
-    "type" tinyint NOT NULL,
-    "remark" varchar(500) DEFAULT NULL,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
-    "tenant_id" bigint not null default  '0',
+    "status"              tinyint      NOT NULL,
+    "type"                tinyint      NOT NULL,
+    "remark"              varchar(500)          DEFAULT NULL,
+    "creator"             varchar(64)           DEFAULT '',
+    "create_time"         timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"             varchar(64)           DEFAULT '',
+    "update_time"         timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"             bit          NOT NULL DEFAULT FALSE,
+    "tenant_id"           bigint       not null default '0',
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '角色信息表';
 ) COMMENT '角色信息表';
 
 
-CREATE TABLE IF NOT EXISTS "system_role_menu" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "role_id" bigint NOT NULL,
-    "menu_id" bigint NOT NULL,
-    "creator" varchar(64) DEFAULT '',
+CREATE TABLE IF NOT EXISTS "system_role_menu"
+(
+    "id"          bigint    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "role_id"     bigint    NOT NULL,
+    "menu_id"     bigint    NOT NULL,
+    "creator"     varchar(64)        DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
+    "updater"     varchar(64)        DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
-    "tenant_id" bigint not null default  '0',
+    "deleted"     bit       NOT NULL DEFAULT FALSE,
+    "tenant_id"   bigint    not null default '0',
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '角色和菜单关联表';
 ) COMMENT '角色和菜单关联表';
 
 
-CREATE TABLE IF NOT EXISTS "system_menu" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar(50) NOT NULL,
-    "permission" varchar(100) NOT NULL DEFAULT '',
-    "type" tinyint NOT NULL,
-    "sort" int NOT NULL DEFAULT '0',
-    "parent_id" bigint NOT NULL DEFAULT '0',
-    "path" varchar(200) DEFAULT '',
-    "icon" varchar(100) DEFAULT '#',
-    "component" varchar(255) DEFAULT NULL,
-    "component_name" varchar(255) DEFAULT NULL,
-    "status" tinyint NOT NULL DEFAULT '0',
-    "visible" bit NOT NULL DEFAULT TRUE,
-    "keep_alive" bit NOT NULL DEFAULT TRUE,
-    "always_show" bit NOT NULL DEFAULT TRUE,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+CREATE TABLE IF NOT EXISTS "system_menu"
+(
+    "id"             bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"           varchar(50)  NOT NULL,
+    "permission"     varchar(100) NOT NULL DEFAULT '',
+    "type"           tinyint      NOT NULL,
+    "sort"           int          NOT NULL DEFAULT '0',
+    "parent_id"      bigint       NOT NULL DEFAULT '0',
+    "path"           varchar(200)          DEFAULT '',
+    "icon"           varchar(100)          DEFAULT '#',
+    "component"      varchar(255)          DEFAULT NULL,
+    "component_name" varchar(255)          DEFAULT NULL,
+    "status"         tinyint      NOT NULL DEFAULT '0',
+    "visible"        bit          NOT NULL DEFAULT TRUE,
+    "keep_alive"     bit          NOT NULL DEFAULT TRUE,
+    "always_show"    bit          NOT NULL DEFAULT TRUE,
+    "creator"        varchar(64)           DEFAULT '',
+    "create_time"    timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"        varchar(64)           DEFAULT '',
+    "update_time"    timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"        bit          NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '菜单权限表';
 ) COMMENT '菜单权限表';
 
 
-CREATE TABLE IF NOT EXISTS "system_user_role" (
-     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-     "user_id" bigint NOT NULL,
-     "role_id" bigint NOT NULL,
-     "creator" varchar(64) DEFAULT '',
-     "create_time" timestamp DEFAULT NULL,
-     "updater" varchar(64) DEFAULT '',
-     "update_time" timestamp DEFAULT NULL,
-     "deleted" bit DEFAULT FALSE,
-    "tenant_id" bigint not null default  '0',
+CREATE TABLE IF NOT EXISTS "system_user_role"
+(
+    "id"          bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"     bigint NOT NULL,
+    "role_id"     bigint NOT NULL,
+    "creator"     varchar(64)     DEFAULT '',
+    "create_time" timestamp       DEFAULT NULL,
+    "updater"     varchar(64)     DEFAULT '',
+    "update_time" timestamp       DEFAULT NULL,
+    "deleted"     bit             DEFAULT FALSE,
+    "tenant_id"   bigint not null default '0',
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '用户和角色关联表';
 ) COMMENT '用户和角色关联表';
 
 
-CREATE TABLE IF NOT EXISTS "system_dict_type" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar(100) NOT NULL DEFAULT '',
-    "type" varchar(100) NOT NULL DEFAULT '',
-    "status" tinyint NOT NULL DEFAULT '0',
-    "remark" varchar(500) DEFAULT NULL,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
-    "deleted_time" timestamp NOT NULL,
+CREATE TABLE IF NOT EXISTS "system_dict_type"
+(
+    "id"           bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"         varchar(100) NOT NULL DEFAULT '',
+    "type"         varchar(100) NOT NULL DEFAULT '',
+    "status"       tinyint      NOT NULL DEFAULT '0',
+    "remark"       varchar(500)          DEFAULT NULL,
+    "creator"      varchar(64)           DEFAULT '',
+    "create_time"  timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"      varchar(64)           DEFAULT '',
+    "update_time"  timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"      bit          NOT NULL DEFAULT FALSE,
+    "deleted_time" timestamp    NOT NULL,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '字典类型表';
 ) COMMENT '字典类型表';
 
 
-CREATE TABLE IF NOT EXISTS `system_user_session` (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    `token` varchar(32) NOT NULL,
-    `user_id` bigint DEFAULT NULL,
-    "user_type" tinyint NOT NULL,
-    `username` varchar(50) NOT NULL DEFAULT '',
-    `user_ip` varchar(50) DEFAULT NULL,
-    `user_agent` varchar(512) DEFAULT NULL,
-    `session_timeout` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    `updater` varchar(64) DEFAULT '' ,
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
-    "tenant_id" bigint not null default  '0',
+CREATE TABLE IF NOT EXISTS `system_user_session`
+(
+    "id"              bigint      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    `token`           varchar(32) NOT NULL,
+    `user_id`         bigint               DEFAULT NULL,
+    "user_type"       tinyint     NOT NULL,
+    `username`        varchar(50) NOT NULL DEFAULT '',
+    `user_ip`         varchar(50)          DEFAULT NULL,
+    `user_agent`      varchar(512)         DEFAULT NULL,
+    `session_timeout` timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "creator"         varchar(64)          DEFAULT '',
+    "create_time"     timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `updater`         varchar(64)          DEFAULT '',
+    "update_time"     timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"         bit         NOT NULL DEFAULT FALSE,
+    "tenant_id"       bigint      not null default '0',
     PRIMARY KEY (`id`)
     PRIMARY KEY (`id`)
 ) COMMENT '用户在线 Session';
 ) COMMENT '用户在线 Session';
 
 
-CREATE TABLE IF NOT EXISTS "system_post" (
+CREATE TABLE IF NOT EXISTS "system_post"
+(
     "id"          bigint      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "id"          bigint      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "code"        varchar(64) NOT NULL,
     "code"        varchar(64) NOT NULL,
     "name"        varchar(50) NOT NULL,
     "name"        varchar(50) NOT NULL,
@@ -147,11 +156,12 @@ CREATE TABLE IF NOT EXISTS "system_post" (
     "updater"     varchar(64)          DEFAULT '',
     "updater"     varchar(64)          DEFAULT '',
     "update_time" timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "update_time" timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted"     bit         NOT NULL DEFAULT FALSE,
     "deleted"     bit         NOT NULL DEFAULT FALSE,
-    "tenant_id" bigint not null default  '0',
+    "tenant_id"   bigint      not null default '0',
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '岗位信息表';
 ) COMMENT '岗位信息表';
 
 
-CREATE TABLE IF NOT EXISTS `system_user_post`(
+CREATE TABLE IF NOT EXISTS `system_user_post`
+(
     "id"          bigint    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "id"          bigint    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "user_id"     bigint             DEFAULT NULL,
     "user_id"     bigint             DEFAULT NULL,
     "post_id"     bigint             DEFAULT NULL,
     "post_id"     bigint             DEFAULT NULL,
@@ -164,451 +174,506 @@ CREATE TABLE IF NOT EXISTS `system_user_post`(
     PRIMARY KEY (`id`)
     PRIMARY KEY (`id`)
 ) COMMENT ='用户岗位表';
 ) COMMENT ='用户岗位表';
 
 
-CREATE TABLE IF NOT EXISTS "system_notice" (
-	"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-	"title" varchar(50) NOT NULL COMMENT '公告标题',
-	"content" text NOT NULL COMMENT '公告内容',
-	"type" tinyint NOT NULL COMMENT '公告类型(1通知 2公告)',
-	"status" tinyint NOT NULL DEFAULT '0' COMMENT '公告状态(0正常 1关闭)',
-	"creator" varchar(64) DEFAULT '' COMMENT '创建者',
-	"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-	"updater" varchar(64) DEFAULT '' COMMENT '更新者',
-	"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-	"deleted" bit NOT NULL DEFAULT 0 COMMENT '是否删除',
-    "tenant_id" bigint not null default  '0',
-    PRIMARY KEY("id")
+CREATE TABLE IF NOT EXISTS "system_notice"
+(
+    "id"          bigint      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "title"       varchar(50) NOT NULL COMMENT '公告标题',
+    "content"     text        NOT NULL COMMENT '公告内容',
+    "type"        tinyint     NOT NULL COMMENT '公告类型(1通知 2公告)',
+    "status"      tinyint     NOT NULL DEFAULT '0' COMMENT '公告状态(0正常 1关闭)',
+    "creator"     varchar(64)          DEFAULT '' COMMENT '创建者',
+    "create_time" datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    "updater"     varchar(64)          DEFAULT '' COMMENT '更新者',
+    "update_time" datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    "deleted"     bit         NOT NULL DEFAULT 0 COMMENT '是否删除',
+    "tenant_id"   bigint      not null default '0',
+    PRIMARY KEY ("id")
 ) COMMENT '通知公告表';
 ) COMMENT '通知公告表';
 
 
-CREATE TABLE IF NOT EXISTS `system_login_log` (
+CREATE TABLE IF NOT EXISTS `system_login_log`
+(
     `id`          bigint(20)   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     `id`          bigint(20)   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     `log_type`    bigint(4)    NOT NULL,
     `log_type`    bigint(4)    NOT NULL,
-    "user_id" bigint not null default '0',
-    "user_type" tinyint NOT NULL,
+    "user_id"     bigint       not null default '0',
+    "user_type"   tinyint      NOT NULL,
     `trace_id`    varchar(64)  NOT NULL DEFAULT '',
     `trace_id`    varchar(64)  NOT NULL DEFAULT '',
     `username`    varchar(50)  NOT NULL DEFAULT '',
     `username`    varchar(50)  NOT NULL DEFAULT '',
     `result`      tinyint(4)   NOT NULL,
     `result`      tinyint(4)   NOT NULL,
     `user_ip`     varchar(50)  NOT NULL,
     `user_ip`     varchar(50)  NOT NULL,
     `user_agent`  varchar(512) NOT NULL,
     `user_agent`  varchar(512) NOT NULL,
-    `creator`   varchar(64)           DEFAULT '',
+    `creator`     varchar(64)           DEFAULT '',
     `create_time` datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
     `create_time` datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    `updater`   varchar(64)           DEFAULT '',
+    `updater`     varchar(64)           DEFAULT '',
     `update_time` datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     `update_time` datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     `deleted`     bit(1)       NOT NULL DEFAULT '0',
     `deleted`     bit(1)       NOT NULL DEFAULT '0',
     PRIMARY KEY (`id`)
     PRIMARY KEY (`id`)
 ) COMMENT ='系统访问记录';
 ) COMMENT ='系统访问记录';
 
 
-CREATE TABLE IF NOT EXISTS `system_operate_log` (
-    `id`               bigint(20)    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    `trace_id`         varchar(64)   NOT NULL DEFAULT '',
-    `user_id`          bigint(20)    NOT NULL,
-    "user_type" tinyint not null default '0',
+CREATE TABLE IF NOT EXISTS `system_operate_log`
+(
+    `id`             bigint(20)    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    `trace_id`       varchar(64)   NOT NULL DEFAULT '',
+    `user_id`        bigint(20)    NOT NULL,
+    "user_type"      tinyint       not null default '0',
     `type`           varchar(50)   NOT NULL,
     `type`           varchar(50)   NOT NULL,
-    `sub_type`             varchar(50)   NOT NULL,
-    `biz_id`          bigint(20)    NOT NULL,
-    `action`          varchar(2000) NOT NULL DEFAULT '',
-    `extra`             varchar(512)  NOT NULL DEFAULT '',
-    `request_method`   varchar(16)            DEFAULT '',
-    `request_url`      varchar(255)           DEFAULT '',
-    `user_ip`          varchar(50)            DEFAULT NULL,
-    `user_agent`       varchar(200)           DEFAULT NULL,
+    `sub_type`       varchar(50)   NOT NULL,
+    `biz_id`         bigint(20)    NOT NULL,
+    `action`         varchar(2000) NOT NULL DEFAULT '',
+    `extra`          varchar(512)  NOT NULL DEFAULT '',
+    `request_method` varchar(16)            DEFAULT '',
+    `request_url`    varchar(255)           DEFAULT '',
+    `user_ip`        varchar(50)            DEFAULT NULL,
+    `user_agent`     varchar(200)           DEFAULT NULL,
     `creator`        varchar(64)            DEFAULT '',
     `creator`        varchar(64)            DEFAULT '',
-    `create_time`      datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `create_time`    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP,
     `updater`        varchar(64)            DEFAULT '',
     `updater`        varchar(64)            DEFAULT '',
-    `update_time`      datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    `deleted`          bit(1)        NOT NULL DEFAULT '0',
-    "tenant_id"         bigint not null default  '0',
+    `update_time`    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    `deleted`        bit(1)        NOT NULL DEFAULT '0',
+    "tenant_id"      bigint        not null default '0',
     PRIMARY KEY (`id`)
     PRIMARY KEY (`id`)
 ) COMMENT ='操作日志记录';
 ) COMMENT ='操作日志记录';
 
 
-CREATE TABLE IF NOT EXISTS "system_users" (
-    "id" bigint not null GENERATED BY DEFAULT AS IDENTITY,
-    "username" varchar(30) not null,
-    "password" varchar(100) not null default '',
-    "nickname" varchar(30) not null,
-    "remark" varchar(500) default null,
-    "dept_id" bigint default null,
-    "post_ids" varchar(255) default null,
-    "email" varchar(50) default '',
-    "mobile" varchar(11) default '',
-    "sex" tinyint default '0',
-    "avatar" varchar(100) default '',
-    "status" tinyint not null default '0',
-    "login_ip" varchar(50) default '',
-    "login_date" timestamp default null,
-    "creator" varchar(64) default '',
-    "create_time" timestamp not null default current_timestamp,
-    "updater" varchar(64) default '',
-    "update_time" timestamp not null default current_timestamp,
-    "deleted" bit not null default false,
-    "tenant_id" bigint not null default  '0',
+CREATE TABLE IF NOT EXISTS "system_users"
+(
+    "id"          bigint       not null GENERATED BY DEFAULT AS IDENTITY,
+    "username"    varchar(30)  not null,
+    "password"    varchar(100) not null default '',
+    "nickname"    varchar(30)  not null,
+    "remark"      varchar(500)          default null,
+    "dept_id"     bigint                default null,
+    "post_ids"    varchar(255)          default null,
+    "email"       varchar(50)           default '',
+    "mobile"      varchar(11)           default '',
+    "sex"         tinyint               default '0',
+    "avatar"      varchar(100)          default '',
+    "status"      tinyint      not null default '0',
+    "login_ip"    varchar(50)           default '',
+    "login_date"  timestamp             default null,
+    "creator"     varchar(64)           default '',
+    "create_time" timestamp    not null default current_timestamp,
+    "updater"     varchar(64)           default '',
+    "update_time" timestamp    not null default current_timestamp,
+    "deleted"     bit          not null default false,
+    "tenant_id"   bigint       not null default '0',
     primary key ("id")
     primary key ("id")
 ) comment '用户信息表';
 ) comment '用户信息表';
 
 
-CREATE TABLE IF NOT EXISTS "system_sms_channel" (
-   "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-   "signature" varchar(10) NOT NULL,
-   "code" varchar(63) NOT NULL,
-   "status" tinyint NOT NULL,
-   "remark" varchar(255) DEFAULT NULL,
-   "api_key" varchar(63) NOT NULL,
-   "api_secret" varchar(63) DEFAULT NULL,
-   "callback_url" varchar(255) DEFAULT NULL,
-   "creator" varchar(64) DEFAULT '',
-   "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "updater" varchar(64) DEFAULT '',
-   "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "deleted" bit NOT NULL DEFAULT FALSE,
-   PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_sms_channel"
+(
+    "id"           bigint      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "signature"    varchar(10) NOT NULL,
+    "code"         varchar(63) NOT NULL,
+    "status"       tinyint     NOT NULL,
+    "remark"       varchar(255)         DEFAULT NULL,
+    "api_key"      varchar(63) NOT NULL,
+    "api_secret"   varchar(63)          DEFAULT NULL,
+    "callback_url" varchar(255)         DEFAULT NULL,
+    "creator"      varchar(64)          DEFAULT '',
+    "create_time"  timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"      varchar(64)          DEFAULT '',
+    "update_time"  timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"      bit         NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT '短信渠道';
 ) COMMENT '短信渠道';
 
 
-CREATE TABLE IF NOT EXISTS "system_sms_template" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "type" tinyint NOT NULL,
-    "status" tinyint NOT NULL,
-    "code" varchar(63) NOT NULL,
-    "name" varchar(63) NOT NULL,
-    "content" varchar(255) NOT NULL,
-    "params" varchar(255) NOT NULL,
-    "remark" varchar(255) DEFAULT NULL,
-    "api_template_id" varchar(63) NOT NULL,
-    "channel_id" bigint NOT NULL,
-    "channel_code" varchar(63) NOT NULL,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+CREATE TABLE IF NOT EXISTS "system_sms_template"
+(
+    "id"              bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "type"            tinyint      NOT NULL,
+    "status"          tinyint      NOT NULL,
+    "code"            varchar(63)  NOT NULL,
+    "name"            varchar(63)  NOT NULL,
+    "content"         varchar(255) NOT NULL,
+    "params"          varchar(255) NOT NULL,
+    "remark"          varchar(255)          DEFAULT NULL,
+    "api_template_id" varchar(63)  NOT NULL,
+    "channel_id"      bigint       NOT NULL,
+    "channel_code"    varchar(63)  NOT NULL,
+    "creator"         varchar(64)           DEFAULT '',
+    "create_time"     timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"         varchar(64)           DEFAULT '',
+    "update_time"     timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"         bit          NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '短信模板';
 ) COMMENT '短信模板';
 
 
-CREATE TABLE IF NOT EXISTS "system_sms_log" (
-   "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-   "channel_id" bigint NOT NULL,
-   "channel_code" varchar(63) NOT NULL,
-   "template_id" bigint NOT NULL,
-   "template_code" varchar(63) NOT NULL,
-   "template_type" tinyint NOT NULL,
-   "template_content" varchar(255) NOT NULL,
-   "template_params" varchar(255) NOT NULL,
-   "api_template_id" varchar(63) NOT NULL,
-   "mobile" varchar(11) NOT NULL,
-   "user_id" bigint DEFAULT '0',
-   "user_type" tinyint DEFAULT '0',
-   "send_status" tinyint NOT NULL DEFAULT '0',
-   "send_time" timestamp DEFAULT NULL,
-   "send_code" int DEFAULT NULL,
-   "send_msg" varchar(255) DEFAULT NULL,
-   "api_send_code" varchar(63) DEFAULT NULL,
-   "api_send_msg" varchar(255) DEFAULT NULL,
-   "api_request_id" varchar(255) DEFAULT NULL,
-   "api_serial_no" varchar(255) DEFAULT NULL,
-   "receive_status" tinyint NOT NULL DEFAULT '0',
-   "receive_time" timestamp DEFAULT NULL,
-   "api_receive_code" varchar(63) DEFAULT NULL,
-   "api_receive_msg" varchar(255) DEFAULT NULL,
-   "creator" varchar(64) DEFAULT '',
-   "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "updater" varchar(64) DEFAULT '',
-   "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "deleted" bit NOT NULL DEFAULT FALSE,
-   PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_sms_log"
+(
+    "id"               bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "channel_id"       bigint       NOT NULL,
+    "channel_code"     varchar(63)  NOT NULL,
+    "template_id"      bigint       NOT NULL,
+    "template_code"    varchar(63)  NOT NULL,
+    "template_type"    tinyint      NOT NULL,
+    "template_content" varchar(255) NOT NULL,
+    "template_params"  varchar(255) NOT NULL,
+    "api_template_id"  varchar(63)  NOT NULL,
+    "mobile"           varchar(11)  NOT NULL,
+    "user_id"          bigint                DEFAULT '0',
+    "user_type"        tinyint               DEFAULT '0',
+    "send_status"      tinyint      NOT NULL DEFAULT '0',
+    "send_time"        timestamp             DEFAULT NULL,
+    "send_code"        int                   DEFAULT NULL,
+    "send_msg"         varchar(255)          DEFAULT NULL,
+    "api_send_code"    varchar(63)           DEFAULT NULL,
+    "api_send_msg"     varchar(255)          DEFAULT NULL,
+    "api_request_id"   varchar(255)          DEFAULT NULL,
+    "api_serial_no"    varchar(255)          DEFAULT NULL,
+    "receive_status"   tinyint      NOT NULL DEFAULT '0',
+    "receive_time"     timestamp             DEFAULT NULL,
+    "api_receive_code" varchar(63)           DEFAULT NULL,
+    "api_receive_msg"  varchar(255)          DEFAULT NULL,
+    "creator"          varchar(64)           DEFAULT '',
+    "create_time"      timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"          varchar(64)           DEFAULT '',
+    "update_time"      timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"          bit          NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT '短信日志';
 ) COMMENT '短信日志';
 
 
-CREATE TABLE IF NOT EXISTS "system_sms_code" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "mobile" varchar(11) NOT NULL,
-    "code" varchar(11) NOT NULL,
-    "scene" bigint NOT NULL,
-    "create_ip" varchar NOT NULL,
-    "today_index" int NOT NULL,
-    "used" bit NOT NULL DEFAULT FALSE,
-    "used_time" timestamp DEFAULT NULL,
-    "used_ip" varchar NULL,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+
+CREATE TABLE IF NOT EXISTS "system_sms_code"
+(
+    "id"          bigint      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "mobile"      varchar(11) NOT NULL,
+    "code"        varchar(11) NOT NULL,
+    "scene"       bigint      NOT NULL,
+    "create_ip"   varchar     NOT NULL,
+    "today_index" int         NOT NULL,
+    "used"        bit         NOT NULL DEFAULT FALSE,
+    "used_time"   timestamp            DEFAULT NULL,
+    "used_ip"     varchar     NULL,
+    "creator"     varchar(64)          DEFAULT '',
+    "create_time" timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"     varchar(64)          DEFAULT '',
+    "update_time" timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"     bit         NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '短信日志';
 ) COMMENT '短信日志';
 
 
-CREATE TABLE IF NOT EXISTS "system_social_client" (
-  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-  "name" varchar(255) NOT NULL,
-  "social_type" int NOT NULL,
-  "user_type" int NOT NULL,
-  "client_id" varchar(255) NOT NULL,
-  "client_secret" varchar(255) NOT NULL,
-  "agent_id" varchar(255) NOT NULL,
-  "status" int NOT NULL,
-  "creator" varchar(64) DEFAULT '',
-  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-  "updater" varchar(64) DEFAULT '',
-  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  "deleted" bit NOT NULL DEFAULT FALSE,
-  "tenant_id" bigint not null default  '0',
-  PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_error_code"
+(
+    "id"               bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "type"             tinyint      NOT NULL DEFAULT '0',
+    "application_name" varchar(50)  NOT NULL,
+    "code"             int          NOT NULL DEFAULT '0',
+    "message"          varchar(512) NOT NULL DEFAULT '',
+    "memo"             varchar(512)          DEFAULT '',
+    "creator"          varchar(64)           DEFAULT '',
+    "create_time"      timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"          varchar(64)           DEFAULT '',
+    "update_time"      timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"          bit          NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
+) COMMENT '错误码表';
+
+CREATE TABLE IF NOT EXISTS "system_social_client"
+(
+    "id"            bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"          varchar(255) NOT NULL,
+    "social_type"   int          NOT NULL,
+    "user_type"     int          NOT NULL,
+    "client_id"     varchar(255) NOT NULL,
+    "client_secret" varchar(255) NOT NULL,
+    "agent_id"      varchar(255) NOT NULL,
+    "status"        int          NOT NULL,
+    "creator"       varchar(64)           DEFAULT '',
+    "create_time"   datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"       varchar(64)           DEFAULT '',
+    "update_time"   datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"       bit          NOT NULL DEFAULT FALSE,
+    "tenant_id"     bigint       not null default '0',
+    PRIMARY KEY ("id")
 ) COMMENT '社交客户端表';
 ) COMMENT '社交客户端表';
 
 
-CREATE TABLE IF NOT EXISTS "system_social_user" (
-   "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-   "type" tinyint NOT NULL,
-   "openid" varchar(64) NOT NULL,
-   "token" varchar(256) DEFAULT NULL,
-   "raw_token_info" varchar(1024) NOT NULL,
-   "nickname" varchar(32) NOT NULL,
-   "avatar" varchar(255) DEFAULT NULL,
-   "raw_user_info" varchar(1024) NOT NULL,
-   "code" varchar(64) NOT NULL,
-   "state" varchar(64),
-   "creator" varchar(64) DEFAULT '',
-   "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "updater" varchar(64) DEFAULT '',
-   "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "deleted" bit NOT NULL DEFAULT FALSE,
-   PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_social_user"
+(
+    "id"             bigint        NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "type"           tinyint       NOT NULL,
+    "openid"         varchar(64)   NOT NULL,
+    "token"          varchar(256)           DEFAULT NULL,
+    "raw_token_info" varchar(1024) NOT NULL,
+    "nickname"       varchar(32)   NOT NULL,
+    "avatar"         varchar(255)           DEFAULT NULL,
+    "raw_user_info"  varchar(1024) NOT NULL,
+    "code"           varchar(64)   NOT NULL,
+    "state"          varchar(64),
+    "creator"        varchar(64)            DEFAULT '',
+    "create_time"    timestamp     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"        varchar(64)            DEFAULT '',
+    "update_time"    timestamp     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"        bit           NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT '社交用户';
 ) COMMENT '社交用户';
 
 
-CREATE TABLE IF NOT EXISTS "system_social_user_bind" (
-   "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-   "user_id" bigint NOT NULL,
-   "user_type" tinyint NOT NULL,
-   "social_type" tinyint NOT NULL,
-   "social_user_id" number NOT NULL,
-   "creator" varchar(64) DEFAULT '',
-   "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "updater" varchar(64) DEFAULT '',
-   "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "deleted" bit NOT NULL DEFAULT FALSE,
-   PRIMARY KEY ("id")
-) COMMENT '社交用户的绑定';
-
-CREATE TABLE IF NOT EXISTS "system_tenant" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar(63) NOT NULL,
-    "contact_user_id" bigint NOT NULL DEFAULT '0',
-    "contact_name" varchar(255) NOT NULL,
-    "contact_mobile" varchar(255),
-    "status" tinyint NOT NULL,
-    "website" varchar(63) DEFAULT '',
-    "package_id"  bigint NOT NULL,
-    "expire_time" timestamp NOT NULL,
-    "account_count" int NOT NULL,
-    "creator" varchar(64) DEFAULT '',
+CREATE TABLE IF NOT EXISTS "system_social_user_bind"
+(
+    "id"          bigint    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"     bigint    NOT NULL,
+    "user_type"   tinyint   NOT NULL,
+    "social_type" tinyint   NOT NULL,
+    "social_user_id" number NOT NULL,
+    "creator"     varchar(64)        DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
+    "updater"     varchar(64)        DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+    "deleted"     bit       NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
+) COMMENT '社交用户的绑定';
+
+CREATE TABLE IF NOT EXISTS "system_tenant"
+(
+    "id"              bigint       NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"            varchar(63)  NOT NULL,
+    "contact_user_id" bigint       NOT NULL DEFAULT '0',
+    "contact_name"    varchar(255) NOT NULL,
+    "contact_mobile"  varchar(255),
+    "status"          tinyint      NOT NULL,
+    "website"         varchar(63)           DEFAULT '',
+    "package_id"      bigint       NOT NULL,
+    "expire_time"     timestamp    NOT NULL,
+    "account_count"   int          NOT NULL,
+    "creator"         varchar(64)           DEFAULT '',
+    "create_time"     timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"         varchar(64)           DEFAULT '',
+    "update_time"     timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted"         bit          NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '租户';
 ) COMMENT '租户';
 
 
-CREATE TABLE IF NOT EXISTS "system_tenant_package" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar(30) NOT NULL,
-    "status" tinyint NOT NULL,
-    "remark" varchar(256),
-    "menu_ids" varchar(2048) NOT NULL,
-    "creator" varchar(64) DEFAULT '',
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar(64) DEFAULT '',
-    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+CREATE TABLE IF NOT EXISTS "system_tenant_package"
+(
+    "id"          bigint        NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"        varchar(30)   NOT NULL,
+    "status"      tinyint       NOT NULL,
+    "remark"      varchar(256),
+    "menu_ids"    varchar(2048) NOT NULL,
+    "creator"     varchar(64)            DEFAULT '',
+    "create_time" datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"     varchar(64)            DEFAULT '',
+    "update_time" datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"     bit           NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '租户套餐表';
 ) COMMENT '租户套餐表';
 
 
-CREATE TABLE IF NOT EXISTS "system_oauth2_client" (
-  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-  "client_id" varchar NOT NULL,
-  "secret" varchar NOT NULL,
-  "name" varchar NOT NULL,
-  "logo" varchar NOT NULL,
-  "description" varchar,
-  "status" int NOT NULL,
-  "access_token_validity_seconds" int NOT NULL,
-  "refresh_token_validity_seconds" int NOT NULL,
-  "redirect_uris" varchar NOT NULL,
-  "authorized_grant_types" varchar NOT NULL,
-  "scopes" varchar NOT NULL DEFAULT '',
-  "auto_approve_scopes" varchar NOT NULL DEFAULT '',
-  "authorities" varchar NOT NULL DEFAULT '',
-  "resource_ids" varchar NOT NULL DEFAULT '',
-  "additional_information" varchar NOT NULL DEFAULT '',
-  "creator" varchar DEFAULT '',
-  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-  "updater" varchar DEFAULT '',
-  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  "deleted" bit NOT NULL DEFAULT FALSE,
-  PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_sensitive_word"
+(
+    "id"          bigint        NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"        varchar(255)  NOT NULL,
+    "tags"        varchar(1024) NOT NULL,
+    "status"      bit           NOT NULL DEFAULT FALSE,
+    "description" varchar(512),
+    "creator"     varchar(64)            DEFAULT '',
+    "create_time" datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"     varchar(64)            DEFAULT '',
+    "update_time" datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"     bit           NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
+) COMMENT '系统敏感词';
+
+CREATE TABLE IF NOT EXISTS "system_oauth2_client"
+(
+    "id"                             bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "client_id"                      varchar  NOT NULL,
+    "secret"                         varchar  NOT NULL,
+    "name"                           varchar  NOT NULL,
+    "logo"                           varchar  NOT NULL,
+    "description"                    varchar,
+    "status"                         int      NOT NULL,
+    "access_token_validity_seconds"  int      NOT NULL,
+    "refresh_token_validity_seconds" int      NOT NULL,
+    "redirect_uris"                  varchar  NOT NULL,
+    "authorized_grant_types"         varchar  NOT NULL,
+    "scopes"                         varchar  NOT NULL DEFAULT '',
+    "auto_approve_scopes"            varchar  NOT NULL DEFAULT '',
+    "authorities"                    varchar  NOT NULL DEFAULT '',
+    "resource_ids"                   varchar  NOT NULL DEFAULT '',
+    "additional_information"         varchar  NOT NULL DEFAULT '',
+    "creator"                        varchar           DEFAULT '',
+    "create_time"                    datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"                        varchar           DEFAULT '',
+    "update_time"                    datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"                        bit      NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT 'OAuth2 客户端表';
 ) COMMENT 'OAuth2 客户端表';
 
 
-CREATE TABLE IF NOT EXISTS "system_oauth2_approve" (
-  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-  "user_id" bigint NOT NULL,
-  "user_type" tinyint NOT NULL,
-  "client_id" varchar NOT NULL,
-  "scope" varchar NOT NULL,
-  "approved" bit NOT NULL DEFAULT FALSE,
-  "expires_time" datetime NOT NULL,
-  "creator" varchar DEFAULT '',
-  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-  "updater" varchar DEFAULT '',
-  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  "deleted" bit NOT NULL DEFAULT FALSE,
-  PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_oauth2_approve"
+(
+    "id"           bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"      bigint   NOT NULL,
+    "user_type"    tinyint  NOT NULL,
+    "client_id"    varchar  NOT NULL,
+    "scope"        varchar  NOT NULL,
+    "approved"     bit      NOT NULL DEFAULT FALSE,
+    "expires_time" datetime NOT NULL,
+    "creator"      varchar           DEFAULT '',
+    "create_time"  datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"      varchar           DEFAULT '',
+    "update_time"  datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"      bit      NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT 'OAuth2 批准表';
 ) COMMENT 'OAuth2 批准表';
 
 
-CREATE TABLE IF NOT EXISTS "system_oauth2_access_token" (
-   "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-   "user_id" bigint NOT NULL,
-   "user_type" tinyint NOT NULL,
-   "user_info" varchar NOT NULL,
-   "access_token" varchar NOT NULL,
-   "refresh_token" varchar NOT NULL,
-   "client_id" varchar NOT NULL,
-   "scopes" varchar NOT NULL,
-   "approved" bit NOT NULL DEFAULT FALSE,
-   "expires_time" datetime NOT NULL,
-   "creator" varchar DEFAULT '',
-   "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-   "updater" varchar DEFAULT '',
-   "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-   "deleted" bit NOT NULL DEFAULT FALSE,
-   "tenant_id" bigint NOT NULL,
-   PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_oauth2_access_token"
+(
+    "id"            bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"       bigint   NOT NULL,
+    "user_type"     tinyint  NOT NULL,
+    "user_info"     varchar  NOT NULL,
+    "access_token"  varchar  NOT NULL,
+    "refresh_token" varchar  NOT NULL,
+    "client_id"     varchar  NOT NULL,
+    "scopes"        varchar  NOT NULL,
+    "approved"      bit      NOT NULL DEFAULT FALSE,
+    "expires_time"  datetime NOT NULL,
+    "creator"       varchar           DEFAULT '',
+    "create_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"       varchar           DEFAULT '',
+    "update_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"       bit      NOT NULL DEFAULT FALSE,
+    "tenant_id"     bigint   NOT NULL,
+    PRIMARY KEY ("id")
 ) COMMENT 'OAuth2 访问令牌';
 ) COMMENT 'OAuth2 访问令牌';
 
 
-CREATE TABLE IF NOT EXISTS "system_oauth2_refresh_token" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "user_id" bigint NOT NULL,
-    "user_type" tinyint NOT NULL,
-    "refresh_token" varchar NOT NULL,
-    "client_id" varchar NOT NULL,
-    "scopes" varchar NOT NULL,
-    "approved" bit NOT NULL DEFAULT FALSE,
-    "expires_time" datetime NOT NULL,
-    "creator" varchar DEFAULT '',
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
-    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+CREATE TABLE IF NOT EXISTS "system_oauth2_refresh_token"
+(
+    "id"            bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"       bigint   NOT NULL,
+    "user_type"     tinyint  NOT NULL,
+    "refresh_token" varchar  NOT NULL,
+    "client_id"     varchar  NOT NULL,
+    "scopes"        varchar  NOT NULL,
+    "approved"      bit      NOT NULL DEFAULT FALSE,
+    "expires_time"  datetime NOT NULL,
+    "creator"       varchar           DEFAULT '',
+    "create_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"       varchar           DEFAULT '',
+    "update_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"       bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT 'OAuth2 刷新令牌';
 ) COMMENT 'OAuth2 刷新令牌';
 
 
-CREATE TABLE IF NOT EXISTS "system_oauth2_code" (
-     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-     "user_id" bigint NOT NULL,
-     "user_type" tinyint NOT NULL,
-     "code" varchar NOT NULL,
-     "client_id" varchar NOT NULL,
-     "scopes" varchar NOT NULL,
-     "expires_time" datetime NOT NULL,
-     "redirect_uri" varchar NOT NULL,
-     "state" varchar NOT NULL,
-     "creator" varchar DEFAULT '',
-     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-     "updater" varchar DEFAULT '',
-     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-     "deleted" bit NOT NULL DEFAULT FALSE,
-     PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "system_oauth2_code"
+(
+    "id"           bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"      bigint   NOT NULL,
+    "user_type"    tinyint  NOT NULL,
+    "code"         varchar  NOT NULL,
+    "client_id"    varchar  NOT NULL,
+    "scopes"       varchar  NOT NULL,
+    "expires_time" datetime NOT NULL,
+    "redirect_uri" varchar  NOT NULL,
+    "state"        varchar  NOT NULL,
+    "creator"      varchar           DEFAULT '',
+    "create_time"  datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"      varchar           DEFAULT '',
+    "update_time"  datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"      bit      NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT 'OAuth2 刷新令牌';
 ) COMMENT 'OAuth2 刷新令牌';
 
 
-CREATE TABLE IF NOT EXISTS "system_mail_account" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "mail" varchar NOT NULL,
-    "username" varchar NOT NULL,
-    "password" varchar NOT NULL,
-    "host" varchar NOT NULL,
-    "port" int NOT NULL,
-    "ssl_enable" bit NOT NULL,
-    "starttls_enable" bit NOT NULL,
-    "creator" varchar DEFAULT '',
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
-    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+CREATE TABLE IF NOT EXISTS "system_mail_account"
+(
+    "id"              bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "mail"            varchar  NOT NULL,
+    "username"        varchar  NOT NULL,
+    "password"        varchar  NOT NULL,
+    "host"            varchar  NOT NULL,
+    "port"            int      NOT NULL,
+    "ssl_enable"      bit      NOT NULL,
+    "starttls_enable" bit      NOT NULL,
+    "creator"         varchar           DEFAULT '',
+    "create_time"     datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"         varchar           DEFAULT '',
+    "update_time"     datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"         bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '邮箱账号表';
 ) COMMENT '邮箱账号表';
 
 
-CREATE TABLE IF NOT EXISTS "system_mail_template" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar NOT NULL,
-    "code" varchar NOT NULL,
-    "account_id" bigint NOT NULL,
-    "nickname" varchar,
-    "title" varchar NOT NULL,
-    "content" varchar NOT NULL,
-    "params" varchar NOT NULL,
-    "status" varchar NOT NULL,
-    "remark" varchar,
-    "creator" varchar DEFAULT '',
+CREATE TABLE IF NOT EXISTS "system_mail_template"
+(
+    "id"          bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"        varchar  NOT NULL,
+    "code"        varchar  NOT NULL,
+    "account_id"  bigint   NOT NULL,
+    "nickname"    varchar,
+    "title"       varchar  NOT NULL,
+    "content"     varchar  NOT NULL,
+    "params"      varchar  NOT NULL,
+    "status"      varchar  NOT NULL,
+    "remark"      varchar,
+    "creator"     varchar           DEFAULT '',
     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
+    "updater"     varchar           DEFAULT '',
     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+    "deleted"     bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '邮件模版表';
 ) COMMENT '邮件模版表';
 
 
-CREATE TABLE IF NOT EXISTS "system_mail_log" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "user_id" bigint,
-    "user_type" varchar,
-    "to_mail" varchar NOT NULL,
-    "account_id" bigint NOT NULL,
-    "from_mail" varchar NOT NULL,
-    "template_id" bigint NOT NULL,
-    "template_code" varchar NOT NULL,
+CREATE TABLE IF NOT EXISTS "system_mail_log"
+(
+    "id"                bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"           bigint,
+    "user_type"         varchar,
+    "to_mail"           varchar  NOT NULL,
+    "account_id"        bigint   NOT NULL,
+    "from_mail"         varchar  NOT NULL,
+    "template_id"       bigint   NOT NULL,
+    "template_code"     varchar  NOT NULL,
     "template_nickname" varchar,
     "template_nickname" varchar,
-    "template_title" varchar NOT NULL,
-    "template_content" varchar NOT NULL,
-    "template_params" varchar NOT NULL,
-    "send_status" varchar NOT NULL,
-    "send_time" datetime,
-    "send_message_id" varchar,
-    "send_exception" varchar,
-    "creator" varchar DEFAULT '',
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
-    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+    "template_title"    varchar  NOT NULL,
+    "template_content"  varchar  NOT NULL,
+    "template_params"   varchar  NOT NULL,
+    "send_status"       varchar  NOT NULL,
+    "send_time"         datetime,
+    "send_message_id"   varchar,
+    "send_exception"    varchar,
+    "creator"           varchar           DEFAULT '',
+    "create_time"       datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"           varchar           DEFAULT '',
+    "update_time"       datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"           bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '邮件日志表';
 ) COMMENT '邮件日志表';
 
 
 -- 将该建表 SQL 语句,添加到 citu-module-system-biz 模块的 test/resources/sql/create_tables.sql 文件里
 -- 将该建表 SQL 语句,添加到 citu-module-system-biz 模块的 test/resources/sql/create_tables.sql 文件里
-CREATE TABLE IF NOT EXISTS "system_notify_template" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar NOT NULL,
-    "code" varchar NOT NULL,
-    "nickname" varchar NOT NULL,
-    "content" varchar NOT NULL,
-    "type" varchar NOT NULL,
-    "params" varchar,
-    "status" varchar NOT NULL,
-    "remark" varchar,
-    "creator" varchar DEFAULT '',
+CREATE TABLE IF NOT EXISTS "system_notify_template"
+(
+    "id"          bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name"        varchar  NOT NULL,
+    "code"        varchar  NOT NULL,
+    "nickname"    varchar  NOT NULL,
+    "content"     varchar  NOT NULL,
+    "type"        varchar  NOT NULL,
+    "params"      varchar,
+    "status"      varchar  NOT NULL,
+    "remark"      varchar,
+    "creator"     varchar           DEFAULT '',
     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
+    "updater"     varchar           DEFAULT '',
     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+    "deleted"     bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '站内信模板表';
 ) COMMENT '站内信模板表';
 
 
-CREATE TABLE IF NOT EXISTS "system_notify_message" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "user_id" bigint NOT NULL,
-    "user_type" varchar NOT NULL,
-    "template_id" bigint NOT NULL,
-    "template_code" varchar NOT NULL,
-    "template_nickname" varchar NOT NULL,
-    "template_content" varchar NOT NULL,
-    "template_type" int NOT NULL,
-    "template_params" varchar NOT NULL,
-    "read_status" bit NOT NULL,
-    "read_time" varchar,
-    "creator" varchar DEFAULT '',
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
-    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
-    "tenant_id" bigint not null default  '0',
+CREATE TABLE IF NOT EXISTS "system_notify_message"
+(
+    "id"                bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"           bigint   NOT NULL,
+    "user_type"         varchar  NOT NULL,
+    "template_id"       bigint   NOT NULL,
+    "template_code"     varchar  NOT NULL,
+    "template_nickname" varchar  NOT NULL,
+    "template_content"  varchar  NOT NULL,
+    "template_type"     int      NOT NULL,
+    "template_params"   varchar  NOT NULL,
+    "read_status"       bit      NOT NULL,
+    "read_time"         varchar,
+    "creator"           varchar           DEFAULT '',
+    "create_time"       datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"           varchar           DEFAULT '',
+    "update_time"       datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"           bit      NOT NULL DEFAULT FALSE,
+    "tenant_id"         bigint   not null default '0',
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '站内信消息表';
 ) COMMENT '站内信消息表';