diff --git a/README.md b/README.md
index 4597c708f..6af0a5447 100644
--- a/README.md
+++ b/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 @@
|
- |
+ - |
diff --git a/pom.xml b/pom.xml
index bab83344e..a601b907d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
- 4.0.0
+ 4.0.0
cn.iocoder
dashboard
@@ -25,6 +25,7 @@
2.4.2
3.0.2
+ 1.5.22
5.1.46
1.2.4
@@ -104,8 +105,17 @@
guava
com.google.guava
+
+ swagger-annotations
+ io.swagger
+
+
+ io.swagger
+ swagger-annotations
+ ${swagger-annotations.version}
+
diff --git a/sql/ruoyi-vue-pro.sql b/sql/ruoyi-vue-pro.sql
index 972be5f97..93c15370d 100644
--- a/sql/ruoyi-vue-pro.sql
+++ b/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;
-- ----------------------------
diff --git a/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java
index a955c402e..2a70b53d4 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java
+++ b/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
diff --git a/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java b/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java
index 7f6c76150..df9ddb22e 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java
+++ b/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;
/**
* 是否删除
*/
diff --git a/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handle/DefaultDBFieldHandler.java b/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handle/DefaultDBFieldHandler.java
new file mode 100644
index 000000000..1651ecc47
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java b/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java
index eace96a26..b86fd762f 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java
@@ -28,6 +28,10 @@ public interface BaseMapperX extends BaseMapper {
return selectOne(new QueryWrapper().eq(field, value));
}
+ default Integer selectCount(String field, Object value) {
+ return selectCount(new QueryWrapper().eq(field, value));
+ }
+
default List selectList() {
return selectList(new QueryWrapper<>());
}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md
deleted file mode 100644
index 3fa673793..000000000
--- a/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md b/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md
new file mode 100644
index 000000000..8d6d0335b
--- /dev/null
+++ b/src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md
@@ -0,0 +1 @@
+
diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
index 927c72996..2b9e2202b 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
+++ b/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);
}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
index 07826b9ab..262f56fe2 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
+++ b/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));
}
}
diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java
index eb67479d4..b6c509b93 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java
+++ b/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 getLoginUserRoleIds() {
- return getLoginUser().getRoleIds();
+ LoginUser loginUser = getLoginUser();
+ return loginUser != null ? loginUser.getRoleIds() : null;
}
/**
diff --git a/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
index b550511af..21511c37c 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
+++ b/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;
diff --git a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
index 9fbe6237c..b87c49008 100644
--- a/src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
+++ b/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 相关 ==========
diff --git a/src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java
index 41bb5a357..27e49771d 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java
+++ b/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;
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/dept/SysDeptBaseVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/dept/SysDeptBaseVO.java
index fd40fd7de..922c395fd 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/dept/SysDeptBaseVO.java
+++ b/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;
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostBaseVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostBaseVO.java
index 162f71387..9a7943b0f 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostBaseVO.java
+++ b/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;
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostExcelVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostExcelVO.java
index 05ac0e962..74afa919e 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostExcelVO.java
+++ b/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)
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/menu/SysMenuBaseVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/menu/SysMenuBaseVO.java
index 818ef8c93..5882f825c 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/menu/SysMenuBaseVO.java
+++ b/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 不能为空")
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/role/SysRoleBaseVO.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/role/SysRoleBaseVO.java
index 80ee8bfcd..bf9a1b141 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/role/SysRoleBaseVO.java
+++ b/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;
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysDeptDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysDeptDO.java
index e2218f461..e116bd3b0 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysDeptDO.java
+++ b/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;
/**
* 负责人
*/
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysPostDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysPostDO.java
index d2eaefd3b..ba5f3e0ad 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysPostDO.java
+++ b/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;
/**
* 状态
*
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/permission/SysMenuDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/permission/SysMenuDO.java
index ee8f302dd..82956e9f9 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/permission/SysMenuDO.java
+++ b/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
*/
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dict/SysDictDataMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dict/SysDictDataMapper.java
index a9a4a1d55..78a410563 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dict/SysDictDataMapper.java
+++ b/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 {
- default SysDictDataDO selectByDictTypeAndLabel(String dictType, String label) {
+ default SysDictDataDO selectByDictTypeAndValue(String dictType, String value) {
return selectOne(new QueryWrapper().eq("dict_type", dictType)
- .eq("label", label));
+ .eq("value", value));
}
default int selectCountByDictType(String dictType) {
- return selectCount(new QueryWrapper().eq("dict_type", dictType));
+ return selectCount("dict_type", dictType);
}
default PageResult selectPage(SysDictDataPageReqVO reqVO) {
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java
index 1cbc7bb0b..dc5b3be84 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java
+++ b/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
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java
index e8ebb82dc..603a9933e 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java
+++ b/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);
}
}
diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictTypeServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictTypeServiceImpl.java
index 24b6be5e0..c210e8e2b 100644
--- a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictTypeServiceImpl.java
+++ b/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;
}
diff --git a/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java b/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java
index 7dc9fb654..e40442732 100644
--- a/src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java
+++ b/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[] append(T object, T... newElements) {
+ public static Consumer[] append(Consumer object, Consumer... newElements) {
if (object == null) {
return newElements;
}
- T[] result = ArrayUtil.newArray(object.getClass(), 1 + newElements.length);
+ Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
result[0] = object;
System.arraycopy(newElements, 0, result, 1, newElements.length);
return result;
diff --git a/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java b/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java
index c8b2d2e80..1d652488f 100644
--- a/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java
+++ b/src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java
@@ -19,4 +19,14 @@ public class ObjectUtils {
return result;
}
+ public static > T max(T obj1, T obj2) {
+ if (obj1 == null) {
+ return obj2;
+ }
+ if (obj2 == null) {
+ return obj1;
+ }
+ return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+ }
+
}
diff --git a/src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java b/src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java
new file mode 100644
index 000000000..59bacb052
--- /dev/null
+++ b/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 {
+ }
+
+}
diff --git a/src/test/java/cn/iocoder/dashboard/BaseDbUnitTest.java b/src/test/java/cn/iocoder/dashboard/BaseDbUnitTest.java
new file mode 100644
index 000000000..821118279
--- /dev/null
+++ b/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 {
+ }
+
+}
diff --git a/src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java b/src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java
index a00fc472d..fe615dcaf 100644
--- a/src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java
+++ b/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
diff --git a/src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java b/src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java
index 1cddd9e1c..c93d766a4 100644
--- a/src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java
+++ b/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 {
diff --git a/src/test/java/cn/iocoder/dashboard/config/SecurityTestConfiguration.java b/src/test/java/cn/iocoder/dashboard/config/SecurityTestConfiguration.java
deleted file mode 100644
index 08738388a..000000000
--- a/src/test/java/cn/iocoder/dashboard/config/SecurityTestConfiguration.java
+++ /dev/null
@@ -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);
- }
-
-}
diff --git a/src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceTest.java
index 07691ba00..a01330527 100644
--- a/src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceTest.java
+++ b/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);
}
// ========== 随机对象 ==========
diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthServiceImplTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthServiceImplTest.java
index 5d9ac58a7..1be999efb 100644
--- a/src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthServiceImplTest.java
+++ b/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() {
diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataServiceTest.java
index 318a4afca..79c89518d 100644
--- a/src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataServiceTest.java
+++ b/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 labelDictDataCache =
+ (ImmutableTable) 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 valueDictDataCache =
+ (ImmutableTable) 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);
- }
-
- @Test
- public void testUpdateDictData_notExists() {
- // 准备参数
- SysDictDataUpdateReqVO reqVO = randomPojo(SysDictDataUpdateReqVO.class);
-
- // 调用, 并断言异常
- assertServiceException(() -> dictDataService.updateDictData(reqVO), DICT_DATA_NOT_EXISTS);
+ // 校验调用
+ verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
}
@Test
public void testDeleteDictData_success() {
// mock 数据
- SysDictDataDO dbDictData = randomPojo(SysDictDataDO.class);
+ SysDictDataDO dbDictData = randomDictDataDO();
dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbDictData.getId();
// 调用
dictDataService.deleteDictData(id);
- // 校验数据不存在了
- assertNull(dictDataMapper.selectById(id));
+ // 校验数据不存在了
+ assertNull(dictDataMapper.selectById(id));
+ // 校验调用
+ verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
}
@Test
- public void testDeleteDictData_notExists() {
- // 准备参数
- Long id = randomLongId();
+ public void testCheckDictDataExists_success() {
+ // mock 数据
+ 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.deleteDictData(id), DICT_DATA_NOT_EXISTS);
+ assertServiceException(() -> dictDataService.checkDictTypeValid(dictType), DICT_TYPE_NOT_ENABLE);
+ }
+
+ @Test
+ public void testCheckDictDataValueUnique_success() {
+ // 调用,成功
+ dictDataService.checkDictDataValueUnique(randomLongId(), randomString(), randomString());
+ }
+
+ @Test
+ public void testCheckDictDataValueUnique_valueDuplicateForCreate() {
+ // 准备参数
+ String dictType = randomString();
+ String value = randomString();
+ // mock 数据
+ dictDataMapper.insert(randomDictDataDO(o -> {
+ o.setDictType(dictType);
+ o.setValue(value);
+ }));
+
+ // 调用,校验异常
+ assertServiceException(() -> dictDataService.checkDictDataValueUnique(null, dictType, value),
+ DICT_DATA_VALUE_DUPLICATE);
+ }
+
+ @Test
+ 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.checkDictDataValueUnique(id, dictType, value),
+ DICT_DATA_VALUE_DUPLICATE);
+ }
+
+ // ========== 随机对象 ==========
+
+ @SafeVarargs
+ private static SysDictDataDO randomDictDataDO(Consumer... consumers) {
+ Consumer 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 是开启
+ });
}
}
diff --git a/src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictTypeServiceTest.java b/src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictTypeServiceTest.java
index e25fe2577..9dd072733 100644
--- a/src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictTypeServiceTest.java
+++ b/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 数据
@@ -186,33 +161,6 @@ public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
assertPojoEquals(reqVO, dictType);
}
- @Test
- public void testUpdateDictType_notExists() {
- // 准备参数
- SysDictTypeUpdateReqVO reqVO = randomPojo(SysDictTypeUpdateReqVO.class);
-
- // 调用, 并断言异常
- assertServiceException(() -> dictTypeService.updateDictType(reqVO), DICT_TYPE_NOT_EXISTS);
- }
-
- @Test
- public void testUpdateDictType_nameDuplicate() {
- // mock 数据,稍后更新它
- SysDictTypeDO dbDictType = randomDictTypeDO();
- dictTypeMapper.insert(dbDictType);
- // mock 数据,ks稍后模拟重复它的名字
- SysDictTypeDO nameDictType = randomDictTypeDO();
- dictTypeMapper.insert(nameDictType);
- // 准备参数
- SysDictTypeUpdateReqVO reqVO = randomPojo(SysDictTypeUpdateReqVO.class, o -> {
- o.setId(dbDictType.getId()); // 设置更新的 ID
- o.setName(nameDictType.getName()); // 模拟 name 重复
- });
-
- // 调用, 并断言异常
- assertServiceException(() -> dictTypeService.updateDictType(reqVO), DICT_TYPE_NAME_DUPLICATE);
- }
-
@Test
public void testDeleteDictType_success() {
// mock 数据
@@ -227,15 +175,6 @@ public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
assertNull(dictTypeMapper.selectById(id));
}
- @Test
- public void testDeleteDictType_notExists() {
- // 准备参数
- Long id = randomLongId();
-
- // 调用, 并断言异常
- assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_NOT_EXISTS);
- }
-
@Test
public void testDeleteDictType_hasChildren() {
// mock 数据
@@ -250,6 +189,83 @@ public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN);
}
+ @Test
+ 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() {
+ // 准备参数
+ String type = randomString();
+ // mock 数据
+ dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type)));
+
+ // 调用,校验异常
+ assertServiceException(() -> dictTypeService.checkDictTypeUnique(null, type),
+ DICT_TYPE_TYPE_DUPLICATE);
+ }
+
+ @Test
+ public void testCheckDictTypeUnique_valueDuplicateForUpdate() {
+ // 准备参数
+ Long id = randomLongId();
+ String type = randomString();
+ // mock 数据
+ dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type)));
+
+ // 调用,校验异常
+ assertServiceException(() -> dictTypeService.checkDictTypeUnique(id, type),
+ DICT_TYPE_TYPE_DUPLICATE);
+ }
+
+ @Test
+ public void testCheckDictTypNameUnique_success() {
+ // 调用,成功
+ dictTypeService.checkDictTypeNameUnique(randomLongId(), randomString());
+ }
+
+ @Test
+ public void testCheckDictTypeNameUnique_nameDuplicateForCreate() {
+ // 准备参数
+ String name = randomString();
+ // mock 数据
+ dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name)));
+
+ // 调用,校验异常
+ assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(null, name),
+ DICT_TYPE_NAME_DUPLICATE);
+ }
+
+ @Test
+ public void testCheckDictTypeNameUnique_nameDuplicateForUpdate() {
+ // 准备参数
+ Long id = randomLongId();
+ String name = randomString();
+ // mock 数据
+ dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name)));
+
+ // 调用,校验异常
+ assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(id, name),
+ DICT_TYPE_NAME_DUPLICATE);
+ }
+
// ========== 随机对象 ==========
@SafeVarargs
diff --git a/src/test/resources/application-unit-test.yaml b/src/test/resources/application-unit-test.yaml
index b8406322c..70cecc4c3 100644
--- a/src/test/resources/application-unit-test.yaml
+++ b/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
-
-# 日志文件配置(不需要配置)
-
--- #################### 芋道相关配置 ####################
# 芋道配置项,设置当前项目所有自定义的配置
diff --git a/src/test/resources/sql/create_tables.sql b/src/test/resources/sql/create_tables.sql
index 827d4923d..51bd2209e 100644
--- a/src/test/resources/sql/create_tables.sql
+++ b/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")