Commit bcb6a68b80110948aa345ebf8f2f6430edcda3e4

Authored by Lyon
2 parents 6163cfb4 d79bf795

Merge branch 'master' into feature_user-session-timeout

# Conflicts:
#	sql/ruoyi-vue-pro.sql
Showing 40 changed files with 625 additions and 252 deletions
README.md
... ... @@ -43,11 +43,12 @@
43 43 1. 幂等组件:基于 Redis 实现幂等组件,解决重复请求问题
44 44 1. 服务保障:基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能
45 45 1. 日志服务:轻量级日志中心,查看远程服务器的日志
  46 +1. 单元测试:基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等
46 47  
47 48 ### 研发工具
48 49  
49 50 1. 表单构建:拖动表单元素生成相应的 HTML 代码
50   -1. 代码生成:前后端代码的生成(Java、Vue、SQL),支持 CRUD 下载
  51 +1. 代码生成:前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载
51 52 1. 系统接口:基于 Swagger 自动生成相关的 RESTful API 接口文档
52 53 1. 数据库文档:基于 Screw 自动生成数据库文档
53 54  
... ... @@ -83,7 +84,9 @@
83 84 | [Spring Boot Admin](https://github.com/skywalking) | Spring Boot 监控平台 | 8.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
84 85 | [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.11.4 | |
85 86 | [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
86   -| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码| 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
  87 +| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
  88 +| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.7.0 | - |
  89 +| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 3.6.28 | - |
87 90  
88 91 **前端**
89 92  
... ... @@ -125,7 +128,7 @@
125 128 </tr>
126 129 <tr>
127 130 <td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
128   - <td><img src="https://oscimg.oschina.net/oscnet/up-6d73c2140ce694e3de4c05035fdc1868d4c.png"/></td>
  131 + <td> - </td>
129 132 </tr>
130 133 </table>
131 134  
... ...
... ... @@ -2,7 +2,7 @@
2 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
3 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5   - <modelVersion>4.0.0</modelVersion>
  5 + <modelVersion>4.0.0</modelVersion>
6 6  
7 7 <groupId>cn.iocoder</groupId>
8 8 <artifactId>dashboard</artifactId>
... ... @@ -25,6 +25,7 @@
25 25 <spring.boot.version>2.4.2</spring.boot.version>
26 26 <!-- Web 相关 -->
27 27 <knife4j.version>3.0.2</knife4j.version>
  28 + <swagger-annotations.version>1.5.22</swagger-annotations.version>
28 29 <!-- DB 相关 -->
29 30 <mysql-connector-java.version>5.1.46</mysql-connector-java.version>
30 31 <druid.version>1.2.4</druid.version>
... ... @@ -104,8 +105,17 @@
104 105 <artifactId>guava</artifactId>
105 106 <groupId>com.google.guava</groupId>
106 107 </exclusion>
  108 + <exclusion>
  109 + <artifactId>swagger-annotations</artifactId>
  110 + <groupId>io.swagger</groupId>
  111 + </exclusion>
107 112 </exclusions>
108 113 </dependency>
  114 + <dependency>
  115 + <groupId>io.swagger</groupId>
  116 + <artifactId>swagger-annotations</artifactId>
  117 + <version>${swagger-annotations.version}</version>
  118 + </dependency>
109 119  
110 120 <!-- DB 相关 -->
111 121 <dependency>
... ...
sql/ruoyi-vue-pro.sql
... ... @@ -11,7 +11,7 @@
11 11 Target Server Version : 50718
12 12 File Encoding : 65001
13 13  
14   - Date: 07/03/2021 00:43:34
  14 + Date: 08/03/2021 00:50:29
15 15 */
16 16  
17 17 SET NAMES utf8mb4;
... ... @@ -43,7 +43,7 @@ CREATE TABLE `inf_api_access_log` (
43 43 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
44 44 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
45 45 PRIMARY KEY (`id`) USING BTREE
46   -) ENGINE=InnoDB AUTO_INCREMENT=1822 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表';
  46 +) ENGINE=InnoDB AUTO_INCREMENT=1850 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表';
47 47  
48 48 -- ----------------------------
49 49 -- Records of inf_api_access_log
... ... @@ -175,7 +175,7 @@ CREATE TABLE `inf_job_log` (
175 175 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
176 176 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
177 177 PRIMARY KEY (`id`) USING BTREE
178   -) ENGINE=InnoDB AUTO_INCREMENT=4458 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表';
  178 +) ENGINE=InnoDB AUTO_INCREMENT=4477 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表';
179 179  
180 180 -- ----------------------------
181 181 -- Records of inf_job_log
... ... @@ -579,7 +579,7 @@ CREATE TABLE `sys_operate_log` (
579 579 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
580 580 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
581 581 PRIMARY KEY (`id`) USING BTREE
582   -) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COMMENT='æ“作日志记录';
  582 +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='æ“作日志记录';
583 583  
584 584 -- ----------------------------
585 585 -- Records of sys_operate_log
... ... @@ -977,13 +977,14 @@ INSERT INTO `sys_user_session` VALUES (&#39;e3ad1ef8b9aa4b329855b29c7b372e8f&#39;, 1, &#39;1
977 977 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');
978 978 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');
979 979 COMMIT;
980   --- ----------------------------null,
981   --- Table structure for tool_codegen_columnnull,
982   --- ----------------------------null,
983   -DROP TABLE IF EXISTS `tool_codegen_column`;null,
984   -CREATE TABLE `tool_codegen_column` (null,
985   - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ç¼–å·',null,
986   - `table_id` bigint(20) NOT NULL COMMENT '表编å·',null,
  980 +
  981 +-- ----------------------------
  982 +-- Table structure for tool_codegen_column
  983 +-- ----------------------------
  984 +DROP TABLE IF EXISTS `tool_codegen_column`;
  985 +CREATE TABLE `tool_codegen_column` (
  986 + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ç¼–å·',
  987 + `table_id` bigint(20) NOT NULL COMMENT '表编å·',
987 988 `column_name` varchar(200) NOT NULL COMMENT '字段å',
988 989 `column_type` varchar(100) NOT NULL COMMENT '字段类型',
989 990 `column_comment` varchar(500) NOT NULL COMMENT '字段æè¿°',
... ... @@ -1007,7 +1008,7 @@ CREATE TABLE `tool_codegen_column` (null,
1007 1008 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
1008 1009 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
1009 1010 PRIMARY KEY (`id`) USING BTREE
1010   -) ENGINE=InnoDB AUTO_INCREMENT=369 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å­—段定义';
  1011 +) ENGINE=InnoDB AUTO_INCREMENT=381 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å­—段定义';
1011 1012  
1012 1013 -- ----------------------------
1013 1014 -- Records of tool_codegen_column
... ... @@ -1149,6 +1150,18 @@ INSERT INTO `tool_codegen_column` VALUES (365, 29, &#39;create_time&#39;, &#39;datetime&#39;, &#39;å
1149 1150 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');
1150 1151 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');
1151 1152 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');
  1153 +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');
  1154 +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');
  1155 +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');
  1156 +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');
  1157 +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');
  1158 +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');
  1159 +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');
  1160 +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');
  1161 +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');
  1162 +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');
  1163 +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');
  1164 +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');
1152 1165 COMMIT;
1153 1166  
1154 1167 -- ----------------------------
... ... @@ -1174,7 +1187,7 @@ CREATE TABLE `tool_codegen_table` (
1174 1187 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
1175 1188 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
1176 1189 PRIMARY KEY (`id`) USING BTREE
1177   -) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å®šä¹‰';
  1190 +) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å®šä¹‰';
1178 1191  
1179 1192 -- ----------------------------
1180 1193 -- Records of tool_codegen_table
... ... @@ -1190,6 +1203,7 @@ INSERT INTO `tool_codegen_table` VALUES (26, 1, &#39;inf_api_access_log&#39;, &#39;API 访é—
1190 1203 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');
1191 1204 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');
1192 1205 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');
  1206 +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');
1193 1207 COMMIT;
1194 1208  
1195 1209 -- ----------------------------
... ...
src/main/java/cn/iocoder/dashboard/framework/mybatis/config/MybatisConfiguration.java
... ... @@ -13,7 +13,8 @@ import org.springframework.context.annotation.Configuration;
13 13 * @author 芋道源码
14 14 */
15 15 @Configuration
16   -@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class)
  16 +@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class,
  17 + lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
17 18 public class MybatisConfiguration {
18 19  
19 20 @Bean
... ...
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/dataobject/BaseDO.java
... ... @@ -21,13 +21,13 @@ public class BaseDO implements Serializable {
21 21 */
22 22 private Date updateTime;
23 23 /**
24   - * 创建者 TODO 芋艿:迁移成编号
  24 + * 创建者
25 25 */
26   - private String createBy;
  26 + private String creator;
27 27 /**
28   - * 更新者 TODO 芋艿:迁移成编号
  28 + * 更新者
29 29 */
30   - private String updateBy;
  30 + private String updater;
31 31 /**
32 32 * 是否删除
33 33 */
... ...
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/handle/DefaultDBFieldHandler.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.mybatis.core.handle;
  2 +
  3 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.dashboard.framework.security.core.LoginUser;
  5 +import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
  6 +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
  7 +import org.apache.ibatis.reflection.MetaObject;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.util.Date;
  11 +import java.util.Objects;
  12 +
  13 +/**
  14 + * 通用参数填充实现类
  15 + *
  16 + * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
  17 + *
  18 + * @author hexiaowu
  19 + */
  20 +@Component
  21 +public class DefaultDBFieldHandler implements MetaObjectHandler {
  22 +
  23 + @Override
  24 + public void insertFill(MetaObject metaObject) {
  25 + if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
  26 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
  27 + BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
  28 + Date current = new Date();
  29 +
  30 + // 创建时间为空,则以当前时间为插入时间
  31 + if (Objects.isNull(baseDO.getCreateTime())) {
  32 + baseDO.setCreateTime(current);
  33 + }
  34 + // 更新时间为空,则以当前时间为更新时间
  35 + if (Objects.isNull(baseDO.getUpdateTime())) {
  36 + baseDO.setUpdateTime(current);
  37 + }
  38 + // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
  39 + if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) {
  40 + baseDO.setCreator(loginUser.getId().toString());
  41 + }
  42 + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
  43 + if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) {
  44 + baseDO.setUpdater(loginUser.getId().toString());
  45 + }
  46 + }
  47 + }
  48 +
  49 + @Override
  50 + public void updateFill(MetaObject metaObject) {
  51 + Object modifyTime = getFieldValByName("updateTime", metaObject);
  52 + Object modifier = getFieldValByName("updater", metaObject);
  53 + // 获取登录用户信息
  54 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
  55 +
  56 + // 更新时间为空,则以当前时间为更新时间
  57 + if (Objects.isNull(modifyTime)) {
  58 + setFieldValByName("updateTime", new Date(), metaObject);
  59 + }
  60 + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
  61 + if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
  62 + setFieldValByName("updater", loginUser.getId(), metaObject);
  63 + }
  64 + }
  65 +}
... ...
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java
... ... @@ -28,6 +28,10 @@ public interface BaseMapperX&lt;T&gt; extends BaseMapper&lt;T&gt; {
28 28 return selectOne(new QueryWrapper<T>().eq(field, value));
29 29 }
30 30  
  31 + default Integer selectCount(String field, Object value) {
  32 + return selectCount(new QueryWrapper<T>().eq(field, value));
  33 + }
  34 +
31 35 default List<T> selectList() {
32 36 return selectList(new QueryWrapper<>());
33 37 }
... ...
src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 安全框架 Spring Security 入门》.md deleted
1   -<http://www.iocoder.cn/Spring-Boot/Spring-Security/?github>
src/main/java/cn/iocoder/dashboard/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md 0 → 100644
  1 +<https://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao>
... ...
src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
... ... @@ -152,7 +152,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
152 152 .anyRequest().authenticated()
153 153 .and()
154 154 .headers().frameOptions().disable();
155   - httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
  155 + httpSecurity.logout().logoutUrl(webProperties.getApiPrefix() + "/logout").logoutSuccessHandler(logoutSuccessHandler);
156 156 // 添加 JWT Filter
157 157 httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
158 158 }
... ...
src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
1 1 package cn.iocoder.dashboard.framework.security.core.handler;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.dashboard.common.pojo.CommonResult;
4 5 import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
5 6 import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
6 7 import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
... ... @@ -36,6 +37,6 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
36 37 securityFrameworkService.logout(token);
37 38 }
38 39 // 返回成功
39   - ServletUtils.writeJSON(response, null);
  40 + ServletUtils.writeJSON(response, CommonResult.success(null));
