Procházet zdrojové kódy

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

rayson před 1 rokem
rodič
revize
7e032746f3
45 změnil soubory, kde provedl 3351 přidání a 486 odebrání
  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>
             </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>
                 <groupId>com.google.guava</groupId>
                 <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 lombok.extern.slf4j.Slf4j;
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
 /**
  * {@link ServiceException} 工具类
  *
  * 目的在于,格式化异常信息提示。
  * 考虑到 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
 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 的集成 ==========
 
     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) {
-        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) {

+ 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.web.config.CituWebAutoConfiguration
 com.citu.framework.apilog.config.CituApiLogRpcAutoConfiguration
+com.citu.framework.errorcode.config.CituErrorCodeAutoConfiguration
 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> <!-- 实现代码生成 -->
         </dependency>
 
+        <dependency>
+            <groupId>cn.smallbun.screw</groupId>
+            <artifactId>screw-core</artifactId> <!-- 实现数据库文档 -->
+        </dependency>
+
         <!-- 监控相关 -->
         <dependency>
             <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
     db-schemas: ${spring.datasource.dynamic.datasource.master.name}
     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: # 多租户相关配置项
     enable: true
     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_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 ==========
     ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1_002_018_000, "社交授权失败,原因是:{}");
     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_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 =========
     ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "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
   access-log: # 访问日志的配置项
     enable: false
+  error-code: # 错误码相关配置项
+    enable: false
   demo: false # 关闭演示模式
 
 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_log";
 DELETE FROM "system_sms_code";
+DELETE FROM "system_error_code";
 DELETE FROM "system_social_client";
 DELETE FROM "system_social_user";
 DELETE FROM "system_social_user_bind";
 DELETE FROM "system_tenant";
 DELETE FROM "system_tenant_package";
+DELETE FROM "system_sensitive_word";
 DELETE FROM "system_oauth2_client";
 DELETE FROM "system_oauth2_approve";
 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")
 ) 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")
 ) 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 '',
-    "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")
 ) 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,
-    "updater" varchar(64) DEFAULT '',
+    "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',
+    "deleted"     bit       NOT NULL DEFAULT FALSE,
+    "tenant_id"   bigint    not null default '0',
     PRIMARY KEY ("id")
 ) 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")
 ) 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")
 ) 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")
 ) 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`)
 ) 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,
     "code"        varchar(64) NOT NULL,
     "name"        varchar(50) NOT NULL,
@@ -147,11 +156,12 @@ CREATE TABLE IF NOT EXISTS "system_post" (
     "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',
+    "tenant_id"   bigint      not null default '0',
     PRIMARY KEY ("id")
 ) 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,
     "user_id"     bigint             DEFAULT NULL,
     "post_id"     bigint             DEFAULT NULL,
@@ -164,451 +174,506 @@ CREATE TABLE IF NOT EXISTS `system_user_post`(
     PRIMARY KEY (`id`)
 ) 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 '通知公告表';
 
-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,
     `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 '',
     `username`    varchar(50)  NOT NULL DEFAULT '',
     `result`      tinyint(4)   NOT NULL,
     `user_ip`     varchar(50)  NOT NULL,
     `user_agent`  varchar(512) NOT NULL,
-    `creator`   varchar(64)           DEFAULT '',
+    `creator`     varchar(64)           DEFAULT '',
     `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',
     PRIMARY KEY (`id`)
 ) 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,
-    `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 '',
-    `create_time`      datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `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(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`)
 ) 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")
 ) 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 '短信渠道';
 
-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")
 ) 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 '短信日志';
 
-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")
 ) 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 '社交客户端表';
 
-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 '社交用户';
 
-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,
-    "updater" varchar(64) DEFAULT '',
+    "updater"     varchar(64)        DEFAULT '',
     "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")
 ) 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")
 ) 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 客户端表';
 
-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 批准表';
 
-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 访问令牌';
 
-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")
 ) 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 刷新令牌';
 
-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")
 ) 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,
-    "updater" varchar DEFAULT '',
+    "updater"     varchar           DEFAULT '',
     "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")
 ) 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_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")
 ) COMMENT '邮件日志表';
 
 -- 将该建表 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,
-    "updater" varchar DEFAULT '',
+    "updater"     varchar           DEFAULT '',
     "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")
 ) 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")
 ) COMMENT '站内信消息表';