Przeglądaj źródła

Merge branch 'master' into feature_user-session-timeout

# Conflicts:
#	sql/ruoyi-vue-pro.sql
Lyon 4 lat temu
rodzic
commit
bcb6a68b80
40 zmienionych plików z 625 dodań i 252 usunięć
  1. 6 3
      README.md
  2. 11 1
      pom.xml
  3. 27 13
      sql/ruoyi-vue-pro.sql
  4. 2 1
      src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java
  5. 4 4
      src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java
  6. 65 0
      src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handle/DefaultDBFieldHandler.java
  7. 4 0
      src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java
  8. 0 1
      src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md
  9. 1 0
      src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md
  10. 1 1
      src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
  11. 2 1
      src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
  12. 21 3
      src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java
  13. 6 4
      src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
  14. 2 1
      src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
  15. 5 2
      src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java
  16. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/dept/SysDeptBaseVO.java
  17. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostBaseVO.java
  18. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostExcelVO.java
  19. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/menu/SysMenuBaseVO.java
  20. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/role/SysRoleBaseVO.java
  21. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysDeptDO.java
  22. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysPostDO.java
  23. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/permission/SysMenuDO.java
  24. 3 3
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dict/SysDictDataMapper.java
  25. 20 1
      src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java
  26. 19 15
      src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java
  27. 8 4
      src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictTypeServiceImpl.java
  28. 4 2
      src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java
  29. 10 0
      src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java
  30. 46 0
      src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java
  31. 37 0
      src/test/java/cn/iocoder/dashboard/BaseDbUnitTest.java
  32. 1 0
      src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java
  33. 0 2
      src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java
  34. 0 16
      src/test/java/cn/iocoder/dashboard/config/SecurityTestConfiguration.java
  35. 45 29
      src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceTest.java
  36. 20 2
      src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthServiceImplTest.java
  37. 149 20
      src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataServiceTest.java
  38. 78 62
      src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictTypeServiceTest.java
  39. 6 39
      src/test/resources/application-unit-test.yaml
  40. 14 14
      src/test/resources/sql/create_tables.sql

+ 6 - 3
README.md

@@ -43,11 +43,12 @@
 1. 幂等组件:基于 Redis 实现幂等组件,解决重复请求问题
 1. 服务保障:基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能
 1. 日志服务:轻量级日志中心,查看远程服务器的日志
+1. 单元测试:基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等
 
 ### 研发工具
 
 1. 表单构建:拖动表单元素生成相应的 HTML 代码
-1. 代码生成:前后端代码的生成(Java、Vue、SQL),支持 CRUD 下载
+1. 代码生成:前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载
 1. 系统接口:基于 Swagger 自动生成相关的 RESTful API 接口文档
 1. 数据库文档:基于 Screw 自动生成数据库文档
 