40 41 }
41 42 }
... ...
src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityFrameworkUtils.java
... ... @@ -2,7 +2,10 @@ package cn.iocoder.dashboard.framework.security.core.util;
2 2  
3 3 import cn.iocoder.dashboard.framework.security.core.LoginUser;
4 4 import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
  5 +import org.springframework.lang.Nullable;
5 6 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  7 +import org.springframework.security.core.Authentication;
  8 +import org.springframework.security.core.context.SecurityContext;
6 9 import org.springframework.security.core.context.SecurityContextHolder;
7 10 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
8 11 import org.springframework.util.StringUtils;
... ... @@ -40,9 +43,20 @@ public class SecurityFrameworkUtils {
40 43  
41 44 /**
42 45 * 获取当前用户
  46 + *
  47 + * @return 当前用户
43 48 */
  49 + @Nullable
44 50 public static LoginUser getLoginUser() {
45   - return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  51 + SecurityContext context = SecurityContextHolder.getContext();
  52 + if (context == null) {
  53 + return null;
  54 + }
  55 + Authentication authentication = context.getAuthentication();
  56 + if (authentication == null) {
  57 + return null;
  58 + }
  59 + return (LoginUser) authentication.getPrincipal();
46 60 }
47 61  
48 62 /**
... ... @@ -50,8 +64,10 @@ public class SecurityFrameworkUtils {
50 64 *
51 65 * @return 用户编号
52 66 */
  67 + @Nullable
53 68 public static Long getLoginUserId() {
54   - return getLoginUser().getId();
  69 + LoginUser loginUser = getLoginUser();
  70 + return loginUser != null ? loginUser.getId() : null;
55 71 }
56 72  
57 73 /**
... ... @@ -59,8 +75,10 @@ public class SecurityFrameworkUtils {
59 75 *
60 76 * @return 角色编号数组
61 77 */
  78 + @Nullable
62 79 public static Set<Long> getLoginUserRoleIds() {
63   - return getLoginUser().getRoleIds();
  80 + LoginUser loginUser = getLoginUser();
  81 + return loginUser != null ? loginUser.getRoleIds() : null;
64 82 }
65 83  
66 84 /**
... ...
src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
... ... @@ -10,15 +10,17 @@ import org.springframework.context.annotation.Configuration;
10 10 import org.springframework.http.HttpHeaders;
11 11 import springfox.documentation.builders.ApiInfoBuilder;
12 12 import springfox.documentation.builders.PathSelectors;
13   -import springfox.documentation.service.*;
  13 +import springfox.documentation.service.ApiInfo;
  14 +import springfox.documentation.service.ApiKey;
  15 +import springfox.documentation.service.AuthorizationScope;
  16 +import springfox.documentation.service.Contact;
  17 +import springfox.documentation.service.SecurityReference;
  18 +import springfox.documentation.service.SecurityScheme;
14 19 import springfox.documentation.spi.DocumentationType;
15 20 import springfox.documentation.spi.service.contexts.SecurityContext;
16 21 import springfox.documentation.spring.web.plugins.Docket;
17 22 import springfox.documentation.swagger2.annotations.EnableSwagger2;
18   -import springfox.documentation.service.ApiKey;
19 23  
20   -import java.util.ArrayList;
21   -import java.util.Arrays;
22 24 import java.util.Collections;
23 25 import java.util.List;
24 26  
... ...
src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java
... ... @@ -27,9 +27,10 @@ public class WebConfiguration implements WebMvcConfigurer {
27 27  
28 28 @Override
29 29 public void configurePathMatch(PathMatchConfigurer configurer) {
  30 + // 设置 API 前缀,仅仅匹配 controller 包下的
30 31 configurer.addPathPrefix(webProperties.getApiPrefix(), clazz ->
31 32 clazz.isAnnotationPresent(RestController.class)
32   - && clazz.getPackage().getName().startsWith(webProperties.getControllerPackage()));
  33 + && clazz.getPackage().getName().startsWith(webProperties.getControllerPackage())); // 仅仅匹配 controller 包
33 34 }
34 35  
35 36 // ========== Filter 相关 ==========
... ...
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;
12 12 import cn.iocoder.dashboard.modules.infra.enums.config.InfConfigTypeEnum;
13 13 import cn.iocoder.dashboard.modules.infra.mq.producer.config.InfConfigProducer;
14 14 import cn.iocoder.dashboard.modules.infra.service.config.InfConfigService;
  15 +import com.google.common.annotations.VisibleForTesting;
15 16 import lombok.extern.slf4j.Slf4j;
16 17 import org.springframework.stereotype.Service;
17 18  
... ... @@ -99,7 +100,8 @@ public class InfConfigServiceImpl implements InfConfigService {
99 100 checkConfigKeyUnique(id, key);
100 101 }
101 102  
102   - private InfConfigDO checkConfigExists(Long id) {
  103 + @VisibleForTesting
  104 + public InfConfigDO checkConfigExists(Long id) {
103 105 if (id == null) {
104 106 return null;
105 107 }
... ... @@ -110,7 +112,8 @@ public class InfConfigServiceImpl implements InfConfigService {
110 112 return config;
111 113 }
112 114  
113   - private void checkConfigKeyUnique(Long id, String key) {
  115 + @VisibleForTesting
  116 + public void checkConfigKeyUnique(Long id, String key) {
114 117 InfConfigDO config = configMapper.selectByKey(key);
115 118 if (config == null) {
116 119 return;
... ...
src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/dept/SysDeptBaseVO.java
... ... @@ -26,7 +26,7 @@ public class SysDeptBaseVO {
26 26  
27 27 @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
28 28 @NotBlank(message = "显示顺序不能为空")
29   - private String sort;
  29 + private Integer sort;
30 30  
31 31 @ApiModelProperty(value = "负责人", example = "芋道")
32 32 private String leader;
... ...
src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostBaseVO.java
... ... @@ -25,7 +25,7 @@ public class SysPostBaseVO {
25 25  
26 26 @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
27 27 @NotBlank(message = "显示顺序不能为空")
28   - private String sort;
  28 + private Integer sort;
29 29  
30 30 @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
31 31 private Integer status;
... ...
src/main/java/cn/iocoder/dashboard/modules/system/controller/dept/vo/post/SysPostExcelVO.java
... ... @@ -23,7 +23,7 @@ public class SysPostExcelVO {
23 23 private String name;
24 24  
25 25 @ExcelProperty("岗位排序")
26   - private String sort;
  26 + private Integer sort;
27 27  
28 28 @ExcelProperty(value = "状态", converter = DictConvert.class)
29 29 @DictFormat(SYS_COMMON_STATUS)
... ...
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/menu/SysMenuBaseVO.java
... ... @@ -29,7 +29,7 @@ public class SysMenuBaseVO {
29 29  
30 30 @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
31 31 @NotBlank(message = "显示顺序不能为空")
32   - private String sort;
  32 + private Integer sort;
33 33  
34 34 @ApiModelProperty(value = "父菜单 ID", required = true, example = "1024")
35 35 @NotNull(message = "父菜单 ID 不能为空")
... ...
src/main/java/cn/iocoder/dashboard/modules/system/controller/permission/vo/role/SysRoleBaseVO.java
... ... @@ -25,7 +25,7 @@ public class SysRoleBaseVO {
25 25  
26 26 @ApiModelProperty(value = "显示顺序不能为空", required = true, example = "1024")
27 27 @NotBlank(message = "显示顺序不能为空")
28   - private String sort;
  28 + private Integer sort;
29 29  
30 30 @ApiModelProperty(value = "角色类型", required = true, example = "1", notes = "见 SysRoleTypeEnum 枚举")
31 31 private Integer type;
... ...
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysDeptDO.java
... ... @@ -35,7 +35,7 @@ public class SysDeptDO extends BaseDO {
35 35 /**
36 36 * 显示顺序
37 37 */
38   - private String sort;
  38 + private Integer sort;
39 39 /**
40 40 * 负责人
41 41 */
... ...
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/dept/SysPostDO.java
... ... @@ -34,7 +34,7 @@ public class SysPostDO extends BaseDO {
34 34 /**
35 35 * 岗位排序
36 36 */
37   - private String sort;
  37 + private Integer sort;
38 38 /**
39 39 * 状态
40 40 *
... ...
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/permission/SysMenuDO.java
... ... @@ -49,7 +49,7 @@ public class SysMenuDO extends BaseDO {
49 49 /**
50 50 * 显示顺序
51 51 */
52   - private String sort;
  52 + private Integer sort;
53 53 /**
54 54 * 父菜单ID
55 55 */
... ...
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dict/SysDictDataMapper.java
... ... @@ -15,13 +15,13 @@ import java.util.List;
15 15 @Mapper
16 16 public interface SysDictDataMapper extends BaseMapperX<SysDictDataDO> {
17 17  
18   - default SysDictDataDO selectByDictTypeAndLabel(String dictType, String label) {
  18 + default SysDictDataDO selectByDictTypeAndValue(String dictType, String value) {
19 19 return selectOne(new QueryWrapper<SysDictDataDO>().eq("dict_type", dictType)
20   - .eq("label", label));
  20 + .eq("value", value));
21 21 }
22 22  
23 23 default int selectCountByDictType(String dictType) {
24   - return selectCount(new QueryWrapper<SysDictDataDO>().eq("dict_type", dictType));
  24 + return selectCount("dict_type", dictType);
25 25 }
26 26  
27 27 default PageResult<SysDictDataDO> selectPage(SysDictDataPageReqVO reqVO) {
... ...
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java
... ... @@ -160,7 +160,26 @@ public class SysAuthServiceImpl implements SysAuthService {
160 160  
161 161 @Override
162 162 public void logout(String token) {
163   -// AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功")); TODO 需要搞一搞
  163 + // 查询用户信息
  164 + LoginUser loginUser = userSessionService.getLoginUser(token);
  165 + if (loginUser == null) {
  166 + return;
  167 + }
  168 + // 删除 session
  169 + userSessionService.deleteUserSession(token);
  170 + // 记录登出日子和
  171 + this.createLogoutLog(loginUser.getUsername());
  172 + }
  173 +
  174 + private void createLogoutLog(String username) {
  175 + SysLoginLogCreateReqVO reqVO = new SysLoginLogCreateReqVO();
  176 + reqVO.setLogType(SysLoginLogTypeEnum.LOGOUT_SELF.getType());
  177 + reqVO.setTraceId(TracerUtils.getTraceId());
  178 + reqVO.setUsername(username);
  179 + reqVO.setUserAgent(ServletUtils.getUserAgent());
  180 + reqVO.setUserIp(ServletUtils.getClientIP());
  181 + reqVO.setResult(SysLoginResultEnum.SUCCESS.getResult());
  182 + loginLogService.createLoginLog(reqVO);
164 183 }
165 184  
166 185 @Override
... ...
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;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
5   -import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
6 5 import cn.iocoder.dashboard.common.pojo.PageResult;
7 6 import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
8 7 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
10 9 import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataPageReqVO;
11 10 import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataUpdateReqVO;
12 11 import cn.iocoder.dashboard.modules.system.convert.dict.SysDictDataConvert;
13   -import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictDataMapper;
14 12 import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
15 13 import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictTypeDO;
  14 +import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictDataMapper;
16 15 import cn.iocoder.dashboard.modules.system.mq.producer.dict.SysDictDataProducer;
17 16 import cn.iocoder.dashboard.modules.system.service.dict.SysDictDataService;
18 17 import cn.iocoder.dashboard.modules.system.service.dict.SysDictTypeService;
  18 +import com.google.common.annotations.VisibleForTesting;
19 19 import com.google.common.collect.ImmutableTable;
20 20 import lombok.extern.slf4j.Slf4j;
21 21 import org.springframework.scheduling.annotation.Scheduled;
... ... @@ -28,6 +28,7 @@ import java.util.Comparator;
28 28 import java.util.Date;
29 29 import java.util.List;
30 30  
  31 +import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
31 32 import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
32 33  
33 34 /**
... ... @@ -156,7 +157,7 @@ public class SysDictDataServiceImpl implements SysDictDataService {
156 157 @Override
157 158 public Long createDictData(SysDictDataCreateReqVO reqVO) {
158 159 // 校验正确性
159   - this.checkCreateOrUpdate(null, reqVO.getLabel(), reqVO.getDictType());
  160 + this.checkCreateOrUpdate(null, reqVO.getValue(), reqVO.getDictType());
160 161 // 插入字典类型
161 162 SysDictDataDO dictData = SysDictDataConvert.INSTANCE.convert(reqVO);
162 163 dictDataMapper.insert(dictData);
... ... @@ -168,7 +169,7 @@ public class SysDictDataServiceImpl implements SysDictDataService {
168 169 @Override
169 170 public void updateDictData(SysDictDataUpdateReqVO reqVO) {
170 171 // 校验正确性
171   - this.checkCreateOrUpdate(reqVO.getId(), reqVO.getLabel(), reqVO.getDictType());
  172 + this.checkCreateOrUpdate(reqVO.getId(), reqVO.getValue(), reqVO.getDictType());
172 173 // 更新字典类型
173 174 SysDictDataDO updateObj = SysDictDataConvert.INSTANCE.convert(reqVO);
174 175 dictDataMapper.updateById(updateObj);
... ... @@ -191,46 +192,49 @@ public class SysDictDataServiceImpl implements SysDictDataService {
191 192 return dictDataMapper.selectCountByDictType(dictType);
192 193 }
193 194  
194   - private void checkCreateOrUpdate(Long id, String label, String dictType) {
  195 + private void checkCreateOrUpdate(Long id, String value, String dictType) {
195 196 // 校验自己存在
196 197 checkDictDataExists(id);
197 198 // 校验字典类型有效
198 199 checkDictTypeValid(dictType);
199 200 // 校验字典数据的值的唯一性
200   - checkDictDataValueUnique(id, dictType, label);
  201 + checkDictDataValueUnique(id, dictType, value);
201 202 }
202 203  
203   - private void checkDictDataValueUnique(Long id, String dictType, String label) {
204   - SysDictDataDO dictData = dictDataMapper.selectByDictTypeAndLabel(dictType, label);
  204 + @VisibleForTesting
  205 + public void checkDictDataValueUnique(Long id, String dictType, String value) {
  206 + SysDictDataDO dictData = dictDataMapper.selectByDictTypeAndValue(dictType, value);
205 207 if (dictData == null) {
206 208 return;
207 209 }
208 210 // 如果 id 为空,说明不用比较是否为相同 id 的字典数据
209 211 if (id == null) {
210   - throw ServiceExceptionUtil.exception(DICT_DATA_VALUE_DUPLICATE);
  212 + throw exception(DICT_DATA_VALUE_DUPLICATE);
211 213 }
212 214 if (!dictData.getId().equals(id)) {
213   - throw ServiceExceptionUtil.exception(DICT_DATA_VALUE_DUPLICATE);
  215 + throw exception(DICT_DATA_VALUE_DUPLICATE);
214 216 }
215 217 }
216 218  
217   - private void checkDictDataExists(Long id) {
  219 + @VisibleForTesting
  220 + public void checkDictDataExists(Long id) {
218 221 if (id == null) {
219 222 return;
220 223 }
221 224 SysDictDataDO dictData = dictDataMapper.selectById(id);
222 225 if (dictData == null) {
223   - throw ServiceExceptionUtil.exception(DICT_DATA_NOT_EXISTS);
  226 + throw exception(DICT_DATA_NOT_EXISTS);
224 227 }
225 228 }
226 229  
227   - private void checkDictTypeValid(String type) {
  230 + @VisibleForTesting
  231 + public void checkDictTypeValid(String type) {
228 232 SysDictTypeDO dictType = dictTypeService.getDictType(type);
229 233 if (dictType == null) {
230   - throw ServiceExceptionUtil.exception(DICT_TYPE_NOT_EXISTS);
  234 + throw exception(DICT_TYPE_NOT_EXISTS);
231 235 }
232 236 if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) {
233   - throw ServiceExceptionUtil.exception(DICT_TYPE_NOT_ENABLE);
  237 + throw exception(DICT_TYPE_NOT_ENABLE);
234 238 }
235 239 }
236 240  
... ...
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;
10 10 import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictTypeMapper;
11 11 import cn.iocoder.dashboard.modules.system.service.dict.SysDictDataService;
12 12 import cn.iocoder.dashboard.modules.system.service.dict.SysDictTypeService;
  13 +import com.google.common.annotations.VisibleForTesting;
13 14 import org.springframework.stereotype.Service;
14 15  
15 16 import javax.annotation.Resource;
... ... @@ -97,8 +98,9 @@ public class SysDictTypeServiceImpl implements SysDictTypeService {
97 98 checkDictTypeUnique(id, type);
98 99 }
99 100  
100   - private void checkDictTypeNameUnique(Long id, String type) {
101   - SysDictTypeDO dictType = dictTypeMapper.selectByName(type);
  101 + @VisibleForTesting
  102 + public void checkDictTypeNameUnique(Long id, String name) {
  103 + SysDictTypeDO dictType = dictTypeMapper.selectByName(name);
102 104 if (dictType == null) {
103 105 return;
104 106 }
... ... @@ -111,7 +113,8 @@ public class SysDictTypeServiceImpl implements SysDictTypeService {
111 113 }
112 114 }
113 115  
114   - private void checkDictTypeUnique(Long id, String type) {
  116 + @VisibleForTesting
  117 + public void checkDictTypeUnique(Long id, String type) {
115 118 SysDictTypeDO dictType = dictTypeMapper.selectByType(type);
116 119 if (dictType == null) {
117 120 return;
... ... @@ -125,7 +128,8 @@ public class SysDictTypeServiceImpl implements SysDictTypeService {
125 128 }
126 129 }
127 130  
128   - private SysDictTypeDO checkDictTypeExists(Long id) {
  131 + @VisibleForTesting
  132 + public SysDictTypeDO checkDictTypeExists(Long id) {
129 133 if (id == null) {
130 134 return null;
131 135 }
... ...
src/main/java/cn/iocoder/dashboard/util/collection/ArrayUtils.java
... ... @@ -2,6 +2,8 @@ package cn.iocoder.dashboard.util.collection;
2 2  
3 3 import cn.hutool.core.util.ArrayUtil;
4 4  
  5 +import java.util.function.Consumer;
  6 +
5 7 /**
6 8 * Array 工具类
7 9 *
... ... @@ -18,11 +20,11 @@ public class ArrayUtils {
18 20 * @return 结果数组
19 21 */
20 22 @SafeVarargs
21   - public static <T> T[] append(T object, T... newElements) {
  23 + public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) {
22 24 if (object == null) {
23 25 return newElements;
24 26 }
25   - T[] result = ArrayUtil.newArray(object.getClass(), 1 + newElements.length);
  27 + Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length);
26 28 result[0] = object;
27 29 System.arraycopy(newElements, 0, result, 1, newElements.length);
28 30 return result;
... ...
src/main/java/cn/iocoder/dashboard/util/object/ObjectUtils.java
... ... @@ -19,4 +19,14 @@ public class ObjectUtils {
19 19 return result;
20 20 }
21 21  
  22 + public static <T extends Comparable<T>> T max(T obj1, T obj2) {
  23 + if (obj1 == null) {
  24 + return obj2;
  25 + }
  26 + if (obj2 == null) {
  27 + return obj1;
  28 + }
  29 + return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
  30 + }
  31 +
22 32 }
... ...
src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java 0 → 100644
  1 +package cn.iocoder.dashboard;
  2 +
  3 +import cn.iocoder.dashboard.config.RedisTestConfiguration;
  4 +import cn.iocoder.dashboard.framework.datasource.config.DataSourceConfiguration;
  5 +import cn.iocoder.dashboard.framework.mybatis.config.MybatisConfiguration;
  6 +import cn.iocoder.dashboard.framework.redis.config.RedisConfig;
  7 +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
  8 +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
  9 +import org.redisson.spring.starter.RedissonAutoConfiguration;
  10 +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
  11 +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  12 +import org.springframework.boot.test.context.SpringBootTest;
  13 +import org.springframework.context.annotation.Import;
  14 +import org.springframework.test.context.ActiveProfiles;
  15 +import org.springframework.test.context.jdbc.Sql;
  16 +
  17 +/**
  18 + * 依赖内存 DB 的单元测试
  19 + *
  20 + * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
  25 +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
  26 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
  27 +public class BaseDbAndRedisUnitTest {
  28 +
  29 + @Import({
  30 + // DB 配置类
  31 + DataSourceConfiguration.class, // 自己的 DB 配置类
  32 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类
  33 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类
  34 + // MyBatis 配置类
  35 + MybatisConfiguration.class, // 自己的 MyBatis 配置类
  36 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
  37 + // Redis 配置类
  38 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
  39 + RedisAutoConfiguration.class, // Spring Redis 自动配置类
  40 + RedisConfig.class, // 自己的 Redis 配置类
  41 + RedissonAutoConfiguration.class, // Redisson 自动高配置类
  42 + })
  43 + public static class Application {
  44 + }
  45 +
  46 +}
... ...
src/test/java/cn/iocoder/dashboard/BaseDbUnitTest.java 0 → 100644
  1 +package cn.iocoder.dashboard;
  2 +
  3 +import cn.iocoder.dashboard.framework.datasource.config.DataSourceConfiguration;
  4 +import cn.iocoder.dashboard.framework.mybatis.config.MybatisConfiguration;
  5 +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
  6 +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
  7 +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  8 +import org.springframework.boot.test.context.SpringBootTest;
  9 +import org.springframework.context.annotation.Import;
  10 +import org.springframework.test.context.ActiveProfiles;
  11 +import org.springframework.test.context.jdbc.Sql;
  12 +
  13 +/**
  14 + * 依赖内存 DB 的单元测试
  15 + *
  16 + * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class)
  21 +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
  22 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
  23 +public class BaseDbUnitTest {
  24 +
  25 + @Import({
  26 + // DB 配置类
  27 + DataSourceConfiguration.class, // 自己的 DB 配置类
  28 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类
  29 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类
  30 + // MyBatis 配置类
  31 + MybatisConfiguration.class, // 自己的 MyBatis 配置类
  32 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
  33 + })
  34 + public static class Application {
  35 + }
  36 +
  37 +}
... ...
src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java
... ... @@ -12,6 +12,7 @@ import javax.annotation.Resource;
12 12 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
13 13 @ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
14 14 @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
  15 +@Deprecated
15 16 public class BaseSpringBootUnitTest {
16 17  
17 18 @Resource
... ...
src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java
... ... @@ -8,12 +8,10 @@ import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
8 8 import org.springframework.boot.context.properties.EnableConfigurationProperties;
9 9 import org.springframework.context.annotation.Bean;
10 10 import org.springframework.context.annotation.Configuration;
11   -import org.springframework.context.annotation.Lazy;
12 11  
13 12 import java.io.IOException;
14 13  
15 14 @Configuration(proxyBeanMethods = false)
16   -@Lazy(false) // 禁用懒加载,因为需要保证 Redis Server 必须先启动
17 15 @EnableConfigurationProperties(RedisProperties.class)
18 16 @AutoConfigureBefore({RedisAutoConfiguration.class, RedissonAutoConfiguration.class}) // 在 Redis 自动配置前,进行初始化
19 17 public class RedisTestConfiguration {
... ...
src/test/java/cn/iocoder/dashboard/config/SecurityTestConfiguration.java deleted
1   -package cn.iocoder.dashboard.config;
2   -
3   -import org.mockito.Mockito;
4   -import org.springframework.context.annotation.Bean;
5   -import org.springframework.context.annotation.Configuration;
6   -import org.springframework.security.authentication.AuthenticationManager;
7   -
8   -@Configuration
9   -public class SecurityTestConfiguration {
10   -
11   - @Bean
12   - public AuthenticationManager authenticationManager() {
13   - return Mockito.mock(AuthenticationManager.class);
14   - }
15   -
16   -}
src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceTest.java
1 1 package cn.iocoder.dashboard.modules.infra.service.config;
2 2  
3   -import cn.iocoder.dashboard.BaseSpringBootUnitTest;
  3 +import cn.iocoder.dashboard.BaseDbUnitTest;
4 4 import cn.iocoder.dashboard.common.pojo.PageResult;
5 5 import cn.iocoder.dashboard.modules.infra.controller.config.vo.InfConfigCreateReqVO;
6 6 import cn.iocoder.dashboard.modules.infra.controller.config.vo.InfConfigExportReqVO;
... ... @@ -15,6 +15,7 @@ import cn.iocoder.dashboard.util.collection.ArrayUtils;
15 15 import cn.iocoder.dashboard.util.object.ObjectUtils;
16 16 import org.junit.jupiter.api.Test;
17 17 import org.springframework.boot.test.mock.mockito.MockBean;
  18 +import org.springframework.context.annotation.Import;
18 19  
19 20 import javax.annotation.Resource;
20 21 import java.util.List;
... ... @@ -24,8 +25,7 @@ import static cn.hutool.core.util.RandomUtil.randomEle;
24 25 import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.*;
25 26 import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
26 27 import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
27   -import static cn.iocoder.dashboard.util.RandomUtils.randomLongId;
28   -import static cn.iocoder.dashboard.util.RandomUtils.randomPojo;
  28 +import static cn.iocoder.dashboard.util.RandomUtils.*;
29 29 import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
30 30 import static org.junit.jupiter.api.Assertions.*;
31 31 import static org.mockito.Mockito.times;
... ... @@ -36,7 +36,8 @@ import static org.mockito.Mockito.verify;
36 36 *
37 37 * @author 芋道源码
38 38 */
39   -public class InfConfigServiceTest extends BaseSpringBootUnitTest {
  39 +@Import(InfConfigServiceImpl.class)
  40 +public class InfConfigServiceTest extends BaseDbUnitTest {
40 41  
41 42 @Resource
42 43 private InfConfigServiceImpl configService;
... ... @@ -146,19 +147,6 @@ public class InfConfigServiceTest extends BaseSpringBootUnitTest {
146 147 }
147 148  
148 149 @Test
149   - public void testCreateConfig_keyDuplicate() {
150   - // 准备参数
151   - InfConfigCreateReqVO reqVO = randomPojo(InfConfigCreateReqVO.class);
152   - // mock 数据
153   - configMapper.insert(randomInfConfigDO(o -> { // @Sql
154   - o.setKey(reqVO.getKey()); // 模拟 key 重复
155   - }));
156   -
157   - // 调用, 并断言异常
158   - assertServiceException(() -> configService.createConfig(reqVO), CONFIG_KEY_DUPLICATE);
159   - }
160   -
161   - @Test
162 150 public void testUpdateConfig_success() {
163 151 // mock 数据
164 152 InfConfigDO dbConfig = randomInfConfigDO();
... ... @@ -178,15 +166,6 @@ public class InfConfigServiceTest extends BaseSpringBootUnitTest {
178 166 }
179 167  
180 168 @Test
181   - public void testUpdateConfig_notExists() {
182   - // 准备参数
183   - InfConfigUpdateReqVO reqVO = randomPojo(InfConfigUpdateReqVO.class);
184   -
185   - // 调用, 并断言异常
186   - assertServiceException(() -> configService.updateConfig(reqVO), CONFIG_NOT_EXISTS);
187   - }
188   -
189   - @Test
190 169 public void testDeleteConfig_success() {
191 170 // mock 数据
192 171 InfConfigDO dbConfig = randomInfConfigDO(o -> {
... ... @@ -219,12 +198,49 @@ public class InfConfigServiceTest extends BaseSpringBootUnitTest {
219 198 }
220 199  
221 200 @Test
222   - public void testDeleteConfig_notExists() {
  201 + public void testCheckConfigExists_success() {
  202 + // mock 数据
  203 + InfConfigDO dbConfigDO = randomInfConfigDO();
  204 + configMapper.insert(dbConfigDO);// @Sql: 先插入出一条存在的数据
  205 +
  206 + // 调用成功
  207 + configService.checkConfigExists(dbConfigDO.getId());
  208 + }
  209 +
  210 + @Test
  211 + public void testCheckConfigExist_notExists() {
  212 + assertServiceException(() -> configService.checkConfigExists(randomLongId()), CONFIG_NOT_EXISTS);
  213 + }
  214 +
  215 + @Test
  216 + public void testCheckConfigKeyUnique_success() {
  217 + // 调用,成功
  218 + configService.checkConfigKeyUnique(randomLongId(), randomString());
  219 + }
  220 +
  221 + @Test
  222 + public void testCheckConfigKeyUnique_keyDuplicateForCreate() {
  223 + // 准备参数
  224 + String key = randomString();
  225 + // mock 数据
  226 + configMapper.insert(randomInfConfigDO(o -> o.setKey(key)));
  227 +
  228 + // 调用,校验异常
  229 + assertServiceException(() -> configService.checkConfigKeyUnique(null, key),
  230 + CONFIG_KEY_DUPLICATE);
  231 + }
  232 +
  233 + @Test
  234 + public void testCheckConfigKeyUnique_keyDuplicateForUpdate() {
223 235 // 准备参数
224 236 Long id = randomLongId();
  237 + String key = randomString();
  238 + // mock 数据
  239 + configMapper.insert(randomInfConfigDO(o -> o.setKey(key)));
225 240  
226   - // 调用, 并断言异常
227   - assertServiceException(() -> configService.deleteConfig(id), CONFIG_NOT_EXISTS);
  241 + // 调用,校验异常
  242 + assertServiceException(() -> configService.checkConfigKeyUnique(id, key),
  243 + CONFIG_KEY_DUPLICATE);
228 244 }
229 245  
230 246 // ========== 随机对象 ==========
... ...
src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthServiceImplTest.java
1 1 package cn.iocoder.dashboard.modules.system.service.auth;
2 2  
3   -import cn.iocoder.dashboard.BaseSpringBootUnitTest;
  3 +import cn.iocoder.dashboard.BaseDbUnitTest;
4 4 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.dashboard.framework.security.core.LoginUser;
6 6 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
7 7 import cn.iocoder.dashboard.modules.system.service.auth.impl.SysAuthServiceImpl;
  8 +import cn.iocoder.dashboard.modules.system.service.common.SysCaptchaService;
  9 +import cn.iocoder.dashboard.modules.system.service.logger.SysLoginLogService;
8 10 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
9 11 import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
10 12 import cn.iocoder.dashboard.util.AssertUtils;
11 13 import org.junit.jupiter.api.Test;
12 14 import org.springframework.boot.test.mock.mockito.MockBean;
  15 +import org.springframework.context.annotation.Import;
  16 +import org.springframework.security.authentication.AuthenticationManager;
13 17 import org.springframework.security.core.userdetails.UsernameNotFoundException;
14 18  
15 19 import javax.annotation.Resource;
... ... @@ -21,7 +25,13 @@ import static org.junit.jupiter.api.Assertions.*;
21 25 import static org.mockito.ArgumentMatchers.eq;
22 26 import static org.mockito.Mockito.when;
23 27  
24   -public class SysAuthServiceImplTest extends BaseSpringBootUnitTest {
  28 +/**
  29 + * {@link SysAuthServiceImpl} 的单元测试
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Import(SysAuthServiceImpl.class)
  34 +public class SysAuthServiceImplTest extends BaseDbUnitTest {
25 35  
26 36 @Resource
27 37 private SysAuthServiceImpl authService;
... ... @@ -30,6 +40,14 @@ public class SysAuthServiceImplTest extends BaseSpringBootUnitTest {
30 40 private SysUserService userService;
31 41 @MockBean
32 42 private SysPermissionService permissionService;
  43 + @MockBean
  44 + private AuthenticationManager authenticationManager;
  45 + @MockBean
  46 + private SysCaptchaService captchaService;
  47 + @MockBean
  48 + private SysLoginLogService loginLogService;
  49 + @MockBean
  50 + private SysUserSessionService userSessionService;
33 51  
34 52 @Test
35 53 public void testLoadUserByUsername_success() {
... ...
src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataServiceTest.java
1 1 package cn.iocoder.dashboard.modules.system.service.dict;
2 2  
3   -import cn.iocoder.dashboard.BaseSpringBootUnitTest;
  3 +import cn.iocoder.dashboard.BaseDbUnitTest;
4 4 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.dashboard.common.pojo.PageResult;
6 6 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;
12 12 import cn.iocoder.dashboard.modules.system.dal.mysql.dict.SysDictDataMapper;
13 13 import cn.iocoder.dashboard.modules.system.mq.producer.dict.SysDictDataProducer;
14 14 import cn.iocoder.dashboard.modules.system.service.dict.impl.SysDictDataServiceImpl;
  15 +import cn.iocoder.dashboard.util.collection.ArrayUtils;
15 16 import cn.iocoder.dashboard.util.object.ObjectUtils;
  17 +import com.google.common.collect.ImmutableTable;
16 18 import org.junit.jupiter.api.Test;
17 19 import org.springframework.boot.test.mock.mockito.MockBean;
  20 +import org.springframework.context.annotation.Import;
18 21  
19 22 import javax.annotation.Resource;
  23 +import java.util.Date;
20 24 import java.util.List;
  25 +import java.util.function.Consumer;
21 26  
22   -import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.DICT_DATA_NOT_EXISTS;
  27 +import static cn.hutool.core.bean.BeanUtil.getFieldValue;
  28 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
23 29 import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
24 30 import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
25 31 import static cn.iocoder.dashboard.util.RandomUtils.*;
... ... @@ -32,7 +38,8 @@ import static org.mockito.Mockito.*;
32 38 *
33 39 * @author 芋道源码
34 40 */
35   -public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
  41 +@Import(SysDictDataServiceImpl.class)
  42 +public class SysDictDataServiceTest extends BaseDbUnitTest {
36 43  
37 44 @Resource
38 45 private SysDictDataServiceImpl dictDataService;
... ... @@ -44,6 +51,37 @@ public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
44 51 @MockBean
45 52 private SysDictDataProducer dictDataProducer;
46 53  
  54 + /**
  55 + * 测试加载到新的字典数据的情况
  56 + */
  57 + @Test
  58 + @SuppressWarnings("unchecked")
  59 + public void testInitLocalCache() {
  60 + // mock 数据
  61 + SysDictDataDO dictData01 = randomDictDataDO();
  62 + dictDataMapper.insert(dictData01);
  63 + SysDictDataDO dictData02 = randomDictDataDO();
  64 + dictDataMapper.insert(dictData02);
  65 +
  66 + // 调用
  67 + dictDataService.initLocalCache();
  68 + // 断言 labelDictDataCache 缓存
  69 + ImmutableTable<String, String, SysDictDataDO> labelDictDataCache =
  70 + (ImmutableTable<String, String, SysDictDataDO>) getFieldValue(dictDataService, "labelDictDataCache");
  71 + assertEquals(2, labelDictDataCache.size());
  72 + assertPojoEquals(dictData01, labelDictDataCache.get(dictData01.getDictType(), dictData01.getLabel()));
  73 + assertPojoEquals(dictData02, labelDictDataCache.get(dictData02.getDictType(), dictData02.getLabel()));
  74 + // 断言 valueDictDataCache 缓存
  75 + ImmutableTable<String, String, SysDictDataDO> valueDictDataCache =
  76 + (ImmutableTable<String, String, SysDictDataDO>) getFieldValue(dictDataService, "valueDictDataCache");
  77 + assertEquals(2, valueDictDataCache.size());
  78 + assertPojoEquals(dictData01, valueDictDataCache.get(dictData01.getDictType(), dictData01.getValue()));
  79 + assertPojoEquals(dictData02, valueDictDataCache.get(dictData02.getDictType(), dictData02.getValue()));
  80 + // 断言 maxUpdateTime 缓存
  81 + Date maxUpdateTime = (Date) getFieldValue(dictDataService, "maxUpdateTime");
  82 + assertEquals(ObjectUtils.max(dictData01.getUpdateTime(), dictData02.getUpdateTime()), maxUpdateTime);
  83 + }
  84 +
47 85 @Test
48 86 public void testGetDictDataPage() {
49 87 // mock 数据
... ... @@ -107,8 +145,7 @@ public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
107 145 SysDictDataCreateReqVO reqVO = randomPojo(SysDictDataCreateReqVO.class,
108 146 o -> o.setStatus(randomCommonStatus()));
109 147 // mock 方法
110   - when(dictTypeService.getDictType(eq(reqVO.getDictType())))
111   - .thenReturn(randomPojo(SysDictTypeDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
  148 + when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType()));
112 149  
113 150 // 调用
114 151 Long dictDataId = dictDataService.createDictData(reqVO);
... ... @@ -124,50 +161,142 @@ public class SysDictDataServiceTest extends BaseSpringBootUnitTest {
124 161 @Test
125 162 public void testUpdateDictData_success() {
126 163 // mock 数据
127   - SysDictDataDO dbDictData = randomPojo(SysDictDataDO.class);
  164 + SysDictDataDO dbDictData = randomDictDataDO();
128 165 dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据
129 166 // 准备参数
130 167 SysDictDataUpdateReqVO reqVO = randomPojo(SysDictDataUpdateReqVO.class, o -> {
131 168 o.setId(dbDictData.getId()); // 设置更新的 ID
  169 + o.setStatus(randomCommonStatus());
132 170 });
  171 + // mock 方法,字典类型
  172 + when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType()));
133 173  
134 174 // 调用
135 175 dictDataService.updateDictData(reqVO);
136 176 // 校验是否更新正确
137 177 SysDictDataDO dictData = dictDataMapper.selectById(reqVO.getId()); // 获取最新的
138 178 assertPojoEquals(reqVO, dictData);
  179 + // 校验调用
  180 + verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
139 181 }
140 182  
141 183 @Test
142   - public void testUpdateDictData_notExists() {
  184 + public void testDeleteDictData_success() {
  185 + // mock 数据
  186 + SysDictDataDO dbDictData = randomDictDataDO();
  187 + dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据
143 188 // 准备参数
144   - SysDictDataUpdateReqVO reqVO = randomPojo(SysDictDataUpdateReqVO.class);
  189 + Long id = dbDictData.getId();
145 190  
146   - // 调用, 并断言异常
147   - assertServiceException(() -> dictDataService.updateDictData(reqVO), DICT_DATA_NOT_EXISTS);
  191 + // 调用
  192 + dictDataService.deleteDictData(id);
  193 + // 校验数据不存在了
  194 + assertNull(dictDataMapper.selectById(id));
  195 + // 校验调用
  196 + verify(dictDataProducer, times(1)).sendDictDataRefreshMessage();
148 197 }
149 198  
150 199 @Test
151   - public void testDeleteDictData_success() {
  200 + public void testCheckDictDataExists_success() {
152 201 // mock 数据
153   - SysDictDataDO dbDictData = randomPojo(SysDictDataDO.class);
  202 + SysDictDataDO dbDictData = randomDictDataDO();
154 203 dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据
  204 +
  205 + // 调用成功
  206 + dictDataService.checkDictDataExists(dbDictData.getId());
  207 + }
  208 +
  209 + @Test
  210 + public void testCheckDictDataExists_notExists() {
  211 + assertServiceException(() -> dictDataService.checkDictDataExists(randomLongId()), DICT_DATA_NOT_EXISTS);
  212 + }
  213 +
  214 + @Test
  215 + public void testCheckDictTypeValid_success() {
  216 + // mock 方法,数据类型被禁用
  217 + String type = randomString();
  218 + when(dictTypeService.getDictType(eq(type))).thenReturn(randomDictTypeDO(type));
  219 +
  220 + // 调用, 成功
  221 + dictDataService.checkDictTypeValid(type);
  222 + }
  223 +
  224 + @Test
  225 + public void testCheckDictTypeValid_notExists() {
  226 + assertServiceException(() -> dictDataService.checkDictTypeValid(randomString()), DICT_TYPE_NOT_EXISTS);
  227 + }
  228 +
  229 + @Test
  230 + public void testCheckDictTypeValid_notEnable() {
  231 + // mock 方法,数据类型被禁用
  232 + String dictType = randomString();
  233 + when(dictTypeService.getDictType(eq(dictType))).thenReturn(
  234 + randomPojo(SysDictTypeDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
  235 +
  236 + // 调用, 并断言异常
  237 + assertServiceException(() -> dictDataService.checkDictTypeValid(dictType), DICT_TYPE_NOT_ENABLE);
  238 + }
  239 +
  240 + @Test
  241 + public void testCheckDictDataValueUnique_success() {
  242 + // 调用,成功
  243 + dictDataService.checkDictDataValueUnique(randomLongId(), randomString(), randomString());
  244 + }
  245 +
  246 + @Test
  247 + public void testCheckDictDataValueUnique_valueDuplicateForCreate() {
155 248 // 准备参数
156   - Long id = dbDictData.getId();
  249 + String dictType = randomString();
  250 + String value = randomString();
  251 + // mock 数据
  252 + dictDataMapper.insert(randomDictDataDO(o -> {
  253 + o.setDictType(dictType);
  254 + o.setValue(value);
  255 + }));
157 256  
158   - // 调用
159   - dictDataService.deleteDictData(id);
160   - // 校验数据不存在了
161   - assertNull(dictDataMapper.selectById(id));
  257 + // 调用,校验异常
  258 + assertServiceException(() -> dictDataService.checkDictDataValueUnique(null, dictType, value),
  259 + DICT_DATA_VALUE_DUPLICATE);
162 260 }
163 261  
164 262 @Test
165   - public void testDeleteDictData_notExists() {
  263 + public void testCheckDictDataValueUnique_valueDuplicateForUpdate() {
166 264 // 准备参数
167 265 Long id = randomLongId();
  266 + String dictType = randomString();
  267 + String value = randomString();
  268 + // mock 数据
  269 + dictDataMapper.insert(randomDictDataDO(o -> {
  270 + o.setDictType(dictType);
  271 + o.setValue(value);
  272 + }));
168 273  
169   - // 调用, 并断言异常
170   - assertServiceException(() -> dictDataService.deleteDictData(id), DICT_DATA_NOT_EXISTS);
  274 + // 调用,校验异常
  275 + assertServiceException(() -> dictDataService.checkDictDataValueUnique(id, dictType, value),
  276 + DICT_DATA_VALUE_DUPLICATE);
  277 + }
  278 +
  279 + // ========== 随机对象 ==========
  280 +
  281 + @SafeVarargs
  282 + private static SysDictDataDO randomDictDataDO(Consumer<SysDictDataDO>... consumers) {
  283 + Consumer<SysDictDataDO> consumer = (o) -> {
  284 + o.setStatus(randomCommonStatus()); // 保证 status 的范围
  285 + };
  286 + return randomPojo(SysDictDataDO.class, ArrayUtils.append(consumer, consumers));
  287 + }
  288 +
  289 + /**
  290 + * 生成一个有效的字典类型
  291 + *
  292 + * @param type 字典类型
  293 + * @return SysDictTypeDO 对象
  294 + */
  295 + private static SysDictTypeDO randomDictTypeDO(String type) {
  296 + return randomPojo(SysDictTypeDO.class, o -> {
  297 + o.setType(type);
  298 + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 是开启
  299 + });
171 300 }
172 301  
173 302 }
... ...
src/test/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictTypeServiceTest.java
1 1 package cn.iocoder.dashboard.modules.system.service.dict;
2 2  
3   -import cn.iocoder.dashboard.BaseSpringBootUnitTest;
  3 +import cn.iocoder.dashboard.BaseDbUnitTest;
4 4 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.dashboard.common.pojo.PageResult;
6 6 import cn.iocoder.dashboard.modules.system.controller.dict.vo.type.SysDictTypeCreateReqVO;
... ... @@ -14,6 +14,7 @@ import cn.iocoder.dashboard.util.collection.ArrayUtils;
14 14 import cn.iocoder.dashboard.util.object.ObjectUtils;
15 15 import org.junit.jupiter.api.Test;
16 16 import org.springframework.boot.test.mock.mockito.MockBean;
  17 +import org.springframework.context.annotation.Import;
17 18  
18 19 import javax.annotation.Resource;
19 20 import java.util.List;
... ... @@ -23,8 +24,7 @@ import static cn.hutool.core.util.RandomUtil.randomEle;
23 24 import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
24 25 import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
25 26 import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
26   -import static cn.iocoder.dashboard.util.RandomUtils.randomLongId;
27   -import static cn.iocoder.dashboard.util.RandomUtils.randomPojo;
  27 +import static cn.iocoder.dashboard.util.RandomUtils.*;
28 28 import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
29 29 import static org.junit.jupiter.api.Assertions.*;
30 30 import static org.mockito.ArgumentMatchers.eq;
... ... @@ -35,7 +35,8 @@ import static org.mockito.Mockito.when;
35 35 *
36 36 * @author 芋道源码
37 37 */
38   -public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
  38 +@Import(SysDictTypeServiceImpl.class)
  39 +public class SysDictTypeServiceTest extends BaseDbUnitTest {
39 40  
40 41 @Resource
41 42 private SysDictTypeServiceImpl dictTypeService;
... ... @@ -143,32 +144,6 @@ public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
143 144 }
144 145  
145 146 @Test
146   - public void testCreateDictType_nameDuplicate() {
147   - // mock 数据
148   - SysDictTypeDO dbDictType = randomDictTypeDO();
149   - dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
150   - // 准备参数
151   - SysDictTypeCreateReqVO reqVO = randomPojo(SysDictTypeCreateReqVO.class,
152   - o -> o.setName(dbDictType.getName())); // 模拟 name 重复
153   -
154   - // 调用, 并断言异常
155   - assertServiceException(() -> dictTypeService.createDictType(reqVO), DICT_TYPE_NAME_DUPLICATE);
156   - }
157   -
158   - @Test
159   - public void testCreateDictType_typeDuplicate() {
160   - // mock 数据
161   - SysDictTypeDO dbDictType = randomDictTypeDO();
162   - dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
163   - // 准备参数
164   - SysDictTypeCreateReqVO reqVO = randomPojo(SysDictTypeCreateReqVO.class,
165   - o -> o.setType(dbDictType.getType())); // 模拟 type 重复
166   -
167   - // 调用, 并断言异常
168   - assertServiceException(() -> dictTypeService.createDictType(reqVO), DICT_TYPE_TYPE_DUPLICATE);
169   - }
170   -
171   - @Test
172 147 public void testUpdateDictType_success() {
173 148 // mock 数据
174 149 SysDictTypeDO dbDictType = randomDictTypeDO();
... ... @@ -187,67 +162,108 @@ public class SysDictTypeServiceTest extends BaseSpringBootUnitTest {
187 162 }
188 163  
189 164 @Test
190   - public void testUpdateDictType_notExists() {
  165 + public void testDeleteDictType_success() {
  166 + // mock 数据
  167 + SysDictTypeDO dbDictType = randomDictTypeDO();
  168 + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
191 169 // 准备参数
192   - SysDictTypeUpdateReqVO reqVO = randomPojo(SysDictTypeUpdateReqVO.class);
  170 + Long id = dbDictType.getId();
193 171  
194   - // 调用, 并断言异常
195   - assertServiceException(() -> dictTypeService.updateDictType(reqVO), DICT_TYPE_NOT_EXISTS);
  172 + // 调用
  173 + dictTypeService.deleteDictType(id);
  174 + // 校验数据不存在了
  175 + assertNull(dictTypeMapper.selectById(id));
196 176 }
197 177  
198 178 @Test
199   - public void testUpdateDictType_nameDuplicate() {
200   - // mock 数据,稍后更新它
  179 + public void testDeleteDictType_hasChildren() {
  180 + // mock 数据
201 181 SysDictTypeDO dbDictType = randomDictTypeDO();
202   - dictTypeMapper.insert(dbDictType);
203   - // mock 数据,ks稍后模拟重复它的名字
204   - SysDictTypeDO nameDictType = randomDictTypeDO();
205   - dictTypeMapper.insert(nameDictType);
  182 + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
206 183 // 准备参数
207   - SysDictTypeUpdateReqVO reqVO = randomPojo(SysDictTypeUpdateReqVO.class, o -> {
208   - o.setId(dbDictType.getId()); // 设置更新的 ID
209   - o.setName(nameDictType.getName()); // 模拟 name 重复
210   - });
  184 + Long id = dbDictType.getId();
  185 + // mock 方法
  186 + when(dictDataService.countByDictType(eq(dbDictType.getType()))).thenReturn(1);
211 187  
212 188 // 调用, 并断言异常
213   - assertServiceException(() -> dictTypeService.updateDictType(reqVO), DICT_TYPE_NAME_DUPLICATE);
  189 + assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN);
214 190 }
215 191  
216 192 @Test
217   - public void testDeleteDictType_success() {
  193 + public void testCheckDictDataExists_success() {
218 194 // mock 数据
219 195 SysDictTypeDO dbDictType = randomDictTypeDO();
220 196 dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
  197 +
  198 + // 调用成功
  199 + dictTypeService.checkDictTypeExists(dbDictType.getId());
  200 + }
  201 +
  202 + @Test
  203 + public void testCheckDictDataExists_notExists() {
  204 + assertServiceException(() -> dictTypeService.checkDictTypeExists(randomLongId()), DICT_TYPE_NOT_EXISTS);
  205 + }
  206 +
  207 + @Test
  208 + public void testCheckDictTypeUnique_success() {
  209 + // 调用,成功
  210 + dictTypeService.checkDictTypeUnique(randomLongId(), randomString());
  211 + }
  212 +
  213 + @Test
  214 + public void testCheckDictTypeUnique_valueDuplicateForCreate() {
221 215 // 准备参数
222   - Long id = dbDictType.getId();
  216 + String type = randomString();
  217 + // mock 数据
  218 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type)));
223 219  
224   - // 调用
225   - dictTypeService.deleteDictType(id);
226   - // 校验数据不存在了
227   - assertNull(dictTypeMapper.selectById(id));
  220 + // 调用,校验异常
  221 + assertServiceException(() -> dictTypeService.checkDictTypeUnique(null, type),
  222 + DICT_TYPE_TYPE_DUPLICATE);
228 223 }
229 224  
230 225 @Test
231   - public void testDeleteDictType_notExists() {
  226 + public void testCheckDictTypeUnique_valueDuplicateForUpdate() {
232 227 // 准备参数
233 228 Long id = randomLongId();
  229 + String type = randomString();
  230 + // mock 数据
  231 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type)));
234 232  
235   - // 调用, 并断言异常
236   - assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_NOT_EXISTS);
  233 + // 调用,校验异常
  234 + assertServiceException(() -> dictTypeService.checkDictTypeUnique(id, type),
  235 + DICT_TYPE_TYPE_DUPLICATE);
237 236 }
238 237  
239 238 @Test
240   - public void testDeleteDictType_hasChildren() {
  239 + public void testCheckDictTypNameUnique_success() {
  240 + // 调用,成功
  241 + dictTypeService.checkDictTypeNameUnique(randomLongId(), randomString());
  242 + }
  243 +
  244 + @Test
  245 + public void testCheckDictTypeNameUnique_nameDuplicateForCreate() {
  246 + // 准备参数
  247 + String name = randomString();
241 248 // mock 数据
242   - SysDictTypeDO dbDictType = randomDictTypeDO();
243   - dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据
  249 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name)));
  250 +
  251 + // 调用,校验异常
  252 + assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(null, name),
  253 + DICT_TYPE_NAME_DUPLICATE);
  254 + }
  255 +
  256 + @Test
  257 + public void testCheckDictTypeNameUnique_nameDuplicateForUpdate() {
244 258 // 准备参数
245   - Long id = dbDictType.getId();
246   - // mock 方法
247   - when(dictDataService.countByDictType(eq(dbDictType.getType()))).thenReturn(1);
  259 + Long id = randomLongId();
  260 + String name = randomString();
  261 + // mock 数据
  262 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name)));
248 263  
249   - // 调用, 并断言异常
250   - assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN);
  264 + // 调用,校验异常
  265 + assertServiceException(() -> dictTypeService.checkDictTypeNameUnique(id, name),
  266 + DICT_TYPE_NAME_DUPLICATE);
251 267 }
252 268  
253 269 // ========== 随机对象 ==========
... ...
src/test/resources/application-unit-test.yaml
... ... @@ -3,21 +3,6 @@ spring:
3 3 lazy-initialization: true # 开启懒加载,加快速度
4 4 banner-mode: off # 单元测试,禁用 Banner
5 5  
6   - # 去除的自动配置项
7   - autoconfigure:
8   - exclude:
9   - - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration # 单元测试,禁用 SpringSecurity
10   - - org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration # 单元测试,禁用 SpringSecurity
11   - - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 单元测试,禁用 Quartz
12   - - com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration # 单元测试,禁用 Lock4j 分布式锁
13   - - org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration # 单元测试,禁用 Scheduler 定时任务
14   - - org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration # 项目没有使用 Spring Data,所以禁用配置类,加速启动
15   -
16   -# Swagger 接口文档的自动配置(单元测试,禁用 Swagger)
17   -springfox:
18   - documentation:
19   - auto-startup: false
20   -
21 6 --- #################### 数据库相关配置 ####################
22 7  
23 8 spring:
... ... @@ -29,6 +14,9 @@ spring:
29 14 username: sa
30 15 password:
31 16 schema: classpath:sql/create_tables.sql # MySQL 转 H2 的语句,使用 https://www.jooq.org/translate/ 工具
  17 + druid:
  18 + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
  19 + initial-size: 1 # 单元测试,配置为 1,提升启动速度
32 20  
33 21 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
34 22 redis:
... ... @@ -36,17 +24,13 @@ spring:
36 24 port: 16379 # 端口(单元测试,使用 16379 端口)
37 25 database: 0 # 数据库索引
38 26  
39   ---- #################### 定时任务相关配置 ####################
  27 +mybatis:
  28 + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
40 29  
41   -# Quartz 配置项,对应 QuartzProperties 配置类(单元测试,禁用 Quartz)
  30 +--- #################### 定时任务相关配置 ####################
42 31  
43 32 --- #################### 配置中心相关配置 ####################
44 33  
45   -# Apollo 配置中心
46   -apollo:
47   - bootstrap:
48   - enabled: false # 单元测试,禁用配置中心
49   -
50 34 --- #################### 服务保障相关配置 ####################
51 35  
52 36 # Lock4j 配置项(单元测试,禁用 Lock4j)
... ... @@ -63,23 +47,6 @@ resilience4j:
63 47  
64 48 --- #################### 监控相关配置 ####################
65 49  
66   -# Actuator 监控端点的配置项
67   -management:
68   - endpoints:
69   - enabled-by-default: false
70   -
71   -# Spring Boot Admin 配置项
72   -spring:
73   - boot:
74   - admin:
75   - # Spring Boot Admin Client 客户端的相关配置
76   - client:
77   - enabled: false
78   - # Spring Boot Admin Server 服务端的相关配置
79   - context-path: /admin # 配置 Spring
80   -
81   -# 日志文件配置(不需要配置)
82   -
83 50 --- #################### 芋道相关配置 ####################
84 51  
85 52 # 芋道配置项,设置当前项目所有自定义的配置
... ...
src/test/resources/sql/create_tables.sql
... ... @@ -9,9 +9,9 @@ CREATE TABLE IF NOT EXISTS &quot;inf_config&quot; (
9 9 "value" varchar(500) NOT NULL DEFAULT '',
10 10 "sensitive" bit NOT NULL,
11 11 "remark" varchar(500) DEFAULT NULL,
12   - "create_by" varchar(64) DEFAULT '',
  12 + "creator" varchar(64) DEFAULT '',
13 13 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
14   - "update_by" varchar(64) DEFAULT '',
  14 + "updater" varchar(64) DEFAULT '',
15 15 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
16 16 "deleted" bit NOT NULL DEFAULT FALSE,
17 17 PRIMARY KEY ("id")
... ... @@ -28,9 +28,9 @@ CREATE TABLE IF NOT EXISTS &quot;sys_dept&quot; (
28 28 "phone" varchar(11) DEFAULT NULL,
29 29 "email" varchar(50) DEFAULT NULL,
30 30 "status" tinyint NOT NULL,
31   - "create_by" varchar(64) DEFAULT '',
  31 + "creator" varchar(64) DEFAULT '',
32 32 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
33   - "update_by" varchar(64) DEFAULT '',
  33 + "updater" varchar(64) DEFAULT '',
34 34 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
35 35 "deleted" bit NOT NULL DEFAULT FALSE,
36 36 PRIMARY KEY ("id")
... ... @@ -44,9 +44,9 @@ CREATE TABLE IF NOT EXISTS &quot;sys_dict_data&quot; (
44 44 "dict_type" varchar(100) NOT NULL DEFAULT '',
45 45 "status" tinyint NOT NULL DEFAULT '0',
46 46 "remark" varchar(500) DEFAULT NULL,
47   - "create_by" varchar(64) DEFAULT '',
  47 + "creator" varchar(64) DEFAULT '',
48 48 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
49   - "update_by" varchar(64) DEFAULT '',
  49 + "updater" varchar(64) DEFAULT '',
50 50 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
51 51 "deleted" bit NOT NULL DEFAULT FALSE,
52 52 PRIMARY KEY ("id")
... ... @@ -62,9 +62,9 @@ CREATE TABLE IF NOT EXISTS &quot;sys_role&quot; (
62 62 "status" tinyint NOT NULL,
63 63 "type" tinyint NOT NULL,
64 64 "remark" varchar(500) DEFAULT NULL,
65   - "create_by" varchar(64) DEFAULT '',
  65 + "creator" varchar(64) DEFAULT '',
66 66 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
67   - "update_by" varchar(64) DEFAULT '',
  67 + "updater" varchar(64) DEFAULT '',
68 68 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
69 69 "deleted" bit NOT NULL DEFAULT FALSE,
70 70 PRIMARY KEY ("id")
... ... @@ -74,9 +74,9 @@ CREATE TABLE IF NOT EXISTS &quot;sys_role_menu&quot; (
74 74 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
75 75 "role_id" bigint NOT NULL,
76 76 "menu_id" bigint NOT NULL,
77   - "create_by" varchar(64) DEFAULT '',
  77 + "creator" varchar(64) DEFAULT '',
78 78 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
79   - "update_by" varchar(64) DEFAULT '',
  79 + "updater" varchar(64) DEFAULT '',
80 80 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
81 81 "deleted" bit NOT NULL DEFAULT FALSE,
82 82 PRIMARY KEY ("id")
... ... @@ -93,9 +93,9 @@ CREATE TABLE IF NOT EXISTS &quot;sys_menu&quot; (
93 93 "icon" varchar(100) DEFAULT '#',
94 94 "component" varchar(255) DEFAULT NULL,
95 95 "status" tinyint NOT NULL DEFAULT '0',
96   - "create_by" varchar(64) DEFAULT '',
  96 + "creator" varchar(64) DEFAULT '',
97 97 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
98   - "update_by" varchar(64) DEFAULT '',
  98 + "updater" varchar(64) DEFAULT '',
99 99 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
100 100 "deleted" bit NOT NULL DEFAULT FALSE,
101 101 PRIMARY KEY ("id")
... ... @@ -107,9 +107,9 @@ CREATE TABLE &quot;sys_dict_type&quot; (
107 107 "type" varchar(100) NOT NULL DEFAULT '',
108 108 "status" tinyint NOT NULL DEFAULT '0',
109 109 "remark" varchar(500) DEFAULT NULL,
110   - "create_by" varchar(64) DEFAULT '',
  110 + "creator" varchar(64) DEFAULT '',
111 111 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
112   - "update_by" varchar(64) DEFAULT '',
  112 + "updater" varchar(64) DEFAULT '',
113 113 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
114 114 "deleted" bit NOT NULL DEFAULT FALSE,
115 115 PRIMARY KEY ("id")
... ...