@@ -83,7 +84,9 @@
 | [Spring Boot Admin](https://github.com/skywalking) | Spring Boot 监控平台 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
 | [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.11.4 |  |
 | [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
-| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码| 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
+| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
+| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.7.0 | - |
+| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 3.6.28 | - |
 
 **前端**
 
@@ -125,7 +128,7 @@
     </tr>
     <tr>
         <td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
-        <td><img src="https://oscimg.oschina.net/oscnet/up-6d73c2140ce694e3de4c05035fdc1868d4c.png"/></td>
+        <td> - </td>
     </tr>
 </table>
 

+ 11 - 1
pom.xml

@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-	<modelVersion>4.0.0</modelVersion>
+    <modelVersion>4.0.0</modelVersion>
 
     <groupId>cn.iocoder</groupId>
     <artifactId>dashboard</artifactId>
@@ -25,6 +25,7 @@
         <spring.boot.version>2.4.2</spring.boot.version>
         <!-- Web 相关 -->
         <knife4j.version>3.0.2</knife4j.version>
+        <swagger-annotations.version>1.5.22</swagger-annotations.version>
         <!-- DB 相关 -->
         <mysql-connector-java.version>5.1.46</mysql-connector-java.version>
         <druid.version>1.2.4</druid.version>
@@ -104,8 +105,17 @@
                     <artifactId>guava</artifactId>
                     <groupId>com.google.guava</groupId>
                 </exclusion>
+                <exclusion>
+                    <artifactId>swagger-annotations</artifactId>
+                    <groupId>io.swagger</groupId>
+                </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+            <version>${swagger-annotations.version}</version>
+        </dependency>
 
         <!-- DB 相关 -->
         <dependency>

+ 27 - 13
sql/ruoyi-vue-pro.sql

@@ -11,7 +11,7 @@
  Target Server Version : 50718
  File Encoding         : 65001
 
- Date: 07/03/2021 00:43:34
+ Date: 08/03/2021 00:50:29
 */
 
 SET NAMES utf8mb4;
@@ -43,7 +43,7 @@ CREATE TABLE `inf_api_access_log` (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=1822 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表';
+) ENGINE=InnoDB AUTO_INCREMENT=1850 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表';
 
 -- ----------------------------
 -- Records of inf_api_access_log
@@ -175,7 +175,7 @@ CREATE TABLE `inf_job_log` (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=4458 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表';
+) ENGINE=InnoDB AUTO_INCREMENT=4477 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表';
 
 -- ----------------------------
 -- Records of inf_job_log
@@ -579,7 +579,7 @@ CREATE TABLE `sys_operate_log` (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COMMENT='操作日志记录';
+) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='操作日志记录';
 
 -- ----------------------------
 -- Records of sys_operate_log
@@ -977,13 +977,14 @@ INSERT INTO `sys_user_session` VALUES ('e3ad1ef8b9aa4b329855b29c7b372e8f', 1, '1
 INSERT INTO `sys_user_session` VALUES ('ea0d48776db84da4ac0f4c2adf62c366', 1, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', '2021-02-08 07:02:03', '', '2021-02-08 07:02:03', '', '2021-03-07 14:58:55', b'0');
 INSERT INTO `sys_user_session` VALUES ('f881f7dc67d04cd29574657fdde32a62', 1, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', '2021-02-05 08:53:20', '', '2021-02-05 08:53:20', '', '2021-03-07 14:58:55', b'0');
 COMMIT;
--- ----------------------------null,
--- Table structure for tool_codegen_columnnull,
--- ----------------------------null,
-DROP TABLE IF EXISTS `tool_codegen_column`;null,
-CREATE TABLE `tool_codegen_column` (null,
-  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',null,
-  `table_id` bigint(20) NOT NULL COMMENT '表编号',null,
+
+-- ----------------------------
+-- Table structure for tool_codegen_column
+-- ----------------------------
+DROP TABLE IF EXISTS `tool_codegen_column`;
+CREATE TABLE `tool_codegen_column` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `table_id` bigint(20) NOT NULL COMMENT '表编号',
   `column_name` varchar(200) NOT NULL COMMENT '字段名',
   `column_type` varchar(100) NOT NULL COMMENT '字段类型',
   `column_comment` varchar(500) NOT NULL COMMENT '字段描述',
@@ -1007,7 +1008,7 @@ CREATE TABLE `tool_codegen_column` (null,
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=369 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表字段定义';
+) ENGINE=InnoDB AUTO_INCREMENT=381 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表字段定义';
 
 -- ----------------------------
 -- Records of tool_codegen_column
@@ -1149,6 +1150,18 @@ INSERT INTO `tool_codegen_column` VALUES (365, 29, 'create_time', 'datetime', '
 INSERT INTO `tool_codegen_column` VALUES (366, 29, 'update_by', 'varchar(64)', '更新者', b'1', b'0', '0', 8, 'String', 'updateBy', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '', '2021-03-06 03:52:57', '', '2021-03-06 03:52:57', b'0');
 INSERT INTO `tool_codegen_column` VALUES (367, 29, 'update_time', 'datetime', '更新时间', b'0', b'0', '0', 9, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '', '2021-03-06 03:52:57', '', '2021-03-06 03:52:57', b'0');
 INSERT INTO `tool_codegen_column` VALUES (368, 29, 'deleted', 'bit(1)', '是否删除', b'0', b'0', '0', 10, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '', '2021-03-06 03:52:57', '', '2021-03-06 03:52:57', b'0');
+INSERT INTO `tool_codegen_column` VALUES (369, 30, 'id', 'bigint(20)', '字典编码', b'0', b'1', '1', 1, 'Long', 'id', '', NULL, b'0', b'1', b'0', '=', b'1', 'input', '', '2021-03-06 06:48:28', '', '2021-03-06 06:48:28', b'0');
+INSERT INTO `tool_codegen_column` VALUES (370, 30, 'sort', 'int(4)', '字典排序', b'0', b'0', '0', 2, 'Integer', 'sort', '', NULL, b'1', b'1', b'0', '=', b'1', 'input', '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:38', b'0');
+INSERT INTO `tool_codegen_column` VALUES (371, 30, 'label', 'varchar(100)', '字典标签', b'0', b'0', '0', 3, 'String', 'label', '', NULL, b'1', b'1', b'1', 'LIKE', b'1', 'input', '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:38', b'0');
+INSERT INTO `tool_codegen_column` VALUES (372, 30, 'value', 'varchar(100)', '字典键值', b'0', b'0', '0', 4, 'String', 'value', '', NULL, b'1', b'1', b'0', '=', b'1', 'input', '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:38', b'0');
+INSERT INTO `tool_codegen_column` VALUES (373, 30, 'dict_type', 'varchar(100)', '字典类型', b'0', b'0', '0', 5, 'String', 'dictType', '', NULL, b'1', b'1', b'1', 'LIKE', b'1', 'select', '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:38', b'0');
+INSERT INTO `tool_codegen_column` VALUES (374, 30, 'status', 'tinyint(4)', '状态(0正常 1停用)', b'0', b'0', '0', 6, 'Integer', 'status', '', NULL, b'1', b'1', b'1', 'LIKE', b'1', 'radio', '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:38', b'0');
+INSERT INTO `tool_codegen_column` VALUES (375, 30, 'remark', 'varchar(500)', '备注', b'1', b'0', '0', 7, 'String', 'remark', '', NULL, b'1', b'1', b'0', '=', b'1', 'input', '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:38', b'0');
+INSERT INTO `tool_codegen_column` VALUES (376, 30, 'create_by', 'varchar(64)', '创建者', b'1', b'0', '0', 8, 'String', 'createBy', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '', '2021-03-06 06:48:28', '', '2021-03-06 06:48:28', b'0');
+INSERT INTO `tool_codegen_column` VALUES (377, 30, 'create_time', 'datetime', '创建时间', b'0', b'0', '0', 9, 'Date', 'createTime', '', NULL, b'0', b'0', b'1', 'BETWEEN', b'1', 'datetime', '', '2021-03-06 06:48:28', '', '2021-03-06 06:48:28', b'0');
+INSERT INTO `tool_codegen_column` VALUES (378, 30, 'update_by', 'varchar(64)', '更新者', b'1', b'0', '0', 10, 'String', 'updateBy', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '', '2021-03-06 06:48:28', '', '2021-03-06 06:48:28', b'0');
+INSERT INTO `tool_codegen_column` VALUES (379, 30, 'update_time', 'datetime', '更新时间', b'0', b'0', '0', 11, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '', '2021-03-06 06:48:28', '', '2021-03-06 06:48:28', b'0');
+INSERT INTO `tool_codegen_column` VALUES (380, 30, 'deleted', 'bit(1)', '是否删除', b'0', b'0', '0', 12, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '', '2021-03-06 06:48:28', '', '2021-03-06 06:48:28', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -1174,7 +1187,7 @@ CREATE TABLE `tool_codegen_table` (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表定义';
+) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COMMENT='代码生成表定义';
 
 -- ----------------------------
 -- Records of tool_codegen_table
@@ -1190,6 +1203,7 @@ INSERT INTO `tool_codegen_table` VALUES (26, 1, 'inf_api_access_log', 'API 访
 INSERT INTO `tool_codegen_table` VALUES (27, 1, 'inf_api_error_log', 'API 错误日志', NULL, 'infra', 'apiErrorLog', 'InfApiErrorLog', 'API 错误日志', '芋道源码', 1, 1083, '', '2021-02-26 06:54:49', '', '2021-02-26 07:53:03', b'0');
 INSERT INTO `tool_codegen_table` VALUES (28, 1, 'sys_dict_type', '字典类型表', NULL, 'system', 'dictType', 'SysDictType', '字典类型', '芋艿', 1, NULL, '', '2021-03-06 03:45:55', '', '2021-03-06 03:51:02', b'1');
 INSERT INTO `tool_codegen_table` VALUES (29, 1, 'sys_dict_type', '字典类型表', NULL, 'system', 'dict', 'SysDictType', '字典类型', '芋艿', 1, NULL, '', '2021-03-06 03:52:57', '', '2021-03-06 04:03:52', b'0');
+INSERT INTO `tool_codegen_table` VALUES (30, 1, 'sys_dict_data', '字典数据表', NULL, 'system', 'type', 'SysDictData', '字典数据', '芋道源码', 1, NULL, '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:47', b'0');
 COMMIT;
 
 -- ----------------------------

+ 2 - 1
src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java

@@ -13,7 +13,8 @@ import org.springframework.context.annotation.Configuration;
  * @author 芋道源码
  */
 @Configuration
-@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class)
+@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class,
+        lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
 public class MybatisConfiguration {
 
     @Bean

+ 4 - 4
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java

@@ -21,13 +21,13 @@ public class BaseDO implements Serializable {
      */
     private Date updateTime;
     /**
-     * 创建者 TODO 芋艿:迁移成编号
+     * 创建者
      */
-    private String createBy;
+    private String creator;
     /**
-     * 更新者 TODO 芋艿:迁移成编号
+     * 更新者
      */
-    private String updateBy;
+    private String updater;
     /**
      * 是否删除
      */

+ 65 - 0
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handle/DefaultDBFieldHandler.java

@@ -0,0 +1,65 @@
+package cn.iocoder.dashboard.framework.mybatis.core.handle;
+
+import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.dashboard.framework.security.core.LoginUser;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * 通用参数填充实现类
+ *
+ * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
+ *
+ * @author hexiaowu
+ */
+@Component
+public class DefaultDBFieldHandler implements MetaObjectHandler {
+
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
+            LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
+            Date current = new Date();
+
+            // 创建时间为空,则以当前时间为插入时间
+            if (Objects.isNull(baseDO.getCreateTime())) {
+                baseDO.setCreateTime(current);
+            }
+            // 更新时间为空,则以当前时间为更新时间
+            if (Objects.isNull(baseDO.getUpdateTime())) {
+                baseDO.setUpdateTime(current);
+            }
+            // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
+            if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) {
+                baseDO.setCreator(loginUser.getId().toString());
+            }
+            // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
+            if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) {
+                baseDO.setUpdater(loginUser.getId().toString());
+            }
+        }
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        Object modifyTime = getFieldValByName("updateTime", metaObject);
+        Object modifier = getFieldValByName("updater", metaObject);
+        // 获取登录用户信息
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+
+        // 更新时间为空,则以当前时间为更新时间
+        if (Objects.isNull(modifyTime)) {
+            setFieldValByName("updateTime", new Date(), metaObject);
+        }
+        // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
+        if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
+            setFieldValByName("updater", loginUser.getId(), metaObject);
+        }
+    }
+}

+ 4 - 0
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java

@@ -28,6 +28,10 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
         return selectOne(new QueryWrapper<T>().eq(field, value));
     }
 
+    default Integer selectCount(String field, Object value) {
+        return selectCount(new QueryWrapper<T>().eq(field, value));
+    }
+
     default List<T> selectList() {
         return selectList(new QueryWrapper<>());
     }

+ 0 - 1
src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md

@@ -1 +0,0 @@
-<http://www.iocoder.cn/Spring-Boot/Spring-Security/?github>

+ 1 - 0
src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md

@@ -0,0 +1 @@
+<https://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao>

+ 1 - 1
src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java

@@ -152,7 +152,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
                     .anyRequest().authenticated()
                 .and()
                 .headers().frameOptions().disable();
-        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
+        httpSecurity.logout().logoutUrl(webProperties.getApiPrefix() + "/logout").logoutSuccessHandler(logoutSuccessHandler);
         // 添加 JWT Filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
     }

+ 2 - 1
src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java

@@ -1,6 +1,7 @@
 package cn.iocoder.dashboard.framework.security.core.handler;
 
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
 import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
 import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
 import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
@@ -36,6 +37,6 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
             securityFrameworkService.logout(token);
         }
         // 返回成功
-        ServletUtils.writeJSON(response, null);
+        ServletUtils.writeJSON(response, CommonResult.success(null));
     }
 }

+ 21 - 3
src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java

@@ -2,7 +2,10 @@ package cn.iocoder.dashboard.framework.security.core.util;
 
 import cn.iocoder.dashboard.framework.security.core.LoginUser;
 import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
+import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.util.StringUtils;
@@ -40,9 +43,20 @@ public class SecurityFrameworkUtils {
 
     /**
      * 获取当前用户
+     *
+     * @return 当前用户
      */
+    @Nullable
     public static LoginUser getLoginUser() {
-        return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        SecurityContext context = SecurityContextHolder.getContext();
+        if (context == null) {
+            return null;
+        }
+        Authentication authentication = context.getAuthentication();
+        if (authentication == null) {
+            return null;
+        }
+        return (LoginUser) authentication.getPrincipal();
     }
 
     /**
@@ -50,8 +64,10 @@ public class SecurityFrameworkUtils {
      *
      * @return 用户编号
      */
+    @Nullable
     public static Long getLoginUserId() {
-        return getLoginUser().getId();
+        LoginUser loginUser = getLoginUser();
+        return loginUser != null ? loginUser.getId() : null;
     }
 
     /**
@@ -59,8 +75,10 @@ public class SecurityFrameworkUtils {
      *
      * @return 角色编号数组
      */
+    @Nullable
     public static Set<Long> getLoginUserRoleIds() {
-        return getLoginUser().getRoleIds();
+        LoginUser loginUser = getLoginUser();
+        return loginUser != null ? loginUser.getRoleIds() : null;
     }
 
     /**

+ 6 - 4
src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java

@@ -10,15 +10,17 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpHeaders;
 import springfox.documentation.builders.ApiInfoBuilder;
 import springfox.documentation.builders.PathSelectors;
-import springfox.documentation.service.*;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.ApiKey;
+import springfox.documentation.service.AuthorizationScope;
+import springfox.documentation.service.Contact;
+import springfox.documentation.service.SecurityReference;
+import springfox.documentation.service.SecurityScheme;
 import springfox.documentation.spi.DocumentationType;
 import springfox.documentation.spi.service.contexts.SecurityContext;
 import springfox.documentation.spring.web.plugins.Docket;
 import springfox.documentation.swagger2.annotations.EnableSwagger2;
-import springfox.documentation.service.ApiKey;
 
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 

+ 2 - 1
src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java

@@ -27,9 +27,10 @@ public class WebConfiguration implements WebMvcConfigurer {
 
     @Override
     public void configurePathMatch(PathMatchConfigurer configurer) {
+        // 设置 API 前缀,仅仅匹配 controller 包下的
         configurer.addPathPrefix(webProperties.getApiPrefix(), clazz ->
                 clazz.isAnnotationPresent(RestController.class)
-                && clazz.getPackage().getName().startsWith(webProperties.getControllerPackage()));
+                && clazz.getPackage().getName().startsWith(webProperties.getControllerPackage())); // 仅仅匹配 controller 包
     }
 
     // ========== Filter 相关 ==========

+ 5 - 2
src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java

@@ -12,6 +12,7 @@ import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
 import cn.iocoder.dashboard.modules.infra.enums.config.InfConfigTypeEnum;
 import cn.iocoder.dashboard.modules.infra.mq.producer.config.InfConfigProducer;
 import cn.iocoder.dashboard.modules.infra.service.config.InfConfigService;
+import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
@@ -99,7 +100,8 @@ public class InfConfigServiceImpl implements InfConfigService {
         checkConfigKeyUnique(id, key);
     }
 
-    private InfConfigDO checkConfigExists(Long id) {
+    @VisibleForTesting
+    public InfConfigDO checkConfigExists(Long id) {
         if (id == null) {
             return null;
         }
@@ -110,7 +112,8 @@ public class InfConfigServiceImpl implements InfConfigService {
         return config;
     }
 
-    private void checkConfigKeyUnique(Long id, String key) {
+    @VisibleForTesting
+    public void checkConfigKeyUnique(Long id, String key) {
         InfConfigDO config = configMapper.selectByKey(key);
         if (config == null) {
             return;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/dept/SysDeptBaseVO.java

@@ -26,7 +26,7 @@ public class SysDeptBaseVO {
 
     @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
     @NotBlank(message = "显示顺序不能为空")
-    private String sort;
+    private Integer sort;
 
     @ApiModelProperty(value = "负责人", example = "芋道")
     private String leader;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostBaseVO.java

@@ -25,7 +25,7 @@ public class SysPostBaseVO {
 
     @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
     @NotBlank(message = "显示顺序不能为空")
-    private String sort;
+    private Integer sort;
 
     @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
     private Integer status;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostExcelVO.java

@@ -23,7 +23,7 @@ public class SysPostExcelVO {
     private String name;
 
     @ExcelProperty("岗位排序")
-    private String sort;
+    private Integer sort;
 
     @ExcelProperty(value = "状态", converter = DictConvert.class)
     @DictFormat(SYS_COMMON_STATUS)

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/menu/SysMenuBaseVO.java

@@ -29,7 +29,7 @@ public class SysMenuBaseVO {
 
     @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
     @NotBlank(message = "显示顺序不能为空")
-    private String sort;
+    private Integer sort;
 
     @ApiModelProperty(value = "父菜单 ID", required = true, example = "1024")
     @NotNull(message = "父菜单 ID 不能为空")

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/role/SysRoleBaseVO.java

@@ -25,7 +25,7 @@ public class SysRoleBaseVO {
 
     @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
     @NotBlank(message = "显示顺序不能为空")
-    private String sort;
+    private Integer sort;
 
     @ApiModelProperty(value = "角色类型", required = true, example = "1", notes = "见 SysRoleTypeEnum 枚举")
     private Integer type;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysDeptDO.java

@@ -35,7 +35,7 @@ public class SysDeptDO extends BaseDO {
     /**
      * 显示顺序
      */
-    private String sort;
+    private Integer sort;
     /**
      * 负责人
      */

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysPostDO.java

@@ -34,7 +34,7 @@ public class SysPostDO extends BaseDO {
     /**
      * 岗位排序
      */
-    private String sort;
+    private Integer sort;
     /**
      * 状态
      *

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/permission/SysMenuDO.java

@@ -49,7 +49,7 @@ public class SysMenuDO extends BaseDO {
     /**
      * 显示顺序
      */
-    private String sort;
+    private Integer sort;
     /**
      * 父菜单ID
      */

+ 3 - 3
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dict/SysDictDataMapper.java

@@ -15,13 +15,13 @@ import java.util.List;
 @Mapper
 public interface SysDictDataMapper extends BaseMapperX<SysDictDataDO> {
 
-    default SysDictDataDO selectByDictTypeAndLabel(String dictType, String label) {
+    default SysDictDataDO selectByDictTypeAndValue(String dictType, String value) {
         return selectOne(new QueryWrapper<SysDictDataDO>().eq("dict_type", dictType)
-                .eq("label", label));
+                .eq("value", value));
     }
 
     default int selectCountByDictType(String dictType) {
-        return selectCount(new QueryWrapper<SysDictDataDO>().eq("dict_type", dictType));
+        return selectCount("dict_type", dictType);
     }
 
     default PageResult<SysDictDataDO> selectPage(SysDictDataPageReqVO reqVO) {

+ 20 - 1
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java

@@ -160,7 +160,26 @@ public class SysAuthServiceImpl implements SysAuthService {
 
     @Override
     public void logout(String token) {
-//        AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功")); TODO 需要搞一搞
+        // 查询用户信息
+        LoginUser loginUser = userSessionService.getLoginUser(token);
+        if (loginUser == null) {
+            return;
+        }
+        // 删除 session
+        userSessionService.deleteUserSession(token);
+        // 记录登出日子和
+        this.createLogoutLog(loginUser.getUsername());
+    }
+
+    private void createLogoutLog(String username) {
+        SysLoginLogCreateReqVO reqVO = new SysLoginLogCreateReqVO();
+        reqVO.setLogType(SysLoginLogTypeEnum.LOGOUT_SELF.getType());
+        reqVO.setTraceId(TracerUtils.getTraceId());
+        reqVO.setUsername(username);
+        reqVO.setUserAgent(ServletUtils.getUserAgent());
+        reqVO.setUserIp(ServletUtils.getClientIP());
+        reqVO.setResult(SysLoginResultEnum.SUCCESS.getResult());
+        loginLogService.createLoginLog(reqVO);
     }
 
     @Override

+ 19 - 15
src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java

@@ -2,7 +2,6 @@ package cn.iocoder.dashboard.modules.system.service.dict.impl;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
-import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.dashboard.common.pojo.PageResult;
 import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataCreateReqVO;
@@ -10,12 +9,13 @@ import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataEx
 import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataPageReqVO;
 import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataUpdateReqVO;
 import cn.iocoder.dashboard.modules.system.convert.dict.SysDictDataConvert;
-import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictDataMapper;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictTypeDO;
+import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictDataMapper;
 import cn.iocoder.dashboard.modules.system.mq.producer.dict.SysDictDataProducer;
 import cn.iocoder.dashboard.modules.system.service.dict.SysDictDataService;
 import cn.iocoder.dashboard.modules.system.service.dict.SysDictTypeService;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableTable;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -28,6 +28,7 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 
+import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
 
 /**
@@ -156,7 +157,7 @@ public class SysDictDataServiceImpl implements SysDictDataService {
     @Override
     public Long createDictData(SysDictDataCreateReqVO reqVO) {
         // 校验正确性
-        this.checkCreateOrUpdate(null, reqVO.getLabel(), reqVO.getDictType());
+        this.checkCreateOrUpdate(null, reqVO.getValue(), reqVO.getDictType());
         // 插入字典类型
         SysDictDataDO dictData = SysDictDataConvert.INSTANCE.convert(reqVO);
         dictDataMapper.insert(dictData);
@@ -168,7 +169,7 @@ public class SysDictDataServiceImpl implements SysDictDataService {
     @Override
     public void updateDictData(SysDictDataUpdateReqVO reqVO) {
         // 校验正确性
-        this.checkCreateOrUpdate(reqVO.getId(), reqVO.getLabel(), reqVO.getDictType());
+        this.checkCreateOrUpdate(reqVO.getId(), reqVO.getValue(), reqVO.getDictType());
         // 更新字典类型
         SysDictDataDO updateObj = SysDictDataConvert.INSTANCE.convert(reqVO);
         dictDataMapper.updateById(updateObj);
@@ -191,46 +192,49 @@ public class SysDictDataServiceImpl implements SysDictDataService {
         return dictDataMapper.selectCountByDictType(dictType);
     }
 
-    private void checkCreateOrUpdate(Long id, String label, String dictType) {
+    private void checkCreateOrUpdate(Long id, String value, String dictType) {
         // 校验自己存在
         checkDictDataExists(id);
         // 校验字典类型有效
         checkDictTypeValid(dictType);
         // 校验字典数据的值的唯一性
-        checkDictDataValueUnique(id, dictType, label);
+        checkDictDataValueUnique(id, dictType, value);
     }
 
-    private void checkDictDataValueUnique(Long id, String dictType, String label) {
-        SysDictDataDO dictData = dictDataMapper.selectByDictTypeAndLabel(dictType, label);
+    @VisibleForTesting
+    public void checkDictDataValueUnique(Long id, String dictType, String value) {
+        SysDictDataDO dictData = dictDataMapper.selectByDictTypeAndValue(dictType, value);
         if (dictData == null) {
             return;
         }
         // 如果 id 为空,说明不用比较是否为相同 id 的字典数据
         if (id == null) {
-            throw ServiceExceptionUtil.exception(DICT_DATA_VALUE_DUPLICATE);
+            throw exception(DICT_DATA_VALUE_DUPLICATE);
         }
         if (!dictData.getId().equals(id)) {
-            throw ServiceExceptionUtil.exception(DICT_DATA_VALUE_DUPLICATE);
+            throw exception(DICT_DATA_VALUE_DUPLICATE);
         }
     }
 
-    private void checkDictDataExists(Long id) {
+    @VisibleForTesting
+    public void checkDictDataExists(Long id) {
         if (id == null) {
             return;
         }
         SysDictDataDO dictData = dictDataMapper.selectById(id);
         if (dictData == null) {
-            throw ServiceExceptionUtil.exception(DICT_DATA_NOT_EXISTS);
+            throw exception(DICT_DATA_NOT_EXISTS);
         }
     }
 
-    private void checkDictTypeValid(String type) {
+    @VisibleForTesting
+    public void checkDictTypeValid(String type) {
         SysDictTypeDO dictType = dictTypeService.getDictType(type);
         if (dictType == null) {
-            throw ServiceExceptionUtil.exception(DICT_TYPE_NOT_EXISTS);
+            throw exception(DICT_TYPE_NOT_EXISTS);
         }
         if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) {
-            throw ServiceExceptionUtil.exception(DICT_TYPE_NOT_ENABLE);
+            throw exception(DICT_TYPE_NOT_ENABLE);
         }
     }
 

+ 8 - 4
src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictTypeServiceImpl.java

@@ -10,6 +10,7 @@ import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictTypeDO;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictTypeMapper;
 import cn.iocoder.dashboard.modules.system.service.dict.SysDictDataService;
 import cn.iocoder.dashboard.modules.system.service.dict.SysDictTypeService;
+import com.google.common.annotations.VisibleForTesting;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -97,8 +98,9 @@ public class SysDictTypeServiceImpl implements SysDictTypeService {
         checkDictTypeUnique(id, type);
     }
 
-    private void checkDictTypeNameUnique(Long id, String type) {
-        SysDictTypeDO dictType = dictTypeMapper.selectByName(type);
+    @VisibleForTesting
+    public void checkDictTypeNameUnique(Long id, String name) {
+        SysDictTypeDO dictType = dictTypeMapper.selectByName(name);
         if (dictType == null) {
             return;
         }
@@ -111,7 +113,8 @@ public class SysDictTypeServiceImpl implements SysDictTypeService {
         }
     }
 
-    private void checkDictTypeUnique(Long id, String type) {
+    @VisibleForTesting
+    public void checkDictTypeUnique(Long id, String type) {
         SysDictTypeDO dictType = dictTypeMapper.selectByType(type);
         if (dictType == null) {
             return;
@@ -125,7 +128,8 @@ public class SysDictTypeServiceImpl implements SysDictTypeService {
         }
     }
 
-    private SysDictTypeDO checkDictTypeExists(Long id) {
+    @VisibleForTesting
+    public SysDictTypeDO checkDictTypeExists(Long id) {
         if (id == null) {
             return null;
         }

+ 4 - 2
src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java

@@ -2,6 +2,8 @@ package cn.iocoder.dashboard.util.collection;
 
 import cn.hutool.core.util.ArrayUtil;
 
+import java.util.function.Consumer;
+
 /**
  * Array 工具类
  *
@@ -18,11 +20,11 @@ public class ArrayUtils {
      * @return 结果数组
      */
     @SafeVarargs
-    public static <T> T[] append(T object, T... newElements) {
+    public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
         if (object == null) {
             return newElements;
         }
-        T[] result = ArrayUtil.newArray(object.getClass(), 1 + newElements.length);
+        Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
         result[0] = object;
         System.arraycopy(newElements, 0, result, 1, newElements.length);
         return result;

+ 10 - 0
src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java

@@ -19,4 +19,14 @@ public class ObjectUtils {
         return result;
     }
 
+    public static <T extends Comparable<T>> T max(T obj1, T obj2) {
+        if (obj1 == null) {
+            return obj2;
+        }
+        if (obj2 == null) {
+            return obj1;
+        }
+        return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+    }
+
 }

+ 46 - 0
src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java

@@ -0,0 +1,46 @@
+package cn.iocoder.dashboard;
+
+import cn.iocoder.dashboard.config.RedisTestConfiguration;
+import cn.iocoder.dashboard.framework.datasource.config.DataSourceConfiguration;
+import cn.iocoder.dashboard.framework.mybatis.config.MybatisConfiguration;
+import cn.iocoder.dashboard.framework.redis.config.RedisConfig;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+/**
+ * 依赖内存 DB 的单元测试
+ *
+ * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
+ *
+ * @author 芋道源码
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
+@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
+@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
+public class BaseDbAndRedisUnitTest {
+
+    @Import({
+            // DB 配置类
+            DataSourceConfiguration.class, // 自己的 DB 配置类
+            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
+            DruidDataSourceAutoConfigure.class, // Druid 自动配置类
+            // MyBatis 配置类
+            MybatisConfiguration.class, // 自己的 MyBatis 配置类
+            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
+            // Redis 配置类
+            RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
+            RedisAutoConfiguration.class, // Spring Redis 自动配置类
+            RedisConfig.class, // 自己的 Redis 配置类
+            RedissonAutoConfiguration.class, // Redisson 自动高配置类
+    })
+    public static class Application {
+    }
+
+}

+ 37 - 0
src/test/java/cn/iocoder/dashboard/BaseDbUnitTest.java

@@ -0,0 +1,37 @@
+package cn.iocoder.dashboard;
+
+import cn.iocoder.dashboard.framework.datasource.config.DataSourceConfiguration;
+import cn.iocoder.dashboard.framework.mybatis.config.MybatisConfiguration;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+/**
+ * 依赖内存 DB 的单元测试
+ *
+ * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
+ *
+ * @author 芋道源码
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class)
+@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
+@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
+public class BaseDbUnitTest {
+
+    @Import({
+            // DB 配置类
+            DataSourceConfiguration.class, // 自己的 DB 配置类
+            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
+            DruidDataSourceAutoConfigure.class, // Druid 自动配置类
+            // MyBatis 配置类
+            MybatisConfiguration.class, // 自己的 MyBatis 配置类
+            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
+    })
+    public static class Application {
+    }
+
+}

+ 1 - 0
src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
 @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
 @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
+@Deprecated
 public class BaseSpringBootUnitTest {
 
     @Resource

+ 0 - 2
src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java

@@ -8,12 +8,10 @@ import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Lazy;
 
 import java.io.IOException;
 
 @Configuration(proxyBeanMethods = false)
-@Lazy(false) // 禁用懒加载,因为需要保证 Redis Server 必须先启动
 @EnableConfigurationProperties(RedisProperties.class)
 @AutoConfigureBefore({RedisAutoConfiguration.class, RedissonAutoConfiguration.class}) // 在 Redis 自动配置前,进行初始化
 public class RedisTestConfiguration {

+ 0 - 16
src/test/java/cn/iocoder/dashboard/config/SecurityTestConfiguration.java

@@ -1,16 +0,0 @@
-package cn.iocoder.dashboard.config;
-
-import org.mockito.Mockito;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.authentication.AuthenticationManager;
-
-@Configuration
-public class SecurityTestConfiguration {
-
-    @Bean
-    public AuthenticationManager authenticationManager() {
-        return Mockito.mock(AuthenticationManager.class);
-    }
-
-}

+ 45 - 29
src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceTest.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.modules.infra.service.config;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.common.pojo.PageResult;
 import cn.iocoder.dashboard.modules.infra.controller.config.vo.InfConfigCreateReqVO;
 import cn.iocoder.dashboard.modules.infra.controller.config.vo.InfConfigExportReqVO;
@@ -15,6 +15,7 @@ import cn.iocoder.dashboard.util.collection.ArrayUtils;
 import cn.iocoder.dashboard.util.object.ObjectUtils;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
 import java.util.List;
@@ -24,8 +25,7 @@ import static cn.hutool.core.util.RandomUtil.randomEle;
 import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.*;
 import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
-import static cn.iocoder.dashboard.util.RandomUtils.randomLongId;
-import static cn.iocoder.dashboard.util.RandomUtils.randomPojo;
+import static cn.iocoder.dashboard.util.RandomUtils.*;
 import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.times;
@@ -36,7 +36,8 @@ import static org.mockito.Mockito.verify;
  *
  * @author 芋道源码
  */
-public class InfConfigServiceTest extends BaseSpringBootUnitTest {
+@Import(InfConfigServiceImpl.class)
+public class InfConfigServiceTest extends BaseDbUnitTest {
 
     @Resource
     private InfConfigServiceImpl configService;
@@ -145,19 +146,6 @@ public class InfConfigServiceTest extends BaseSpringBootUnitTest {
         verify(configProducer, times(1)).sendConfigRefreshMessage();
     }
 
-    @Test
-    public void testCreateConfig_keyDuplicate() {
-        // 准备参数
-        InfConfigCreateReqVO reqVO = randomPojo(InfConfigCreateReqVO.class);
-        // mock 数据
-        configMapper.insert(randomInfConfigDO(o -> { // @Sql
-            o.setKey(reqVO.getKey()); // 模拟 key 重复
-        }));
-
-        // 调用, 并断言异常
-        assertServiceException(() -> configService.createConfig(reqVO), CONFIG_KEY_DUPLICATE);
-    }
-
     @Test
     public void testUpdateConfig_success() {
         // mock 数据
@@ -177,15 +165,6 @@ public class InfConfigServiceTest extends BaseSpringBootUnitTest {
         verify(configProducer, times(1)).sendConfigRefreshMessage();
     }
 
-    @Test
-    public void testUpdateConfig_notExists() {
-        // 准备参数
-        InfConfigUpdateReqVO reqVO = randomPojo(InfConfigUpdateReqVO.class);
-
-        // 调用, 并断言异常
-        assertServiceException(() -> configService.updateConfig(reqVO), CONFIG_NOT_EXISTS);
-    }
-
     @Test
     public void testDeleteConfig_success() {
         // mock 数据
@@ -219,12 +198,49 @@ public class InfConfigServiceTest extends BaseSpringBootUnitTest {
     }
 
     @Test
-    public void testDeleteConfig_notExists() {
+    public void testCheckConfigExists_success() {
+        // mock 数据
+        InfConfigDO dbConfigDO = randomInfConfigDO();
+        configMapper.insert(dbConfigDO);// @Sql: 先插入出一条存在的数据
+
+        // 调用成功
+        configService.checkConfigExists(dbConfigDO.getId());
+    }
+
+    @Test
+    public void testCheckConfigExist_notExists() {
+        assertServiceException(() -> configService.checkConfigExists(randomLongId()), CONFIG_NOT_EXISTS);
+    }
+
+    @Test
+    public void testCheckConfigKeyUnique_success() {
+        // 调用,成功
+        configService.checkConfigKeyUnique(randomLongId(), randomString());
+    }
+
+    @Test
+    public void testCheckConfigKeyUnique_keyDuplicateForCreate() {
+        // 准备参数
+        String key = randomString();
+        // mock 数据
+        configMapper.insert(randomInfConfigDO(o -> o.setKey(key)));
+
+        // 调用,校验异常
+        assertServiceException(() -> configService.checkConfigKeyUnique(null, key),
+                CONFIG_KEY_DUPLICATE);
+    }
+
+    @Test
+    public void testCheckConfigKeyUnique_keyDuplicateForUpdate() {
         // 准备参数
         Long id = randomLongId();
+        String key = randomString();
+        // mock 数据
+        configMapper.insert(randomInfConfigDO(o -> o.setKey(key)));
 
-        // 调用, 并断言异常
-        assertServiceException(() -> configService.deleteConfig(id), CONFIG_NOT_EXISTS);
+        // 调用,校验异常
+        assertServiceException(() -> configService.checkConfigKeyUnique(id, key),
+                CONFIG_KEY_DUPLICATE);
     }
 
     // ========== 随机对象 ==========

+ 20 - 2
src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthServiceImplTest.java

@@ -1,15 +1,19 @@
 package cn.iocoder.dashboard.modules.system.service.auth;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
 import cn.iocoder.dashboard.framework.security.core.LoginUser;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.dashboard.modules.system.service.auth.impl.SysAuthServiceImpl;
+import cn.iocoder.dashboard.modules.system.service.common.SysCaptchaService;
+import cn.iocoder.dashboard.modules.system.service.logger.SysLoginLogService;
 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
 import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
 import cn.iocoder.dashboard.util.AssertUtils;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 
 import javax.annotation.Resource;
@@ -21,7 +25,13 @@ import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
-public class SysAuthServiceImplTest extends BaseSpringBootUnitTest {
+/**
+ * {@link SysAuthServiceImpl} 的单元测试
+ *
+ * @author 芋道源码
+ */
+@Import(SysAuthServiceImpl.class)
+public class SysAuthServiceImplTest extends BaseDbUnitTest {
 
     @Resource
     private SysAuthServiceImpl authService;
@@ -30,6 +40,14 @@ public class SysAuthServiceImplTest extends BaseSpringBootUnitTest {
     private SysUserService userService;
     @MockBean
     private SysPermissionService permissionService;
+    @MockBean
+    private AuthenticationManager authenticationManager;
+    @MockBean
+    private SysCaptchaService captchaService;
+    @MockBean
+    private SysLoginLogService loginLogService;
+    @MockBean
+    private SysUserSessionService userSessionService;
 
     @Test
     public void testLoadUserByUsername_success() {

+ 149 - 20
src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataServiceTest.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.modules.system.service.dict;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
 import cn.iocoder.dashboard.common.pojo.PageResult;
 import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataCreateReqVO;
@@ -12,14 +12,20 @@ import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictTypeDO;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictDataMapper;
 import cn.iocoder.dashboard.modules.system.mq.producer.dict.SysDictDataProducer;
 import cn.iocoder.dashboard.modules.system.service.dict.impl.SysDictDataServiceImpl;
+import cn.iocoder.dashboard.util.collection.ArrayUtils;
 import cn.iocoder.dashboard.util.object.ObjectUtils;
+import com.google.common.collect.ImmutableTable;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
+import java.util.Date;
 import java.util.List;
+import java.util.function.Consumer;
 
-import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.DICT_DATA_NOT_EXISTS;
+import static cn.hutool.core.bean.BeanUtil.getFieldValue;
+import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
 import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
 import static cn.iocoder.dashboard.util.RandomUtils.*;
@@ -32,7 +38,8 @@ import static org.mockito.Mockito.*;
 *
 * @author 芋道源码
 */
-public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
+@Import(SysDictDataServiceImpl.class)
+public class SysDictDataServiceTest extends BaseDbUnitTest {
 
     @Resource
     private SysDictDataServiceImpl dictDataService;
@@ -44,6 +51,37 @@ public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
     @MockBean
     private SysDictDataProducer dictDataProducer;
 
+    /**
+     * 测试加载到新的字典数据的情况
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testInitLocalCache() {
+        // mock 数据
+        SysDictDataDO dictData01 = randomDictDataDO();
+        dictDataMapper.insert(dictData01);
+        SysDictDataDO dictData02 = randomDictDataDO();
+        dictDataMapper.insert(dictData02);
+
+        // 调用
+        dictDataService.initLocalCache();
+        // 断言 labelDictDataCache 缓存
+        ImmutableTable<String, String, SysDictDataDO> labelDictDataCache =
+                (ImmutableTable<String, String, SysDictDataDO>) getFieldValue(dictDataService, "labelDictDataCache");
+        assertEquals(2, labelDictDataCache.size());
+        assertPojoEquals(dictData01, labelDictDataCache.get(dictData01.getDictType(), dictData01.getLabel()));
+        assertPojoEquals(dictData02, labelDictDataCache.get(dictData02.getDictType(), dictData02.getLabel()));
+        // 断言 valueDictDataCache 缓存
+        ImmutableTable<String, String, SysDictDataDO> valueDictDataCache =
+                (ImmutableTable<String, String, SysDictDataDO>) getFieldValue(dictDataService, "valueDictDataCache");
+        assertEquals(2, valueDictDataCache.size());
+        assertPojoEquals(dictData01, valueDictDataCache.get(dictData01.getDictType(), dictData01.getValue()));
+        assertPojoEquals(dictData02, valueDictDataCache.get(dictData02.getDictType(), dictData02.getValue()));
+        // 断言 maxUpdateTime 缓存
+        Date maxUpdateTime = (Date) getFieldValue(dictDataService, "maxUpdateTime");
+        assertEquals(ObjectUtils.max(dictData01.getUpdateTime(), dictData02.getUpdateTime()), maxUpdateTime);
+    }
+
     @Test
     public void testGetDictDataPage() {
         // mock 数据
@@ -107,8 +145,7 @@ public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
         SysDictDataCreateReqVO reqVO = randomPojo(SysDictDataCreateReqVO.class,
                 o -> o.setStatus(randomCommonStatus()));
         // mock 方法
-        when(dictTypeService.getDictType(eq(reqVO.getDictType())))
-                .thenReturn(randomPojo(SysDictTypeDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
+        when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType()));
 
         // 调用
         Long dictDataId = dictDataService.createDictData(reqVO);
@@ -124,50 +161,142 @@ public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
     @Test
     public void testUpdateDictData_success() {
         // mock 数据
-        SysDictDataDO dbDictData = randomPojo(SysDictDataDO.class);
+        SysDictDataDO dbDictData = randomDictDataDO();
         dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据
         // 准备参数
         SysDictDataUpdateReqVO reqVO = randomPojo(SysDictDataUpdateReqVO.class, o -> {
             o.setId(dbDictData.getId()); // 设置更新的 ID
+            o.setStatus(randomCommonStatus());
         });
+        // mock 方法,字典类型
+        when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType()));
 
         // 调用
         dictDataService.updateDictData(reqVO);
         // 校验是否更新正确
         SysDictDataDO dictData = dictDataMapper.selectById(reqVO.getId()); // 获取最新的
         assertPojoEquals(reqVO, dictData);
+        // 校验调用
+        verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
     }
 
     @Test
-    public void testUpdateDictData_notExists() {
+    public void testDeleteDictData_success() {
+        // mock 数据
+        SysDictDataDO dbDictData = randomDictDataDO();
+        dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据
         // 准备参数
-        SysDictDataUpdateReqVO reqVO = randomPojo(SysDictDataUpdateReqVO.class);
+        Long id = dbDictData.getId();
 
-        // 调用, 并断言异常
-        assertServiceException(() -> dictDataService.updateDictData(reqVO), DICT_DATA_NOT_EXISTS);
+        // 调用
+        dictDataService.deleteDictData(id);
+        // 校验数据不存在了
+        assertNull(dictDataMapper.selectById(id));
+        // 校验调用
+        verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
     }
 
     @Test
-    public void testDeleteDictData_success() {
+    public void testCheckDictDataExists_success() {
         // mock 数据
-        SysDictDataDO dbDictData = randomPojo(SysDictDataDO.class);
+        SysDictDataDO dbDictData = randomDictDataDO();
         dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据
+
+        // 调用成功
+        dictDataService.checkDictDataExists(dbDictData.getId());
+    }
+
+    @Test
+    public void testCheckDictDataExists_notExists() {
+        assertServiceException(() -> dictDataService.checkDictDataExists(randomLongId()), DICT_DATA_NOT_EXISTS);
+    }
+
+    @Test
+    public void testCheckDictTypeValid_success() {
+        // mock 方法,数据类型被禁用
+        String type = randomString();
+        when(dictTypeService.getDictType(eq(type))).thenReturn(randomDictTypeDO(type));
+
+        // 调用, 成功
+        dictDataService.checkDictTypeValid(type);
+    }
+
+    @Test
+    public void testCheckDictTypeValid_notExists() {
+        assertServiceException(() -> dictDataService.checkDictTypeValid(randomString()), DICT_TYPE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testCheckDictTypeValid_notEnable() {
+        // mock 方法,数据类型被禁用
+        String dictType = randomString();
+        when(dictTypeService.getDictType(eq(dictType))).thenReturn(
+                randomPojo(SysDictTypeDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
+
+        // 调用, 并断言异常
+        assertServiceException(() -> dictDataService.checkDictTypeValid(dictType), DICT_TYPE_NOT_ENABLE);
+    }
+
+    @Test
+    public void testCheckDictDataValueUnique_success() {
+        // 调用,成功
+        dictDataService.checkDictDataValueUnique(randomLongId(), randomString(), randomString());
+    }
+
+    @Test
+    public void testCheckDictDataValueUnique_valueDuplicateForCreate() {
         // 准备参数
-        Long id = dbDictData.getId();
+        String dictType = randomString();
+        String value = randomString();
+        // mock 数据
+        dictDataMapper.insert(randomDictDataDO(o -> {
+            o.setDictType(dictType);
+            o.setValue(value);
+        }));
 
-        // 调用
-        dictDataService.deleteDictData(id);
-       // 校验数据不存在了
-       assertNull(dictDataMapper.selectById(id));
+        // 调用,校验异常
+        assertServiceException(() -> dictDataService.checkDictDataValueUnique(null, dictType, value),
+                DICT_DATA_VALUE_DUPLICATE);
     }
 
     @Test
-    public void testDeleteDictData_notExists() {
+    public void testCheckDictDataValueUnique_valueDuplicateForUpdate() {
         // 准备参数
         Long id = randomLongId();
+        String dictType = randomString();
+        String value = randomString();
+        // mock 数据
+        dictDataMapper.insert(randomDictDataDO(o -> {
+            o.setDictType(dictType);
+            o.setValue(value);
+        }));
 
-        // 调用, 并断言异常
-        assertServiceException(() -> dictDataService.deleteDictData(id), DICT_DATA_NOT_EXISTS);
+        // 调用,校验异常
+        assertServiceException(() -> dictDataService.checkDictDataValueUnique(id, dictType, value),
+                DICT_DATA_VALUE_DUPLICATE);
+    }
+
+    // ========== 随机对象 ==========
+
+    @SafeVarargs
+    private static SysDictDataDO randomDictDataDO(Consumer<SysDictDataDO>... consumers) {
+        Consumer<SysDictDataDO> consumer = (o) -> {
+            o.setStatus(randomCommonStatus()); // 保证 status 的范围
+        };
+        return randomPojo(SysDictDataDO.class, ArrayUtils.append(consumer, consumers));
+    }
+
+    /**
+     * 生成一个有效的字典类型
+     *
+     * @param type 字典类型
+     * @return SysDictTypeDO 对象
+     */
+    private static SysDictTypeDO randomDictTypeDO(String type) {
+        return randomPojo(SysDictTypeDO.class, o -> {
+            o.setType(type);
+            o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 是开启
+        });
     }
 
 }

+ 78 - 62
src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictTypeServiceTest.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.modules.system.service.dict;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
 import cn.iocoder.dashboard.common.pojo.PageResult;
 import cn.iocoder.dashboard.modules.system.controller.dict.vo.type.SysDictTypeCreateReqVO;
@@ -14,6 +14,7 @@ import cn.iocoder.dashboard.util.collection.ArrayUtils;
 import cn.iocoder.dashboard.util.object.ObjectUtils;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
 import java.util.List;
@@ -23,8 +24,7 @@ import static cn.hutool.core.util.RandomUtil.randomEle;
 import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
 import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
-import static cn.iocoder.dashboard.util.RandomUtils.randomLongId;
-import static cn.iocoder.dashboard.util.RandomUtils.randomPojo;
+import static cn.iocoder.dashboard.util.RandomUtils.*;
 import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.eq;
@@ -35,7 +35,8 @@ import static org.mockito.Mockito.when;
 *
 * @author 芋道源码
 */
-public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
+@Import(SysDictTypeServiceImpl.class)
+public class SysDictTypeServiceTest extends BaseDbUnitTest {
 
     @Resource
     private SysDictTypeServiceImpl dictTypeService;
@@ -142,32 +143,6 @@ public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
         assertPojoEquals(reqVO, dictType);
     }
 
-    @Test
-    public void testCreateDictType_nameDuplicate() {
-        // mock 数据
-        SysDictTypeDO dbDictType = randomDictTypeDO();
-        dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        SysDictTypeCreateReqVO reqVO = randomPojo(SysDictTypeCreateReqVO.class,
-                o -> o.setName(dbDictType.getName())); // 模拟 name 重复
-
-        // 调用, 并断言异常
-        assertServiceException(() -> dictTypeService.createDictType(reqVO), DICT_TYPE_NAME_DUPLICATE);
-    }
-
-    @Test
-    public void testCreateDictType_typeDuplicate() {
-        // mock 数据
-        SysDictTypeDO dbDictType = randomDictTypeDO();
-        dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        SysDictTypeCreateReqVO reqVO = randomPojo(SysDictTypeCreateReqVO.class,
-                o -> o.setType(dbDictType.getType())); // 模拟 type 重复
-
-        // 调用, 并断言异常
-        assertServiceException(() -> dictTypeService.createDictType(reqVO), DICT_TYPE_TYPE_DUPLICATE);
-    }
-
     @Test
     public void testUpdateDictType_success() {
         // mock 数据
@@ -187,67 +162,108 @@ public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
     }
 
     @Test
-    public void testUpdateDictType_notExists() {
+    public void testDeleteDictType_success() {
+        // mock 数据
+        SysDictTypeDO dbDictType = randomDictTypeDO();
+        dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
         // 准备参数
-        SysDictTypeUpdateReqVO reqVO = randomPojo(SysDictTypeUpdateReqVO.class);
+        Long id = dbDictType.getId();
 
-        // 调用, 并断言异常
-        assertServiceException(() -> dictTypeService.updateDictType(reqVO), DICT_TYPE_NOT_EXISTS);
+        // 调用
+        dictTypeService.deleteDictType(id);
+        // 校验数据不存在了
+        assertNull(dictTypeMapper.selectById(id));
     }
 
     @Test
-    public void testUpdateDictType_nameDuplicate() {
-        // mock 数据,稍后更新它
+    public void testDeleteDictType_hasChildren() {
+        // mock 数据
         SysDictTypeDO dbDictType = randomDictTypeDO();
-        dictTypeMapper.insert(dbDictType);
-        // mock 数据,ks稍后模拟重复它的名字
-        SysDictTypeDO nameDictType = randomDictTypeDO();
-        dictTypeMapper.insert(nameDictType);
+        dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
         // 准备参数
-        SysDictTypeUpdateReqVO reqVO = randomPojo(SysDictTypeUpdateReqVO.class, o -> {
-            o.setId(dbDictType.getId()); // 设置更新的 ID
-            o.setName(nameDictType.getName()); // 模拟 name 重复
-        });
+        Long id = dbDictType.getId();
+        // mock 方法
+        when(dictDataService.countByDictType(eq(dbDictType.getType()))).thenReturn(1);
 
         // 调用, 并断言异常
-        assertServiceException(() -> dictTypeService.updateDictType(reqVO), DICT_TYPE_NAME_DUPLICATE);
+        assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN);
     }
 
     @Test
-    public void testDeleteDictType_success() {
+    public void testCheckDictDataExists_success() {
         // mock 数据
         SysDictTypeDO dbDictType = randomDictTypeDO();
         dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
+
+        // 调用成功
+        dictTypeService.checkDictTypeExists(dbDictType.getId());
+    }
+
+    @Test
+    public void testCheckDictDataExists_notExists() {
+        assertServiceException(() -> dictTypeService.checkDictTypeExists(randomLongId()), DICT_TYPE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testCheckDictTypeUnique_success() {
+        // 调用,成功
+        dictTypeService.checkDictTypeUnique(randomLongId(), randomString());
+    }
+
+    @Test
+    public void testCheckDictTypeUnique_valueDuplicateForCreate() {
         // 准备参数
-        Long id = dbDictType.getId();
+        String type = randomString();
+        // mock 数据
+        dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type)));
 
-        // 调用
-        dictTypeService.deleteDictType(id);
-        // 校验数据不存在了
-        assertNull(dictTypeMapper.selectById(id));
+        // 调用,校验异常
+        assertServiceException(() -> dictTypeService.checkDictTypeUnique(null, type),
+                DICT_TYPE_TYPE_DUPLICATE);
     }
 
     @Test
-    public void testDeleteDictType_notExists() {
+    public void testCheckDictTypeUnique_valueDuplicateForUpdate() {
         // 准备参数
         Long id = randomLongId();
+        String type = randomString();
+        // mock 数据
+        dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type)));
 
-        // 调用, 并断言异常
-        assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_NOT_EXISTS);
+        // 调用,校验异常
+        assertServiceException(() -> dictTypeService.checkDictTypeUnique(id, type),
+                DICT_TYPE_TYPE_DUPLICATE);
     }
 
     @Test
-    public void testDeleteDictType_hasChildren() {
+    public void testCheckDictTypNameUnique_success() {
+        // 调用,成功
+        dictTypeService.checkDictTypeNameUnique(randomLongId(), randomString());
+    }
+
+    @Test
+    public void testCheckDictTypeNameUnique_nameDuplicateForCreate() {
+        // 准备参数
+        String name = randomString();
         // mock 数据
-        SysDictTypeDO dbDictType = randomDictTypeDO();
-        dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
+        dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name)));
+
+        // 调用,校验异常
+        assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(null, name),
+                DICT_TYPE_NAME_DUPLICATE);
+    }
+
+    @Test
+    public void testCheckDictTypeNameUnique_nameDuplicateForUpdate() {
         // 准备参数
-        Long id = dbDictType.getId();
-        // mock 方法
-        when(dictDataService.countByDictType(eq(dbDictType.getType()))).thenReturn(1);
+        Long id = randomLongId();
+        String name = randomString();
+        // mock 数据
+        dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name)));
 
-        // 调用, 并断言异常
-        assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN);
+        // 调用,校验异常
+        assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(id, name),
+                DICT_TYPE_NAME_DUPLICATE);
     }
 
     // ========== 随机对象 ==========

+ 6 - 39
src/test/resources/application-unit-test.yaml

@@ -3,21 +3,6 @@ spring:
     lazy-initialization: true # 开启懒加载,加快速度
     banner-mode: off # 单元测试,禁用 Banner
 
-  # 去除的自动配置项
-  autoconfigure:
-    exclude:
-      - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration # 单元测试,禁用 SpringSecurity
-      - org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration # 单元测试,禁用 SpringSecurity
-      - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 单元测试,禁用 Quartz
-      - com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration # 单元测试,禁用 Lock4j 分布式锁
-      - org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration # 单元测试,禁用 Scheduler 定时任务
-      - org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration # 项目没有使用 Spring Data,所以禁用配置类,加速启动
-
-# Swagger 接口文档的自动配置(单元测试,禁用 Swagger)
-springfox:
-  documentation:
-    auto-startup: false
-
 --- #################### 数据库相关配置 ####################
 
 spring:
@@ -29,6 +14,9 @@ spring:
     username: sa
     password:
     schema: classpath:sql/create_tables.sql # MySQL 转 H2 的语句,使用 https://www.jooq.org/translate/ 工具
+    druid:
+      async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
+      initial-size: 1 # 单元测试,配置为 1,提升启动速度
 
   # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
   redis:
@@ -36,17 +24,13 @@ spring:
     port: 16379 # 端口(单元测试,使用 16379 端口)
     database: 0 # 数据库索引
 
---- #################### 定时任务相关配置 ####################
+mybatis:
+  lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
 
-# Quartz 配置项,对应 QuartzProperties 配置类(单元测试,禁用 Quartz)
+--- #################### 定时任务相关配置 ####################
 
 --- #################### 配置中心相关配置 ####################
 
-# Apollo 配置中心
-apollo:
-  bootstrap:
-    enabled: false # 单元测试,禁用配置中心
-
 --- #################### 服务保障相关配置 ####################
 
 # Lock4j 配置项(单元测试,禁用 Lock4j)
@@ -63,23 +47,6 @@ resilience4j:
 
 --- #################### 监控相关配置 ####################
 
-# Actuator 监控端点的配置项
-management:
-  endpoints:
-    enabled-by-default: false
-
-# Spring Boot Admin 配置项
-spring:
-  boot:
-    admin:
-      # Spring Boot Admin Client 客户端的相关配置
-      client:
-        enabled: false
-      # Spring Boot Admin Server 服务端的相关配置
-      context-path: /admin # 配置 Spring
-
-# 日志文件配置(不需要配置)
-
 --- #################### 芋道相关配置 ####################
 
 # 芋道配置项,设置当前项目所有自定义的配置

+ 14 - 14
src/test/resources/sql/create_tables.sql

@@ -9,9 +9,9 @@ CREATE TABLE IF NOT EXISTS "inf_config" (
     "value" varchar(500) NOT NULL DEFAULT '',
     "sensitive" bit NOT NULL,
     "remark" varchar(500) DEFAULT NULL,
-    "create_by" varchar(64) DEFAULT '',
+    "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "update_by" varchar(64) DEFAULT '',
+    "updater" varchar(64) DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
@@ -28,9 +28,9 @@ CREATE TABLE IF NOT EXISTS "sys_dept" (
     "phone" varchar(11) DEFAULT NULL,
     "email" varchar(50) DEFAULT NULL,
     "status" tinyint NOT NULL,
-    "create_by" varchar(64) DEFAULT '',
+    "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "update_by" varchar(64) DEFAULT '',
+    "updater" varchar(64) DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
@@ -44,9 +44,9 @@ CREATE TABLE IF NOT EXISTS "sys_dict_data" (
     "dict_type" varchar(100) NOT NULL DEFAULT '',
     "status" tinyint NOT NULL DEFAULT '0',
     "remark" varchar(500) DEFAULT NULL,
-    "create_by" varchar(64) DEFAULT '',
+    "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "update_by" varchar(64) DEFAULT '',
+    "updater" varchar(64) DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
@@ -62,9 +62,9 @@ CREATE TABLE IF NOT EXISTS "sys_role" (
     "status" tinyint NOT NULL,
     "type" tinyint NOT NULL,
     "remark" varchar(500) DEFAULT NULL,
-    "create_by" varchar(64) DEFAULT '',
+    "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "update_by" varchar(64) DEFAULT '',
+    "updater" varchar(64) DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
@@ -74,9 +74,9 @@ CREATE TABLE IF NOT EXISTS "sys_role_menu" (
     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "role_id" bigint NOT NULL,
     "menu_id" bigint NOT NULL,
-    "create_by" varchar(64) DEFAULT '',
+    "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "update_by" varchar(64) DEFAULT '',
+    "updater" varchar(64) DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
@@ -93,9 +93,9 @@ CREATE TABLE IF NOT EXISTS "sys_menu" (
     "icon" varchar(100) DEFAULT '#',
     "component" varchar(255) DEFAULT NULL,
     "status" tinyint NOT NULL DEFAULT '0',
-    "create_by" varchar(64) DEFAULT '',
+    "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "update_by" varchar(64) DEFAULT '',
+    "updater" varchar(64) DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
@@ -107,9 +107,9 @@ CREATE TABLE "sys_dict_type" (
     "type" varchar(100) NOT NULL DEFAULT '',
     "status" tinyint NOT NULL DEFAULT '0',
     "remark" varchar(500) DEFAULT NULL,
-    "create_by" varchar(64) DEFAULT '',
+    "creator" varchar(64) DEFAULT '',
     "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "update_by" varchar(64) DEFAULT '',
+    "updater" varchar(64) DEFAULT '',
     "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
     "deleted" bit NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")