Browse Source

Merge branch 'master' of https://github.com/YunaiV/ruoyi-vue-pro

YunaiV 2 years ago
parent
commit
8f333562af
34 changed files with 5800 additions and 707 deletions
  1. 120 41
      sql/mysql/ruoyi-vue-pro.sql
  2. 290 148
      sql/oracle/ruoyi-vue-pro.sql
  3. 324 329
      sql/postgresql/ruoyi-vue-pro.sql
  4. 3536 1
      sql/sqlserver/ruoyi-vue-pro.sql
  5. 9 6
      yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java
  6. 1 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/sql.vm
  7. 0 15
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http
  8. 4 56
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java
  9. 14 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http
  10. 80 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.java
  11. 3 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java
  12. 3 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java
  13. 3 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java
  14. 0 14
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java
  15. 25 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2UserConvert.java
  16. 1 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java
  17. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java
  18. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java
  19. 9 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImpl.java
  20. 6 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java
  21. 2 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeService.java
  22. 2 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImpl.java
  23. 7 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java
  24. 330 0
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java
  25. 267 0
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java
  26. 62 7
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImplTest.java
  27. 100 0
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java
  28. 165 0
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImplTest.java
  29. 289 0
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java
  30. 60 52
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceTest.java
  31. 4 0
      yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql
  32. 71 1
      yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql
  33. 1 1
      yudao-ui-admin/src/views/sso.vue
  34. 10 4
      yudao-ui-admin/src/views/system/oauth2/client/index.vue

+ 120 - 41
sql/mysql/ruoyi-vue-pro.sql

@@ -11,7 +11,7 @@
  Target Server Version : 80026
  File Encoding         : 65001
 
- Date: 13/05/2022 00:25:55
+ Date: 25/05/2022 23:28:25
 */
 
 SET NAMES utf8mb4;
@@ -300,7 +300,7 @@ CREATE TABLE `bpm_form`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的表单定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的表单定义';
 
 -- ----------------------------
 -- Records of bpm_form
@@ -359,7 +359,7 @@ CREATE TABLE `bpm_process_definition_ext`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 98 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Bpm 流程定义的拓展表\n';
+) ENGINE = InnoDB AUTO_INCREMENT = 104 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Bpm 流程定义的拓展表\n';
 
 -- ----------------------------
 -- Records of bpm_process_definition_ext
@@ -389,7 +389,7 @@ CREATE TABLE `bpm_process_instance_ext`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 201 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的流程实例的拓展';
+) ENGINE = InnoDB AUTO_INCREMENT = 204 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的流程实例的拓展';
 
 -- ----------------------------
 -- Records of bpm_process_instance_ext
@@ -415,7 +415,7 @@ CREATE TABLE `bpm_task_assign_rule`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 192 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Bpm 任务规则表';
+) ENGINE = InnoDB AUTO_INCREMENT = 201 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Bpm 任务规则表';
 
 -- ----------------------------
 -- Records of bpm_task_assign_rule
@@ -444,7 +444,7 @@ CREATE TABLE `bpm_task_ext`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 214 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的流程任务的拓展表';
+) ENGINE = InnoDB AUTO_INCREMENT = 217 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '工作流的流程任务的拓展表';
 
 -- ----------------------------
 -- Records of bpm_task_ext
@@ -504,7 +504,7 @@ CREATE TABLE `infra_api_access_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 29228 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表';
+) ENGINE = InnoDB AUTO_INCREMENT = 33232 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表';
 
 -- ----------------------------
 -- Records of infra_api_access_log
@@ -546,7 +546,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 429 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 454 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -673,7 +673,7 @@ CREATE TABLE `infra_data_source_config`  (
   `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 = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据源配置表';
+) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据源配置表';
 
 -- ----------------------------
 -- Records of infra_data_source_config
@@ -698,7 +698,7 @@ CREATE TABLE `infra_file`  (
   `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 = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 83 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file
@@ -1153,11 +1153,11 @@ CREATE TABLE `system_dept`  (
 -- ----------------------------
 BEGIN;
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-14 01:04:05', b'0', 1);
-INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '深圳总公司', 100, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-02-22 19:47:48', b'0', 1);
+INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '深圳总公司', 100, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-05-16 20:25:23', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (102, '长沙分公司', 100, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:40', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, '研发部门', 101, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-14 01:04:14', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, '市场部门', 101, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:38', b'0', 1);
-INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (105, '测试部门', 101, 3, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:37', b'0', 1);
+INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (105, '测试部门', 101, 3, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-05-16 20:25:15', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (106, '财务部门', 101, 4, 103, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-15 21:32:22', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, '运维部门', 101, 5, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:33', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, '市场部门', 102, 1, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-02-16 08:35:45', b'0', 1);
@@ -1186,7 +1186,7 @@ CREATE TABLE `system_dict_data`  (
   `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 = 1161 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1162 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
 
 -- ----------------------------
 -- Records of system_dict_data
@@ -1346,13 +1346,13 @@ CREATE TABLE `system_dict_type`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE INDEX `dict_type`(`type` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 148 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
+) ENGINE = InnoDB AUTO_INCREMENT = 149 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
 
 -- ----------------------------
 -- Records of system_dict_type
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '用户性别', 'system_user_sex', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:30:31', b'0');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '用户性别', 'system_user_sex', 0, NULL, 'admin', '2021-01-05 17:03:48', '1', '2022-05-16 20:29:32', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, '参数类型', 'infra_config_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:36:54', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, '通知类型', 'system_notice_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:35:26', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, '操作类型', 'system_operate_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:32:21', b'0');
@@ -1360,7 +1360,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 'Boolean 是否类型', 'infra_boolean_string', 0, 'boolean 转是否', '', '2021-01-19 03:20:08', '', '2022-02-01 16:37:10', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (104, '登陆结果', 'system_login_result', 0, '登陆结果', '', '2021-01-18 06:17:11', '', '2022-02-01 16:36:00', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (105, 'Redis 超时类型', 'infra_redis_timeout_type', 0, 'RedisKeyDefine.TimeoutTypeEnum', '', '2021-01-26 00:52:50', '', '2022-02-01 16:50:29', b'0');
-INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (106, '代码生成模板类型', 'infra_codegen_template_type', 0, NULL, '', '2021-02-05 07:08:06', '', '2022-03-10 16:33:42', b'0');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (106, '代码生成模板类型', 'infra_codegen_template_type', 0, NULL, '', '2021-02-05 07:08:06', '1', '2022-05-16 20:26:50', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (107, '定时任务状态', 'infra_job_status', 0, NULL, '', '2021-02-07 07:44:16', '', '2022-02-01 16:51:11', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (108, '定时任务日志状态', 'infra_job_log_status', 0, NULL, '', '2021-02-08 10:03:51', '', '2022-02-01 16:50:43', b'0');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (109, '用户类型', 'user_type', 0, NULL, '', '2021-02-26 00:15:51', '', '2021-02-26 00:15:51', b'0');
@@ -1411,7 +1411,7 @@ CREATE TABLE `system_error_code`  (
   `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 = 5458 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '错误码表';
+) ENGINE = InnoDB AUTO_INCREMENT = 5829 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '错误码表';
 
 -- ----------------------------
 -- Records of system_error_code
@@ -1440,7 +1440,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1341 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 1416 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -1744,10 +1744,11 @@ DROP TABLE IF EXISTS `system_oauth2_access_token`;
 CREATE TABLE `system_oauth2_access_token`  (
   `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
   `user_id` bigint NOT NULL COMMENT '用户编号',
+  `user_type` tinyint NOT NULL COMMENT '用户类型',
   `access_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问令牌',
   `refresh_token` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '刷新令牌',
-  `user_type` tinyint NOT NULL COMMENT '用户类型',
   `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号',
+  `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权范围',
   `expires_time` datetime NOT NULL COMMENT '过期时间',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -1756,7 +1757,7 @@ CREATE TABLE `system_oauth2_access_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 172 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -1764,6 +1765,33 @@ CREATE TABLE `system_oauth2_access_token`  (
 BEGIN;
 COMMIT;
 
+-- ----------------------------
+-- Table structure for system_oauth2_approve
+-- ----------------------------
+DROP TABLE IF EXISTS `system_oauth2_approve`;
+CREATE TABLE `system_oauth2_approve`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `user_id` bigint NOT NULL COMMENT '用户编号',
+  `user_type` tinyint NOT NULL COMMENT '用户类型',
+  `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号',
+  `scope` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '授权范围',
+  `approved` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否接受',
+  `expires_time` datetime NOT NULL COMMENT '过期时间',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 80 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 批准表';
+
+-- ----------------------------
+-- Records of system_oauth2_approve
+-- ----------------------------
+BEGIN;
+COMMIT;
+
 -- ----------------------------
 -- Table structure for system_oauth2_client
 -- ----------------------------
@@ -1779,9 +1807,9 @@ CREATE TABLE `system_oauth2_client`  (
   `access_token_validity_seconds` int NOT NULL COMMENT '访问令牌的有效期',
   `refresh_token_validity_seconds` int NOT NULL COMMENT '刷新令牌的有效期',
   `redirect_uris` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '可重定向的 URI 地址',
-  `auto_approve` bit(1) NOT NULL COMMENT '是否自动授权',
   `authorized_grant_types` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '授权类型',
   `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权范围',
+  `auto_approve_scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '自动通过的授权范围',
   `authorities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '权限',
   `resource_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '资源',
   `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '附加信息',
@@ -1797,8 +1825,37 @@ CREATE TABLE `system_oauth2_client`  (
 -- Records of system_oauth2_client
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `auto_approve`, `authorized_grant_types`, `scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yudao.iocoder.cn/a5e2e244368878a366b516805a4aabf1.png', '我是描述', 0, 999999999, 8640, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', b'1', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\"]', '[\"user_info\"]', '[\"system:user:query\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2022-05-12 01:00:20', b'0');
-INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `auto_approve`, `authorized_grant_types`, `scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yudao.iocoder.cn/277a899d573723f1fcdfb57340f00379.png', NULL, 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', b'1', '[\"password\",\"authorization_code\",\"implicit\"]', '[]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2022-05-12 00:59:53', b'0');
+INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yudao.iocoder.cn/a5e2e244368878a366b516805a4aabf1.png', '我是描述', 0, 999999999, 8640, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[\"user.read\",\"user.write\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2022-05-23 13:33:11', b'0');
+INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yudao.iocoder.cn/277a899d573723f1fcdfb57340f00379.png', NULL, 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\"]', '[\"user_info\",\"projects\"]', '[\"user_info\"]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2022-05-14 15:11:31', b'0');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for system_oauth2_code
+-- ----------------------------
+DROP TABLE IF EXISTS `system_oauth2_code`;
+CREATE TABLE `system_oauth2_code`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `user_id` bigint NOT NULL COMMENT '用户编号',
+  `user_type` tinyint NOT NULL COMMENT '用户类型',
+  `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '授权码',
+  `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号',
+  `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '授权范围',
+  `expires_time` datetime NOT NULL COMMENT '过期时间',
+  `redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '可重定向的 URI 地址',
+  `state` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '状态',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 授权码表';
+
+-- ----------------------------
+-- Records of system_oauth2_code
+-- ----------------------------
+BEGIN;
 COMMIT;
 
 -- ----------------------------
@@ -1811,6 +1868,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `refresh_token` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '刷新令牌',
   `user_type` tinyint NOT NULL COMMENT '用户类型',
   `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号',
+  `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权范围',
   `expires_time` datetime NOT NULL COMMENT '过期时间',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -1819,7 +1877,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 106 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -1859,7 +1917,7 @@ CREATE TABLE `system_operate_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2097 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 2214 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
 
 -- ----------------------------
 -- Records of system_operate_log
@@ -1917,7 +1975,7 @@ CREATE TABLE `system_role`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 112 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 114 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
 
 -- ----------------------------
 -- Records of system_role
@@ -1929,6 +1987,7 @@ INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_sco
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121);
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (110, '测试角色', 'test', 0, 1, '[]', 0, 2, '嘿嘿', '110', '2022-02-23 00:14:34', '110', '2022-02-23 13:14:58', b'0', 121);
 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122);
+INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
 COMMIT;
 
 -- ----------------------------
@@ -1946,7 +2005,7 @@ CREATE TABLE `system_role_menu`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1695 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1729 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表';
 
 -- ----------------------------
 -- Records of system_role_menu
@@ -2143,6 +2202,23 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1692, 101, 114, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1693, 101, 115, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1694, 101, 116, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1712, 113, 1024, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1713, 113, 1025, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1714, 113, 1, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1715, 113, 102, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1716, 113, 103, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1717, 113, 104, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1718, 113, 1013, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1719, 113, 1014, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1720, 113, 1015, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1721, 113, 1016, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1722, 113, 1017, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1723, 113, 1018, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1724, 113, 1019, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1725, 113, 1020, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1726, 113, 1021, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1727, 113, 1022, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
+INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1728, 113, 1023, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
 COMMIT;
 
 -- ----------------------------
@@ -2199,7 +2275,7 @@ BEGIN;
 INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道', 'YUN_PIAN', 0, '呵呵呵哒', '1555a14277cb8a608cf45a9e6a80d510', NULL, 'http://vdwapu.natappfree.cc/admin-api/system/sms/callback/yunpian', '', '2021-03-31 06:12:20', '1', '2022-02-23 16:48:44', b'0');
 INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '啦啦啦', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2021-04-14 00:08:37', b'0');
 INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0');
-INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, '测试演示', 'DEBUG_DING_TALK', 0, NULL, '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2022-04-10 23:07:59', '1', '2022-04-10 23:07:59', b'0');
+INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, '测试演示', 'DEBUG_DING_TALK', 0, NULL, '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2022-04-10 23:07:59', '1', '2022-05-16 20:34:49', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -2224,7 +2300,7 @@ CREATE TABLE `system_sms_code`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号'
-) ENGINE = InnoDB AUTO_INCREMENT = 467 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
+) ENGINE = InnoDB AUTO_INCREMENT = 468 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
 
 -- ----------------------------
 -- Records of system_sms_code
@@ -2267,7 +2343,7 @@ CREATE TABLE `system_sms_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 = 140 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 144 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
 
 -- ----------------------------
 -- Records of system_sms_log
@@ -2337,7 +2413,7 @@ CREATE TABLE `system_social_user`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
+) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
 
 -- ----------------------------
 -- Records of system_social_user
@@ -2362,7 +2438,7 @@ CREATE TABLE `system_social_user_bind`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
+) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
 
 -- ----------------------------
 -- Records of system_social_user_bind
@@ -2391,14 +2467,14 @@ CREATE TABLE `system_tenant`  (
   `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 = 123 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '租户表';
+) ENGINE = InnoDB AUTO_INCREMENT = 125 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '租户表';
 
 -- ----------------------------
 -- Records of system_tenant
 -- ----------------------------
 BEGIN;
 INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `domain`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'https://www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2022-02-23 12:15:11', b'0');
-INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `domain`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'http://www.iocoder.cn', 111, '2024-03-11 00:00:00', 20, '1', '2022-02-22 00:56:14', '1', '2022-03-19 18:37:20', b'0');
+INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `domain`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'http://www.iocoder.cn', 111, '2024-03-11 00:00:00', 20, '1', '2022-02-22 00:56:14', '1', '2022-05-17 10:03:59', b'0');
 INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `domain`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'https://www.iocoder.cn', 111, '2022-04-30 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0');
 COMMIT;
 
@@ -2442,7 +2518,7 @@ CREATE TABLE `system_user_post`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 115 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表';
+) ENGINE = InnoDB AUTO_INCREMENT = 116 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表';
 
 -- ----------------------------
 -- Records of system_user_post
@@ -2451,6 +2527,7 @@ BEGIN;
 INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, 1, 1, 'admin', '2022-05-02 07:25:24', 'admin', '2022-05-02 07:25:24', b'0', 1);
 INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 100, 1, 'admin', '2022-05-02 07:25:24', 'admin', '2022-05-02 07:25:24', b'0', 1);
 INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 114, 3, 'admin', '2022-05-02 07:25:24', 'admin', '2022-05-02 07:25:24', b'0', 1);
+INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 104, 1, '1', '2022-05-16 19:36:28', '1', '2022-05-16 19:36:28', b'0', 1);
 COMMIT;
 
 -- ----------------------------
@@ -2468,7 +2545,7 @@ CREATE TABLE `system_user_role`  (
   `deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表';
+) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表';
 
 -- ----------------------------
 -- Records of system_user_role
@@ -2489,6 +2566,7 @@ INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_t
 INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (16, 113, 111, '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122);
 INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (17, 114, 101, '1', '2022-03-19 21:51:13', '1', '2022-03-19 21:51:13', b'0', 1);
 INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (18, 1, 2, '1', '2022-05-12 20:39:29', '1', '2022-05-12 20:39:29', b'0', 1);
+INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (19, 116, 113, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
 COMMIT;
 
 -- ----------------------------
@@ -2546,16 +2624,16 @@ CREATE TABLE `system_users`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE INDEX `idx_username`(`username` ASC, `update_time` ASC, `tenant_id` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 116 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 117 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
 
 -- ----------------------------
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://test.yudao.iocoder.cn/48934f2f-92d4-4250-b917-d10d2b262c6a', 0, '127.0.0.1', '2022-05-13 00:12:13', 'admin', '2021-01-05 17:03:47', NULL, '2022-05-13 00:12:13', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '', NULL, '', '2021-01-07 09:07:17', '104', '2021-12-16 09:26:10', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://test.yudao.iocoder.cn/48934f2f-92d4-4250-b917-d10d2b262c6a', 0, '127.0.0.1', '2022-05-23 20:27:29', 'admin', '2021-01-05 17:03:47', NULL, '2022-05-23 20:27:29', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-05-22 19:35:33', '', '2021-01-07 09:07:17', NULL, '2022-05-22 19:35:33', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$wWoPT7sqriM2O1YXRL.je.GiL538OR6ZTN8aQZr9JAGdnpCH2tpYe', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '127.0.0.1', '2022-01-18 00:33:40', '', '2021-01-13 23:50:35', NULL, '2022-01-18 00:33:40', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$e5RpuDCC0GYSt0Hvd2.CjujIXwgGct4SnXi6dVGxdgFsnqgEryk5a', '测试号', NULL, 107, '[]', '111@qq.com', '15601691200', 1, '', 0, '127.0.0.1', '2022-03-19 21:46:19', '', '2021-01-21 02:13:53', NULL, '2022-03-19 21:46:19', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$e5RpuDCC0GYSt0Hvd2.CjujIXwgGct4SnXi6dVGxdgFsnqgEryk5a', '测试号', NULL, 107, '[1]', '111@qq.com', '15601691200', 1, '', 0, '127.0.0.1', '2022-03-19 21:46:19', '', '2021-01-21 02:13:53', '1', '2022-05-16 19:36:28', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2022-02-27 08:26:51', b'0', 118);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2022-02-27 08:26:53', b'0', 119);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2022-02-27 08:26:56', b'0', 120);
@@ -2564,7 +2642,8 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, 'newobject', '$2a$10$jh5MsR.ud/gKe3mVeUp5t.nEXGDSmHyv5OYjWQwHO8wlGmMSI9Twy', '新对象', NULL, NULL, '[]', '', '', 0, '', 0, '', NULL, '1', '2022-02-23 19:08:03', '1', '2022-02-23 19:08:03', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', NULL, '2022-03-19 18:38:51', b'0', 122);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[3]', '', '', 0, '', 0, '127.0.0.1', '2022-03-19 22:15:43', '1', '2022-03-19 21:50:58', NULL, '2022-03-19 22:15:43', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$10$/WCwGHu1eq0wOVDd/u8HweJ0gJCHyLS6T7ndCqI8UXZAQom1etk2e', '1', '11', 100, '[]', '', '', 0, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2022-04-30 02:55:43', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$10$/WCwGHu1eq0wOVDd/u8HweJ0gJCHyLS6T7ndCqI8UXZAQom1etk2e', '1', '11', 101, '[]', '', '', 1, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2022-05-22 20:18:45', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, '15601691302', '$2a$10$L5C4S0U6adBWMvFv1Wwl4.DI/NwYS3WIfLj5Q.Naqr5II8CmqsDZ6', '小豆', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
 COMMIT;
 
 SET FOREIGN_KEY_CHECKS = 1;

File diff suppressed because it is too large
+ 290 - 148
sql/oracle/ruoyi-vue-pro.sql


File diff suppressed because it is too large
+ 324 - 329
sql/postgresql/ruoyi-vue-pro.sql


File diff suppressed because it is too large
+ 3536 - 1
sql/sqlserver/ruoyi-vue-pro.sql


+ 9 - 6
yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java

@@ -2,13 +2,16 @@ package cn.iocoder.yudao.framework.test.core.util;
 
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import uk.co.jemos.podam.api.PodamFactory;
 import uk.co.jemos.podam.api.PodamFactoryImpl;
 
 import java.lang.reflect.Type;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -22,7 +25,6 @@ public class RandomUtils {
 
     private static final int RANDOM_STRING_LENGTH = 10;
 
-    private static final Set<String> TINYINT_FIELDS = SetUtils.asSet("type", "category");
     private static final int TINYINT_MAX = 127;
 
     private static final int RANDOM_DATE_MAX = 30;
@@ -41,9 +43,10 @@ public class RandomUtils {
             if (attributeMetadata.getAttributeName().equals("status")) {
                 return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus();
             }
-            // 针对部分字段,使用 tinyint 范围
-            if (TINYINT_FIELDS.contains(attributeMetadata.getAttributeName())) {
-                return RandomUtil.randomInt(1, TINYINT_MAX + 1);
+            // 如果是 type、status 结尾的字段,返回 tinyint 范围
+            if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(),
+                    "type", "status", "category", "scope")) {
+                return RandomUtil.randomInt(0, TINYINT_MAX + 1);
             }
             return RandomUtil.randomInt();
         });

+ 1 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/sql.vm

@@ -9,6 +9,7 @@ VALUES (
 );
 
 -- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
 SELECT @parentId := LAST_INSERT_ID();
 
 -- 按钮 SQL

+ 0 - 15
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http

@@ -52,18 +52,3 @@ tenant-id: {{adminTenentId}}
 POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106
 Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
 tenant-id: {{adminTenentId}}
-
-### 请求 /system/oauth2/user/get 接口 => 成功
-GET {{baseUrl}}/system/oauth2/user/get
-Authorization: Bearer 9502bd7a768a4ade920b90f41e2efd5c
-tenant-id: {{adminTenentId}}
-
-### 请求 /system/oauth2/user/update 接口 => 成功
-PUT {{baseUrl}}/system/oauth2/user/update
-Content-Type: application/json
-Authorization: Bearer 9502bd7a768a4ade920b90f41e2efd5c
-tenant-id: {{adminTenentId}}
-
-{
-  "nickname": "芋道源码"
-}

+ 4 - 56
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.system.controller.admin.oauth2;
 
-import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
@@ -13,36 +12,26 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
-import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
-import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserUpdateReqVO;
 import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert;
-import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
-import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
-import cn.iocoder.yudao.module.system.service.dept.DeptService;
-import cn.iocoder.yudao.module.system.service.dept.PostService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
-import cn.iocoder.yudao.module.system.service.user.AdminUserService;
 import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
-import javax.validation.Valid;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -61,7 +50,7 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
  * 另外,一个公司如果有多个管理后台,它们 client_id 产生的 access token 相互之间是无法互通的,即无法访问它们系统的 API 接口,直到两个 client_id 产生信任授权。
  *
  * 考虑到【本系统】暂时不想做的过于复杂,默认只有获取到 access token 之后,可以访问【本系统】管理后台的 /system-api/* 所有接口,除非手动添加 scope 控制。
- * scope 的使用示例,可见当前类的 getUserInfo 和 updateUserInfo 方法,上面有 @PreAuthorize("@ss.hasScope('user.read')") 和 @PreAuthorize("@ss.hasScope('user.write')") 注解
+ * scope 的使用示例,可见 {@link OAuth2UserController} 类
  *
  * @author 芋道源码
  */
@@ -194,8 +183,7 @@ public class OAuth2OpenController {
         // 0. 校验用户已经登录。通过 Spring Security 实现
 
         // 1. 获得 Client 客户端的信息
-        OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
-                null, null, null);
+        OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId);
         // 2. 获得用户已经授权的信息
         List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
         // 拼接返回
@@ -232,7 +220,6 @@ public class OAuth2OpenController {
         @SuppressWarnings("unchecked")
         Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
         scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
-        // TODO 芋艿:针对 approved + scopes 在看看 spring security 的实现
         // 0. 校验用户已经登录。通过 Spring Security 实现
 
         // 1.1 校验 responseType 是否满足 code 或者 token 值
@@ -271,7 +258,7 @@ public class OAuth2OpenController {
         if (StrUtil.equalsAny(responseType, "token")) {
             return OAuth2GrantTypeEnum.IMPLICIT;
         }
-        throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token");
+        throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token");
     }
 
     private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client,
@@ -288,7 +275,7 @@ public class OAuth2OpenController {
     private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client,
                                                 List<String> scopes, String redirectUri, String state) {
         // 1. 创建 code 授权码
-        String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId,getUserType(), client.getClientId(), scopes,
+        String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId, getUserType(), client.getClientId(), scopes,
                 redirectUri, state);
         // 2. 拼接重定向的 URL
         return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state);
@@ -306,43 +293,4 @@ public class OAuth2OpenController {
         return clientIdAndSecret;
     }
 
-    // ============ 用户操作的示例,展示 scope 的使用 ============
-
-    @Resource
-    private AdminUserService userService;
-    @Resource
-    private DeptService deptService;
-    @Resource
-    private PostService postService;
-
-    @GetMapping("/user/get")
-    @ApiOperation("获得用户基本信息")
-    @PreAuthorize("@ss.hasScope('user.read')")
-    public CommonResult<OAuth2OpenUserInfoRespVO> getUserInfo() {
-        // 获得用户基本信息
-        AdminUserDO user = userService.getUser(getLoginUserId());
-        OAuth2OpenUserInfoRespVO resp = OAuth2OpenConvert.INSTANCE.convert(user);
-        // 获得部门信息
-        if (user.getDeptId() != null) {
-            DeptDO dept = deptService.getDept(user.getDeptId());
-            resp.setDept(OAuth2OpenConvert.INSTANCE.convert(dept));
-        }
-        // 获得岗位信息
-        if (CollUtil.isNotEmpty(user.getPostIds())) {
-            List<PostDO> posts = postService.getPosts(user.getPostIds());
-            resp.setPosts(OAuth2OpenConvert.INSTANCE.convertList(posts));
-        }
-        return success(resp);
-    }
-
-    @PutMapping("/user/update")
-    @ApiOperation("更新用户基本信息")
-    @PreAuthorize("@ss.hasScope('user.write')")
-    public CommonResult<Boolean> updateUserInfo(@Valid @RequestBody OAuth2OpenUserUpdateReqVO reqVO) {
-        // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。
-        // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做
-        userService.updateUserProfile(getLoginUserId(), OAuth2OpenConvert.INSTANCE.convert(reqVO));
-        return success(true);
-    }
-
 }

+ 14 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http

@@ -0,0 +1,14 @@
+### 请求 /system/oauth2/user/get 接口 => 成功
+GET {{baseUrl}}/system/oauth2/user/get
+Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d
+tenant-id: {{adminTenentId}}
+
+### 请求 /system/oauth2/user/update 接口 => 成功
+PUT {{baseUrl}}/system/oauth2/user/update
+Content-Type: application/json
+Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d
+tenant-id: {{adminTenentId}}
+
+{
+  "nickname": "芋道源码"
+}

+ 80 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.system.controller.admin.oauth2;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO;
+import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2UserConvert;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
+import cn.iocoder.yudao.module.system.service.dept.DeptService;
+import cn.iocoder.yudao.module.system.service.dept.PostService;
+import cn.iocoder.yudao.module.system.service.user.AdminUserService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+/**
+ * 提供给外部应用调用为主
+ *
+ * 1. 在 getUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.read')") 注解,声明需要满足 scope = user.read
+ * 2. 在 updateUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.write')") 注解,声明需要满足 scope = user.write
+ *
+ * @author 芋道源码
+ */
+@Api(tags = "管理后台 - OAuth2.0 用户")
+@RestController
+@RequestMapping("/system/oauth2/user")
+@Validated
+@Slf4j
+public class OAuth2UserController {
+
+    @Resource
+    private AdminUserService userService;
+    @Resource
+    private DeptService deptService;
+    @Resource
+    private PostService postService;
+
+    @GetMapping("/get")
+    @ApiOperation("获得用户基本信息")
+    @PreAuthorize("@ss.hasScope('user.read')") //
+    public CommonResult<OAuth2UserInfoRespVO> getUserInfo() {
+        // 获得用户基本信息
+        AdminUserDO user = userService.getUser(getLoginUserId());
+        OAuth2UserInfoRespVO resp = OAuth2UserConvert.INSTANCE.convert(user);
+        // 获得部门信息
+        if (user.getDeptId() != null) {
+            DeptDO dept = deptService.getDept(user.getDeptId());
+            resp.setDept(OAuth2UserConvert.INSTANCE.convert(dept));
+        }
+        // 获得岗位信息
+        if (CollUtil.isNotEmpty(user.getPostIds())) {
+            List<PostDO> posts = postService.getPosts(user.getPostIds());
+            resp.setPosts(OAuth2UserConvert.INSTANCE.convertList(posts));
+        }
+        return success(resp);
+    }
+
+    @PutMapping("/update")
+    @ApiOperation("更新用户基本信息")
+    @PreAuthorize("@ss.hasScope('user.write')")
+    public CommonResult<Boolean> updateUserInfo(@Valid @RequestBody OAuth2UserUpdateReqVO reqVO) {
+        // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。
+        // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做
+        userService.updateUserProfile(getLoginUserId(), OAuth2UserConvert.INSTANCE.convert(reqVO));
+        return success(true);
+    }
+
+}

+ 3 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java

@@ -7,7 +7,7 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import java.util.Set;
+import java.util.List;
 
 @ApiModel("管理后台 - 【开放接口】校验令牌 Response VO")
 @Data
@@ -28,13 +28,13 @@ public class OAuth2OpenCheckTokenRespVO {
     @ApiModelProperty(value = "客户端编号", required = true, example = "car")
     private String clientId;
     @ApiModelProperty(value = "授权范围", required = true, example = "user_info")
-    private Set<String> scopes;
+    private List<String> scopes;
 
     @ApiModelProperty(value = "访问令牌", required = true, example = "tudou")
     @JsonProperty("access_token")
     private String accessToken;
 
     @ApiModelProperty(value = "过期时间", required = true, example = "1593092157", notes = "时间戳 / 1000,即单位:秒")
-    @JsonProperty("exp")
     private Long exp;
+
 }

+ 3 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserInfoRespVO.java → yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user;
+package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user;
 
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -8,11 +8,11 @@ import lombok.NoArgsConstructor;
 
 import java.util.List;
 
-@ApiModel("管理后台 - 【开放接口】获得用户基本信息 Response VO")
+@ApiModel("管理后台 - OAuth2.0 获得用户基本信息 Response VO")
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-public class OAuth2OpenUserInfoRespVO {
+public class OAuth2UserInfoRespVO {
 
     @ApiModelProperty(value = "用户编号", required = true, example = "1")
     private Long id;

+ 3 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserUpdateReqVO.java → yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user;
+package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user;
 
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -10,11 +10,11 @@ import org.hibernate.validator.constraints.Length;
 import javax.validation.constraints.Email;
 import javax.validation.constraints.Size;
 
-@ApiModel("管理后台 - 【开放接口】更新用户基本信息 Request VO")
+@ApiModel("管理后台 - OAuth2.0 更新用户基本信息 Request VO")
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-public class OAuth2OpenUserUpdateReqVO {
+public class OAuth2UserUpdateReqVO {
 
     @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
     @Size(max = 30, message = "用户昵称长度不能超过 30 个字符")

+ 0 - 14
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java

@@ -7,15 +7,9 @@ import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
-import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
-import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserUpdateReqVO;
-import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
-import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
-import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
@@ -46,14 +40,6 @@ public interface OAuth2OpenConvert {
     }
     OAuth2OpenCheckTokenRespVO convert3(OAuth2AccessTokenDO bean);
 
-    // ============ 用户操作的示例 ============
-
-    OAuth2OpenUserInfoRespVO convert(AdminUserDO bean);
-    OAuth2OpenUserInfoRespVO.Dept convert(DeptDO dept);
-    List<OAuth2OpenUserInfoRespVO.Post> convertList(List<PostDO> list);
-
-    UserProfileUpdateReqVO convert(OAuth2OpenUserUpdateReqVO bean);
-
     default OAuth2OpenAuthorizeInfoRespVO convert(OAuth2ClientDO client, List<OAuth2ApproveDO> approves) {
         // 构建 scopes
         List<KeyValue<String, Boolean>> scopes = new ArrayList<>(client.getScopes().size());

+ 25 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2UserConvert.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.system.convert.oauth2;
+
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+@Mapper
+public interface OAuth2UserConvert {
+
+    OAuth2UserConvert INSTANCE = Mappers.getMapper(OAuth2UserConvert.class);
+
+    OAuth2UserInfoRespVO convert(AdminUserDO bean);
+    OAuth2UserInfoRespVO.Dept convert(DeptDO dept);
+    List<OAuth2UserInfoRespVO.Post> convertList(List<PostDO> list);
+
+    UserProfileUpdateReqVO convert(OAuth2UserUpdateReqVO bean);
+
+}

+ 1 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java

@@ -9,7 +9,6 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
-import lombok.experimental.Accessors;
 
 import java.util.Date;
 import java.util.List;
@@ -22,11 +21,10 @@ import java.util.List;
  *
  * @author 芋道源码
  */
-@TableName("system_oauth2_access_token")
+@TableName(value = "system_oauth2_access_token", autoResultMap = true)
 @KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
-@Accessors(chain = true)
 public class OAuth2AccessTokenDO extends TenantBaseDO {
 
     /**

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java

@@ -18,7 +18,7 @@ import java.util.List;
  *
  * @author 芋道源码
  */
-@TableName("system_oauth2_refresh_token")
+@TableName(value = "system_oauth2_refresh_token", autoResultMap = true)
 // 由于 Oracle 的 SEQ 的名字长度有限制,所以就先用 system_oauth2_access_token_seq 吧,反正也没啥问题
 @KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java

@@ -25,7 +25,7 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX<OAuth2AccessTokenDO
         return selectPage(reqVO, new LambdaQueryWrapperX<OAuth2AccessTokenDO>()
                 .eqIfPresent(OAuth2AccessTokenDO::getUserId, reqVO.getUserId())
                 .eqIfPresent(OAuth2AccessTokenDO::getUserType, reqVO.getUserType())
-                .eqIfPresent(OAuth2AccessTokenDO::getClientId, reqVO.getClientId())
+                .likeIfPresent(OAuth2AccessTokenDO::getClientId, reqVO.getClientId())
                 .gt(OAuth2AccessTokenDO::getExpiresTime, new Date())
                 .orderByDesc(OAuth2AccessTokenDO::getId));
     }

+ 9 - 5
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImpl.java

@@ -6,7 +6,9 @@ import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
 import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ApproveMapper;
+import com.google.common.annotations.VisibleForTesting;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
@@ -35,6 +37,7 @@ public class OAuth2ApproveServiceImpl implements OAuth2ApproveService {
     private OAuth2ApproveMapper oauth2ApproveMapper;
 
     @Override
+    @Transactional
     public boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection<String> requestedScopes) {
         // 第一步,基于 Client 的自动授权计算,如果 scopes 都在自动授权中,则返回 true 通过
         OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
@@ -49,14 +52,14 @@ public class OAuth2ApproveServiceImpl implements OAuth2ApproveService {
         }
 
         // 第二步,算上用户已经批准的授权。如果 scopes 都包含,则返回 true
-        List<OAuth2ApproveDO> approveDOs = oauth2ApproveMapper.selectListByUserIdAndUserTypeAndClientId(
-                userId, userType, clientId);
+        List<OAuth2ApproveDO> approveDOs = getApproveList(userId, userType, clientId);
         Set<String> scopes = convertSet(approveDOs, OAuth2ApproveDO::getScope,
-                o -> o.getApproved() && !DateUtils.isExpired(o.getExpiresTime())); // 只保留未过期
+                OAuth2ApproveDO::getApproved); // 只保留未过期的 + 同意
         return CollUtil.containsAll(scopes, requestedScopes);
     }
 
     @Override
+    @Transactional
     public boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map<String, Boolean> requestedScopes) {
         // 如果 requestedScopes 为空,说明没有要求,则返回 true 通过
         if (CollUtil.isEmpty(requestedScopes)) {
@@ -83,8 +86,9 @@ public class OAuth2ApproveServiceImpl implements OAuth2ApproveService {
         return approveDOs;
     }
 
-    private void saveApprove(Long userId, Integer userType, String clientId,
-                             String scope, Boolean approved, Date expireTime) {
+    @VisibleForTesting
+    void saveApprove(Long userId, Integer userType, String clientId,
+                     String scope, Boolean approved, Date expireTime) {
         // 先更新
         OAuth2ApproveDO approveDO = new OAuth2ApproveDO().setUserId(userId).setUserType(userType)
                 .setClientId(clientId).setScope(scope).setApproved(approved).setExpiresTime(expireTime);

+ 6 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java

@@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ClientMapper;
 import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
 import com.google.common.annotations.VisibleForTesting;
 import lombok.Getter;
+import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
@@ -51,7 +52,8 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
      *
      * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
      */
-    @Getter
+    @Getter // 解决单测
+    @Setter // 解决单测
     private volatile Map<String, OAuth2ClientDO> clientCache;
     /**
      * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新
@@ -151,7 +153,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
     }
 
     @VisibleForTesting
-    public void validateClientIdExists(Long id, String clientId) {
+    void validateClientIdExists(Long id, String clientId) {
         OAuth2ClientDO client = oauth2ClientMapper.selectByClientId(clientId);
         if (client == null) {
             return;
@@ -160,7 +162,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
         if (id == null) {
             throw exception(OAUTH2_CLIENT_EXISTS);
         }
-        if (!client.getClientId().equals(clientId)) {
+        if (!client.getId().equals(id)) {
             throw exception(OAUTH2_CLIENT_EXISTS);
         }
     }
@@ -189,7 +191,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
 
         // 校验客户端密钥
         if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) {
-            throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR, clientSecret);
+            throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR);
         }
         // 校验授权方式
         if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) {

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeService.java

@@ -26,8 +26,8 @@ public interface OAuth2CodeService {
      * @param state 状态
      * @return 授权码的信息
      */
-    OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, List<String> scopes,
-                                         String redirectUri, String state);
+    OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId,
+                                         List<String> scopes, String redirectUri, String state);
 
     /**
      * 使用授权码

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImpl.java

@@ -33,8 +33,8 @@ public class OAuth2CodeServiceImpl implements OAuth2CodeService {
     private OAuth2CodeMapper oauth2CodeMapper;
 
     @Override
-    public OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, List<String> scopes,
-                                                String redirectUri, String state) {
+    public OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId,
+                                                List<String> scopes, String redirectUri, String state) {
         OAuth2CodeDO codeDO = new OAuth2CodeDO().setCode(generateCode())
                 .setUserId(userId).setUserType(userType)
                 .setClientId(clientId).setScopes(scopes)

+ 7 - 5
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java

@@ -61,11 +61,13 @@ public class OAuth2Utils {
         if (CollUtil.isNotEmpty(scopes)) {
             vars.put("scope", buildScopeStr(scopes));
         }
-        for (String key : additionalInformation.keySet()) {
-            Object value = additionalInformation.get(key);
-            if (value != null) {
-                keys.put("extra_" + key, key);
-                vars.put("extra_" + key, value);
+        if (CollUtil.isNotEmpty(additionalInformation)) {
+            for (String key : additionalInformation.keySet()) {
+                Object value = additionalInformation.get(key);
+                if (value != null) {
+                    keys.put("extra_" + key, key);
+                    vars.put("extra_" + key, value);
+                }
             }
         }
         // Do not include the refresh token (even if there is one)

+ 330 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java

@@ -0,0 +1,330 @@
+package cn.iocoder.yudao.module.system.controller.admin.oauth2;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
+import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
+import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService;
+import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
+import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService;
+import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
+import org.assertj.core.util.Lists;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link OAuth2OpenController} 的单元测试
+ *
+ * @author 芋道源码
+ */
+public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private OAuth2OpenController oauth2OpenController;
+
+    @Mock
+    private OAuth2GrantService oauth2GrantService;
+    @Mock
+    private OAuth2ClientService oauth2ClientService;
+    @Mock
+    private OAuth2ApproveService oauth2ApproveService;
+    @Mock
+    private OAuth2TokenService oauth2TokenService;
+
+    @Test
+    public void testPostAccessToken_authorizationCode() {
+        // 准备参数
+        String granType = OAuth2GrantTypeEnum.AUTHORIZATION_CODE.getGrantType();
+        String code = randomString();
+        String redirectUri = randomString();
+        String state = randomString();
+        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
+        // mock 方法(client)
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
+        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(new ArrayList<>()), eq(redirectUri))).thenReturn(client);
+
+        // mock 方法(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setExpiresTime(addTime(Duration.ofMillis(30010L))); // 多给 10 毫秒,保证可执行完
+        when(oauth2GrantService.grantAuthorizationCodeForAccessToken(eq("test_client_id"),
+                eq(code), eq(redirectUri), eq(state))).thenReturn(accessTokenDO);
+
+        // 调用
+        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
+                code, redirectUri, state, null, null, null, null);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertPojoEquals(accessTokenDO, result.getData());
+        assertEquals(30L, result.getData().getExpiresIn()); // 执行过程会过去几毫秒
+    }
+
+    @Test
+    public void testPostAccessToken_password() {
+        // 准备参数
+        String granType = OAuth2GrantTypeEnum.PASSWORD.getGrantType();
+        String username = randomString();
+        String password = randomString();
+        String scope = "write read";
+        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
+        // mock 方法(client)
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
+        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"),
+                eq(granType), eq(Lists.newArrayList("write", "read")), isNull())).thenReturn(client);
+
+        // mock 方法(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setExpiresTime(addTime(Duration.ofMillis(30010L))); // 多给 10 毫秒,保证可执行完
+        when(oauth2GrantService.grantPassword(eq(username), eq(password), eq("test_client_id"),
+                eq(Lists.newArrayList("write", "read")))).thenReturn(accessTokenDO);
+
+        // 调用
+        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
+                null, null, null, username, password, scope, null);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertPojoEquals(accessTokenDO, result.getData());
+        assertEquals(30L, result.getData().getExpiresIn()); // 执行过程会过去几毫秒
+    }
+
+    @Test
+    public void testPostAccessToken_refreshToken() {
+        // 准备参数
+        String granType = OAuth2GrantTypeEnum.REFRESH_TOKEN.getGrantType();
+        String refreshToken = randomString();
+        String password = randomString();
+        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
+        // mock 方法(client)
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
+        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"),
+                eq(granType), eq(Lists.newArrayList()), isNull())).thenReturn(client);
+
+        // mock 方法(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setExpiresTime(addTime(Duration.ofMillis(30010L))); // 多给 10 毫秒,保证可执行完
+        when(oauth2GrantService.grantRefreshToken(eq(refreshToken), eq("test_client_id"))).thenReturn(accessTokenDO);
+
+        // 调用
+        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
+                null, null, null, null, password, null, refreshToken);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertPojoEquals(accessTokenDO, result.getData());
+        assertEquals(30L, result.getData().getExpiresIn()); // 执行过程会过去几毫秒
+    }
+
+    @Test
+    public void testPostAccessToken_implicit() {
+        // 调用,并断言
+        assertServiceException(() -> oauth2OpenController.postAccessToken(null,
+                        OAuth2GrantTypeEnum.IMPLICIT.getGrantType(), null, null, null,
+                        null, null, null, null),
+                new ErrorCode(400, "Token 接口不支持 implicit 授权模式"));
+    }
+
+    @Test
+    public void testRevokeToken() {
+        // 准备参数
+        HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret");
+        String token = randomString();
+        // mock 方法(client)
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id");
+        when(oauth2ClientService.validOAuthClientFromCache(eq("demo_client_id"),
+                eq("demo_client_secret"), isNull(), isNull(), isNull())).thenReturn(client);
+        // mock 方法(移除)
+        when(oauth2GrantService.revokeToken(eq("demo_client_id"), eq(token))).thenReturn(true);
+
+        // 调用
+        CommonResult<Boolean> result = oauth2OpenController.revokeToken(request, token);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void testCheckToken() {
+        // 准备参数
+        HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret");
+        String token = randomString();
+        // mock 方法
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setUserType(UserTypeEnum.ADMIN.getValue()).setExpiresTime(new Date(1653485731195L));
+        when(oauth2TokenService.checkAccessToken(eq(token))).thenReturn(accessTokenDO);
+
+        // 调用
+        CommonResult<OAuth2OpenCheckTokenRespVO> result = oauth2OpenController.checkToken(request, token);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertPojoEquals(accessTokenDO, result.getData());
+        assertEquals(1653485731L, result.getData().getExp()); // 执行过程会过去几毫秒
+    }
+
+    @Test
+    public void testAuthorize() {
+        // 准备参数
+        String clientId = randomString();
+        // mock 方法(client)
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id").setScopes(ListUtil.toList("read", "write", "all"));
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(client);
+        // mock 方法(approve)
+        List<OAuth2ApproveDO> approves = asList(
+                randomPojo(OAuth2ApproveDO.class).setScope("read").setApproved(true),
+                randomPojo(OAuth2ApproveDO.class).setScope("write").setApproved(false));
+        when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves);
+
+        // 调用
+        CommonResult<OAuth2OpenAuthorizeInfoRespVO> result = oauth2OpenController.authorize(clientId);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertPojoEquals(client, result.getData().getClient());
+        assertEquals(new KeyValue<>("read", true), result.getData().getScopes().get(0));
+        assertEquals(new KeyValue<>("write", false), result.getData().getScopes().get(1));
+        assertEquals(new KeyValue<>("all", false), result.getData().getScopes().get(2));
+    }
+
+    @Test
+    public void testApproveOrDeny_grantTypeError() {
+        // 调用,并断言
+        assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null,
+                        null, null, null, null),
+                new ErrorCode(400, "response_type 参数值只允许 code 和 token"));
+    }
+
+    @Test // autoApprove = true,但是不通过
+    public void testApproveOrDeny_autoApproveNo() {
+        // 准备参数
+        String responseType = "code";
+        String clientId = randomString();
+        String scope = "{\"read\": true, \"write\": false}";
+        String redirectUri = randomString();
+        String state = randomString();
+        // mock 方法
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"),
+                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
+
+        // 调用
+        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
+                scope, redirectUri, true, state);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertNull(result.getData());
+    }
+
+    @Test // autoApprove = false,但是不通过
+    public void testApproveOrDeny_ApproveNo() {
+        // 准备参数
+        String responseType = "token";
+        String clientId = randomString();
+        String scope = "{\"read\": true, \"write\": false}";
+        String redirectUri = "https://www.iocoder.cn";
+        String state = "test";
+        // mock 方法
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"),
+                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
+
+        // 调用
+        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
+                scope, redirectUri, false, state);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertEquals("https://www.iocoder.cn#error=access_denied&error_description=User%20denied%20access&state=test", result.getData());
+    }
+
+    @Test // autoApprove = true,通过 + token
+    public void testApproveOrDeny_autoApproveWithToken() {
+        // 准备参数
+        String responseType = "token";
+        String clientId = randomString();
+        String scope = "{\"read\": true, \"write\": false}";
+        String redirectUri = "https://www.iocoder.cn";
+        String state = "test";
+        // mock 方法(client)
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"),
+                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
+        // mock 方法(场景一)
+        when(oauth2ApproveService.checkForPreApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
+                eq(clientId), eq(SetUtils.asSet("read", "write")))).thenReturn(true);
+        // mock 方法(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setAccessToken("test_access_token").setExpiresTime(addTime(Duration.ofMillis(30010L)));
+        when(oauth2GrantService.grantImplicit(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
+                eq(clientId), eq(ListUtil.toList("read")))).thenReturn(accessTokenDO);
+
+        // 调用
+        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
+                scope, redirectUri, true, state);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertEquals("https://www.iocoder.cn#access_token=test_access_token&token_type=bearer&state=test&expires_in=30&scope=read", result.getData());
+    }
+
+    @Test // autoApprove = false,通过 + code
+    public void testApproveOrDeny_approveWithCode() {
+        // 准备参数
+        String responseType = "code";
+        String clientId = randomString();
+        String scope = "{\"read\": true, \"write\": false}";
+        String redirectUri = "https://www.iocoder.cn";
+        String state = "test";
+        // mock 方法(client)
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"),
+                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
+        // mock 方法(场景二)
+        when(oauth2ApproveService.updateAfterApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId),
+                eq(MapUtil.builder(new LinkedHashMap<String, Boolean>()).put("read", true).put("write", false).build())))
+                .thenReturn(true);
+        // mock 方法(访问令牌)
+        String authorizationCode = "test_code";
+        when(oauth2GrantService.grantAuthorizationCodeForCode(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
+                eq(clientId), eq(ListUtil.toList("read")), eq(redirectUri), eq(state))).thenReturn(authorizationCode);
+
+        // 调用
+        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
+                scope, redirectUri, false, state);
+        // 断言
+        assertEquals(0, result.getCode());
+        assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData());
+    }
+
+    private HttpServletRequest mockRequest(String clientId, String secret) {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getParameter(eq("client_id"))).thenReturn(clientId);
+        when(request.getParameter(eq("client_secret"))).thenReturn(secret);
+        return request;
+    }
+
+}

+ 267 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java

@@ -0,0 +1,267 @@
+package cn.iocoder.yudao.module.system.service.oauth2;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
+import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ApproveMapper;
+import org.assertj.core.util.Lists;
+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.time.Duration;
+import java.util.*;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link OAuth2ApproveServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(OAuth2ApproveServiceImpl.class)
+public class OAuth2ApproveServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private OAuth2ApproveServiceImpl oauth2ApproveService;
+
+    @Resource
+    private OAuth2ApproveMapper oauth2ApproveMapper;
+
+    @MockBean
+    private OAuth2ClientService oauth2ClientService;
+
+    @Test
+    public void checkForPreApproval_clientAutoApprove() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        List<String> requestedScopes = Lists.newArrayList("read");
+        // mock 方法
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId)))
+                .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(requestedScopes));
+
+        // 调用
+        boolean success = oauth2ApproveService.checkForPreApproval(userId, userType,
+                clientId, requestedScopes);
+        // 断言
+        assertTrue(success);
+        List<OAuth2ApproveDO> result = oauth2ApproveMapper.selectList();
+        assertEquals(1, result.size());
+        assertEquals(userId, result.get(0).getUserId());
+        assertEquals(userType, result.get(0).getUserType());
+        assertEquals(clientId, result.get(0).getClientId());
+        assertEquals("read", result.get(0).getScope());
+        assertTrue(result.get(0).getApproved());
+        assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime()));
+    }
+
+    @Test
+    public void checkForPreApproval_approve() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        List<String> requestedScopes = Lists.newArrayList("read");
+        // mock 方法
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId)))
+                .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null));
+        // mock 数据
+        OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId)
+                .setUserType(userType).setClientId(clientId).setScope("read")
+                .setExpiresTime(addTime(Duration.ofDays(1))).setApproved(true); // 同意
+        oauth2ApproveMapper.insert(approve);
+
+        // 调用
+        boolean success = oauth2ApproveService.checkForPreApproval(userId, userType,
+                clientId, requestedScopes);
+        // 断言
+        assertTrue(success);
+    }
+
+    @Test
+    public void checkForPreApproval_reject() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        List<String> requestedScopes = Lists.newArrayList("read");
+        // mock 方法
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId)))
+                .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null));
+        // mock 数据
+        OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId)
+                .setUserType(userType).setClientId(clientId).setScope("read")
+                .setExpiresTime(addTime(Duration.ofDays(1))).setApproved(false); // 拒绝
+        oauth2ApproveMapper.insert(approve);
+
+        // 调用
+        boolean success = oauth2ApproveService.checkForPreApproval(userId, userType,
+                clientId, requestedScopes);
+        // 断言
+        assertFalse(success);
+    }
+
+    @Test
+    public void testUpdateAfterApproval_none() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+
+        // 调用
+        boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId,
+                null);
+        // 断言
+        assertTrue(success);
+        List<OAuth2ApproveDO> result = oauth2ApproveMapper.selectList();
+        assertEquals(0, result.size());
+    }
+
+    @Test
+    public void testUpdateAfterApproval_approved() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        Map<String, Boolean> requestedScopes = new LinkedHashMap<>(); // 有序,方便判断
+        requestedScopes.put("read", true);
+        requestedScopes.put("write", false);
+        // mock 方法
+
+        // 调用
+        boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId,
+                requestedScopes);
+        // 断言
+        assertTrue(success);
+        List<OAuth2ApproveDO> result = oauth2ApproveMapper.selectList();
+        assertEquals(2, result.size());
+        // read
+        assertEquals(userId, result.get(0).getUserId());
+        assertEquals(userType, result.get(0).getUserType());
+        assertEquals(clientId, result.get(0).getClientId());
+        assertEquals("read", result.get(0).getScope());
+        assertTrue(result.get(0).getApproved());
+        assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime()));
+        // write
+        assertEquals(userId, result.get(1).getUserId());
+        assertEquals(userType, result.get(1).getUserType());
+        assertEquals(clientId, result.get(1).getClientId());
+        assertEquals("write", result.get(1).getScope());
+        assertFalse(result.get(1).getApproved());
+        assertFalse(DateUtils.isExpired(result.get(1).getExpiresTime()));
+    }
+
+    @Test
+    public void testUpdateAfterApproval_reject() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        Map<String, Boolean> requestedScopes = new LinkedHashMap<>();
+        requestedScopes.put("write", false);
+        // mock 方法
+
+        // 调用
+        boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId,
+                requestedScopes);
+        // 断言
+        assertFalse(success);
+        List<OAuth2ApproveDO> result = oauth2ApproveMapper.selectList();
+        assertEquals(1, result.size());
+        // write
+        assertEquals(userId, result.get(0).getUserId());
+        assertEquals(userType, result.get(0).getUserType());
+        assertEquals(clientId, result.get(0).getClientId());
+        assertEquals("write", result.get(0).getScope());
+        assertFalse(result.get(0).getApproved());
+        assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime()));
+    }
+
+    @Test
+    public void testGetApproveList() {
+        // 准备参数
+        Long userId = 10L;
+        Integer userType = UserTypeEnum.ADMIN.getValue();
+        String clientId = randomString();
+        // mock 数据
+        OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId)
+                .setUserType(userType).setClientId(clientId).setExpiresTime(addTime(Duration.ofDays(1L)));
+        oauth2ApproveMapper.insert(approve); // 未过期
+        oauth2ApproveMapper.insert(ObjectUtil.clone(approve).setId(null)
+                .setExpiresTime(addTime(Duration.ofDays(-1L)))); // 已过期
+
+        // 调用
+        List<OAuth2ApproveDO> result = oauth2ApproveService.getApproveList(userId, userType, clientId);
+        // 断言
+        assertEquals(1, result.size());
+        assertPojoEquals(approve, result.get(0));
+    }
+
+    @Test
+    public void testSaveApprove_insert() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        String scope = randomString();
+        Boolean approved = randomBoolean();
+        Date expireTime = randomDay(1, 30);
+        // mock 方法
+
+        // 调用
+        oauth2ApproveService.saveApprove(userId, userType, clientId,
+                scope, approved, expireTime);
+        // 断言
+        List<OAuth2ApproveDO> result = oauth2ApproveMapper.selectList();
+        assertEquals(1, result.size());
+        assertEquals(userId, result.get(0).getUserId());
+        assertEquals(userType, result.get(0).getUserType());
+        assertEquals(clientId, result.get(0).getClientId());
+        assertEquals(scope, result.get(0).getScope());
+        assertEquals(approved, result.get(0).getApproved());
+        assertEquals(expireTime, result.get(0).getExpiresTime());
+    }
+
+    @Test
+    public void testSaveApprove_update() {
+        // mock 数据
+        OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class);
+        oauth2ApproveMapper.insert(approve);
+        // 准备参数
+        Long userId = approve.getUserId();
+        Integer userType = approve.getUserType();
+        String clientId = approve.getClientId();
+        String scope = approve.getScope();
+        Boolean approved = randomBoolean();
+        Date expireTime = randomDay(1, 30);
+        // mock 方法
+
+        // 调用
+        oauth2ApproveService.saveApprove(userId, userType, clientId,
+                scope, approved, expireTime);
+        // 断言
+        List<OAuth2ApproveDO> result = oauth2ApproveMapper.selectList();
+        assertEquals(1, result.size());
+        assertEquals(approve.getId(), result.get(0).getId());
+        assertEquals(userId, result.get(0).getUserId());
+        assertEquals(userType, result.get(0).getUserType());
+        assertEquals(clientId, result.get(0).getClientId());
+        assertEquals(scope, result.get(0).getScope());
+        assertEquals(approved, result.get(0).getApproved());
+        assertEquals(expireTime, result.get(0).getExpiresTime());
+    }
+
+}

+ 62 - 7
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/OAuth2ClientServiceImplTest.java → yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImplTest.java

@@ -1,5 +1,6 @@
-package cn.iocoder.yudao.module.system.service.auth;
+package cn.iocoder.yudao.module.system.service.oauth2;
 
+import cn.hutool.core.map.MapUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
@@ -9,13 +10,12 @@ import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.client.OAuth2Cl
 import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
 import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2ClientMapper;
 import cn.iocoder.yudao.module.system.mq.producer.auth.OAuth2ClientProducer;
-import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientServiceImpl;
-import org.junit.jupiter.api.Disabled;
 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.Collections;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
@@ -23,7 +23,7 @@ import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CLIENT_NOT_EXISTS;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.verify;
 
@@ -132,7 +132,31 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
     }
 
     @Test
-    @Disabled
+    public void testValidateClientIdExists_withId() {
+        // mock 数据
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou");
+        oauth2ClientMapper.insert(client);
+        // 准备参数
+        Long id = randomLongId();
+        String clientId = "tudou";
+
+        // 调用,不会报错
+        assertServiceException(() -> oauth2ClientService.validateClientIdExists(id, clientId), OAUTH2_CLIENT_EXISTS);
+    }
+
+    @Test
+    public void testValidateClientIdExists_noId() {
+        // mock 数据
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou");
+        oauth2ClientMapper.insert(client);
+        // 准备参数
+        String clientId = "tudou";
+
+        // 调用,不会报错
+        assertServiceException(() -> oauth2ClientService.validateClientIdExists(null, clientId), OAUTH2_CLIENT_EXISTS);
+    }
+
+    @Test
     public void testGetOAuth2ClientPage() {
        // mock 数据
        OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class, o -> { // 等会查询到
@@ -143,10 +167,10 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
        // 测试 name 不匹配
        oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰")));
        // 测试 status 不匹配
-       oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())));
+       oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
        // 准备参数
        OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO();
-       reqVO.setName("long");
+       reqVO.setName("");
        reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
 
        // 调用
@@ -157,4 +181,35 @@ public class OAuth2ClientServiceImplTest extends BaseDbUnitTest {
        assertPojoEquals(dbOAuth2Client, pageResult.getList().get(0));
     }
 
+    @Test
+    public void testValidOAuthClientFromCache() {
+        // mock 方法
+        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("default")
+                .setStatus(CommonStatusEnum.ENABLE.getStatus());
+        OAuth2ClientDO client02 = randomPojo(OAuth2ClientDO.class).setClientId("disable")
+                .setStatus(CommonStatusEnum.DISABLE.getStatus());
+        Map<String, OAuth2ClientDO> clientCache = MapUtil.<String, OAuth2ClientDO>builder()
+                .put(client.getClientId(), client)
+                .put(client02.getClientId(), client02).build();
+        oauth2ClientService.setClientCache(clientCache);
+
+        // 调用,并断言
+        assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache(randomString(),
+                null, null, null, null), OAUTH2_CLIENT_NOT_EXISTS);
+        assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("disable",
+                null, null, null, null), OAUTH2_CLIENT_DISABLE);
+        assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default",
+                randomString(), null, null, null), OAUTH2_CLIENT_CLIENT_SECRET_ERROR);
+        assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default",
+                null, randomString(), null, null), OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS);
+        assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default",
+                null, null, Collections.singleton(randomString()), null), OAUTH2_CLIENT_SCOPE_OVER);
+        assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default",
+                null, null, null, "test"), OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, "test");
+        // 成功调用
+        OAuth2ClientDO result = oauth2ClientService.validOAuthClientFromCache(client.getClientId(), client.getSecret(),
+                client.getAuthorizedGrantTypes().get(0), client.getScopes(), client.getRedirectUris().get(0));
+        assertPojoEquals(client, result);
+    }
+
 }

+ 100 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2CodeServiceImplTest.java

@@ -0,0 +1,100 @@
+package cn.iocoder.yudao.module.system.service.oauth2;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO;
+import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2CodeMapper;
+import org.assertj.core.util.Lists;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.time.Duration;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link OAuth2CodeServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(OAuth2CodeServiceImpl.class)
+class OAuth2CodeServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private OAuth2CodeServiceImpl oauth2CodeService;
+
+    @Resource
+    private OAuth2CodeMapper oauth2CodeMapper;
+
+    @Test
+    public void testCreateAuthorizationCode() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = RandomUtil.randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        List<String> scopes = Lists.newArrayList("read", "write");
+        String redirectUri = randomString();
+        String state = randomString();
+
+        // 调用
+        OAuth2CodeDO codeDO = oauth2CodeService.createAuthorizationCode(userId, userType, clientId,
+                scopes, redirectUri, state);
+        // 断言
+        OAuth2CodeDO dbCodeDO = oauth2CodeMapper.selectByCode(codeDO.getCode());
+        assertPojoEquals(codeDO, dbCodeDO, "createTime", "updateTime", "deleted");
+        assertEquals(userId, codeDO.getUserId());
+        assertEquals(userType, codeDO.getUserType());
+        assertEquals(clientId, codeDO.getClientId());
+        assertEquals(scopes, codeDO.getScopes());
+        assertEquals(redirectUri, codeDO.getRedirectUri());
+        assertEquals(state, codeDO.getState());
+        assertFalse(DateUtils.isExpired(codeDO.getExpiresTime()));
+    }
+
+    @Test
+    public void testConsumeAuthorizationCode_null() {
+        // 调用,并断言
+        assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(randomString()),
+                OAUTH2_CODE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testConsumeAuthorizationCode_expired() {
+        // 准备参数
+        String code = "test_code";
+        // mock 数据
+        OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code)
+                .setExpiresTime(addTime(Duration.ofDays(-1)));
+        oauth2CodeMapper.insert(codeDO);
+
+        // 调用,并断言
+        assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(code),
+                OAUTH2_CODE_EXPIRE);
+    }
+
+    @Test
+    public void testConsumeAuthorizationCode_success() {
+        // 准备参数
+        String code = "test_code";
+        // mock 数据
+        OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code)
+                .setExpiresTime(addTime(Duration.ofDays(1)));
+        oauth2CodeMapper.insert(codeDO);
+
+        // 调用
+        OAuth2CodeDO result = oauth2CodeService.consumeAuthorizationCode(code);
+        assertPojoEquals(codeDO, result);
+        assertNull(oauth2CodeMapper.selectByCode(code));
+    }
+
+}

+ 165 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImplTest.java

@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.system.service.oauth2;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2CodeDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
+import cn.iocoder.yudao.module.system.service.auth.AdminAuthService;
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.List;
+
+import static cn.hutool.core.util.RandomUtil.randomEle;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link OAuth2GrantServiceImpl} 的单元测试
+ *
+ * @author 芋道源码
+ */
+public class OAuth2GrantServiceImplTest extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private OAuth2GrantServiceImpl oauth2GrantService;
+
+    @Mock
+    private OAuth2TokenService oauth2TokenService;
+    @Mock
+    private OAuth2CodeService oauth2CodeService;
+    @Mock
+    private AdminAuthService adminAuthService;
+
+    @Test
+    public void testGrantImplicit() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        List<String> scopes = Lists.newArrayList("read", "write");
+        // mock 方法
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class);
+        when(oauth2TokenService.createAccessToken(eq(userId), eq(userType),
+                eq(clientId), eq(scopes))).thenReturn(accessTokenDO);
+
+        // 调用,并断言
+        assertPojoEquals(accessTokenDO, oauth2GrantService.grantImplicit(
+                userId, userType, clientId, scopes));
+    }
+
+    @Test
+    public void testGrantAuthorizationCodeForCode() {
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        List<String> scopes = Lists.newArrayList("read", "write");
+        String redirectUri = randomString();
+        String state = randomString();
+        // mock 方法
+        OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class);
+        when(oauth2CodeService.createAuthorizationCode(eq(userId), eq(userType),
+                eq(clientId), eq(scopes), eq(redirectUri), eq(state))).thenReturn(codeDO);
+
+        // 调用,并断言
+        assertEquals(codeDO.getCode(), oauth2GrantService.grantAuthorizationCodeForCode(userId, userType,
+                clientId, scopes, redirectUri, state));
+    }
+
+    @Test
+    public void testGrantAuthorizationCodeForAccessToken() {
+        // 准备参数
+        String clientId = randomString();
+        String code = randomString();
+        List<String> scopes = Lists.newArrayList("read", "write");
+        String redirectUri = randomString();
+        String state = randomString();
+        // mock 方法(code)
+        OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class, o -> {
+            o.setClientId(clientId);
+            o.setRedirectUri(redirectUri);
+            o.setState(state);
+            o.setScopes(scopes);
+        });
+        when(oauth2CodeService.consumeAuthorizationCode(eq(code))).thenReturn(codeDO);
+        // mock 方法(创建令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class);
+        when(oauth2TokenService.createAccessToken(eq(codeDO.getUserId()), eq(codeDO.getUserType()),
+                eq(codeDO.getClientId()), eq(codeDO.getScopes()))).thenReturn(accessTokenDO);
+
+        // 调用,并断言
+        assertPojoEquals(accessTokenDO, oauth2GrantService.grantAuthorizationCodeForAccessToken(
+                clientId, code, redirectUri, state));
+    }
+
+    @Test
+    public void testGrantPassword() {
+        // 准备参数
+        String username = randomString();
+        String password = randomString();
+        String clientId = randomString();
+        List<String> scopes = Lists.newArrayList("read", "write");
+        // mock 方法(认证)
+        AdminUserDO user = randomPojo(AdminUserDO.class);
+        when(adminAuthService.authenticate(eq(username), eq(password))).thenReturn(user);
+        // mock 方法(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class);
+        when(oauth2TokenService.createAccessToken(eq(user.getId()), eq(UserTypeEnum.ADMIN.getValue()),
+                eq(clientId), eq(scopes))).thenReturn(accessTokenDO);
+
+        // 调用,并断言
+        assertPojoEquals(accessTokenDO, oauth2GrantService.grantPassword(
+                username, password, clientId, scopes));
+    }
+
+    @Test
+    public void testGrantRefreshToken() {
+        // 准备参数
+        String refreshToken = randomString();
+        String clientId = randomString();
+        // mock 方法
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class);
+        when(oauth2TokenService.refreshAccessToken(eq(refreshToken), eq(clientId)))
+                .thenReturn(accessTokenDO);
+
+        // 调用,并断言
+        assertPojoEquals(accessTokenDO, oauth2GrantService.grantRefreshToken(
+                refreshToken, clientId));
+    }
+
+    @Test
+    public void testRevokeToken_clientIdError() {
+        // 准备参数
+        String clientId = randomString();
+        String accessToken = randomString();
+        // mock 方法
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class);
+        when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO);
+
+        // 调用,并断言
+        assertFalse(oauth2GrantService.revokeToken(clientId, accessToken));
+    }
+
+    @Test
+    public void testRevokeToken_success() {
+        // 准备参数
+        String clientId = randomString();
+        String accessToken = randomString();
+        // mock 方法(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setClientId(clientId);
+        when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO);
+        // mock 方法(移除)
+        when(oauth2TokenService.removeAccessToken(eq(accessToken))).thenReturn(accessTokenDO);
+
+        // 调用,并断言
+        assertTrue(oauth2GrantService.revokeToken(clientId, accessToken));
+    }
+
+}

+ 289 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java

@@ -0,0 +1,289 @@
+package cn.iocoder.yudao.module.system.service.oauth2;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
+import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper;
+import cn.iocoder.yudao.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
+import org.assertj.core.util.Lists;
+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.time.Duration;
+import java.util.Date;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link OAuth2TokenServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import({OAuth2TokenServiceImpl.class, OAuth2AccessTokenRedisDAO.class})
+public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
+
+    @Resource
+    private OAuth2TokenServiceImpl oauth2TokenService;
+
+    @Resource
+    private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
+    @Resource
+    private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;
+
+    @Resource
+    private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO;
+
+    @MockBean
+    private OAuth2ClientService oauth2ClientService;
+
+    @Test
+    public void testCreateAccessToken() {
+        TenantContextHolder.setTenantId(0L);
+        // 准备参数
+        Long userId = randomLongId();
+        Integer userType = RandomUtil.randomEle(UserTypeEnum.values()).getValue();
+        String clientId = randomString();
+        List<String> scopes = Lists.newArrayList("read", "write");
+        // mock 方法
+        OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId)
+                .setAccessTokenValiditySeconds(30).setRefreshTokenValiditySeconds(60);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO);
+
+        // 调用
+        OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, userType, clientId, scopes);
+        // 断言访问令牌
+        OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken());
+        assertPojoEquals(accessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted");
+        assertEquals(userId, accessTokenDO.getUserId());
+        assertEquals(userType, accessTokenDO.getUserType());
+        assertEquals(clientId, accessTokenDO.getClientId());
+        assertEquals(scopes, accessTokenDO.getScopes());
+        assertFalse(DateUtils.isExpired(accessTokenDO.getExpiresTime()));
+        // 断言访问令牌的缓存
+        OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken());
+        assertPojoEquals(accessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted");
+        // 断言刷新令牌
+        OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0);
+        assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted");
+        assertFalse(DateUtils.isExpired(refreshTokenDO.getExpiresTime()));
+    }
+
+    @Test
+    public void testRefreshAccessToken_null() {
+        // 准备参数
+        String refreshToken = randomString();
+        String clientId = randomString();
+        // mock 方法
+
+        // 调用,并断言
+        assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId),
+                new ErrorCode(400, "无效的刷新令牌"));
+    }
+
+    @Test
+    public void testRefreshAccessToken_clientIdError() {
+        // 准备参数
+        String refreshToken = randomString();
+        String clientId = randomString();
+        // mock 方法
+        OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO);
+        // mock 数据(访问令牌)
+        OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class)
+                .setRefreshToken(refreshToken).setClientId("error");
+        oauth2RefreshTokenMapper.insert(refreshTokenDO);
+
+        // 调用,并断言
+        assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId),
+                new ErrorCode(400, "刷新令牌的客户端编号不正确"));
+    }
+
+    @Test
+    public void testRefreshAccessToken_expired() {
+        // 准备参数
+        String refreshToken = randomString();
+        String clientId = randomString();
+        // mock 方法
+        OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO);
+        // mock 数据(访问令牌)
+        OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class)
+                .setRefreshToken(refreshToken).setClientId(clientId)
+                .setExpiresTime(addTime(Duration.ofDays(-1)));
+        oauth2RefreshTokenMapper.insert(refreshTokenDO);
+
+        // 调用,并断言
+        assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId),
+                new ErrorCode(401, "刷新令牌已过期"));
+    }
+
+    @Test
+    public void testRefreshAccessToken_success() {
+        TenantContextHolder.setTenantId(0L);
+        // 准备参数
+        String refreshToken = randomString();
+        String clientId = randomString();
+        // mock 方法
+        OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId)
+                .setAccessTokenValiditySeconds(30);
+        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO);
+        // mock 数据(访问令牌)
+        OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class)
+                .setRefreshToken(refreshToken).setClientId(clientId)
+                .setExpiresTime(addTime(Duration.ofDays(1)));
+        oauth2RefreshTokenMapper.insert(refreshTokenDO);
+        // mock 数据(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setRefreshToken(refreshToken);
+        oauth2AccessTokenMapper.insert(accessTokenDO);
+        oauth2AccessTokenRedisDAO.set(accessTokenDO);
+
+        // 调用
+        OAuth2AccessTokenDO newAccessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId);
+        // 断言,老的访问令牌被删除
+        assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()));
+        assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()));
+        // 断言,新的访问令牌
+        OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken());
+        assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted");
+        assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted",
+                "creator", "updater");
+        assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime()));
+        // 断言,新的访问令牌的缓存
+        OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(newAccessTokenDO.getAccessToken());
+        assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted");
+    }
+
+    @Test
+    public void testGetAccessToken() {
+        // mock 数据(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setExpiresTime(addTime(Duration.ofDays(1)));
+        oauth2AccessTokenMapper.insert(accessTokenDO);
+        // 准备参数
+        String accessToken = accessTokenDO.getAccessToken();
+
+        // 调用
+        OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken);
+        // 断言
+        assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted",
+                "creator", "updater");
+        assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "createTime", "updateTime", "deleted",
+                "creator", "updater");
+    }
+
+    @Test
+    public void testCheckAccessToken_null() {
+        // 调研,并断言
+        assertServiceException(() -> oauth2TokenService.checkAccessToken(randomString()),
+                new ErrorCode(401, "访问令牌不存在"));
+    }
+
+    @Test
+    public void testCheckAccessToken_expired() {
+        // mock 数据(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setExpiresTime(addTime(Duration.ofDays(-1)));
+        oauth2AccessTokenMapper.insert(accessTokenDO);
+        // 准备参数
+        String accessToken = accessTokenDO.getAccessToken();
+
+        // 调研,并断言
+        assertServiceException(() -> oauth2TokenService.checkAccessToken(accessToken),
+                new ErrorCode(401, "访问令牌已过期"));
+    }
+
+    @Test
+    public void testCheckAccessToken_success() {
+        // mock 数据(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setExpiresTime(addTime(Duration.ofDays(1)));
+        oauth2AccessTokenMapper.insert(accessTokenDO);
+        // 准备参数
+        String accessToken = accessTokenDO.getAccessToken();
+
+        // 调研,并断言
+        OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken);
+        // 断言
+        assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted",
+                "creator", "updater");
+    }
+
+    @Test
+    public void testRemoveAccessToken_null() {
+        // 调用,并断言
+        assertNull(oauth2TokenService.removeAccessToken(randomString()));
+    }
+
+    @Test
+    public void testRemoveAccessToken_success() {
+        // mock 数据(访问令牌)
+        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
+                .setExpiresTime(addTime(Duration.ofDays(1)));
+        oauth2AccessTokenMapper.insert(accessTokenDO);
+        // mock 数据(刷新令牌)
+        OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class)
+                .setRefreshToken(accessTokenDO.getRefreshToken());
+        oauth2RefreshTokenMapper.insert(refreshTokenDO);
+        // 调用
+        OAuth2AccessTokenDO result = oauth2TokenService.removeAccessToken(accessTokenDO.getAccessToken());
+        assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted",
+                "creator", "updater");
+        // 断言数据
+        assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()));
+        assertNull(oauth2RefreshTokenMapper.selectByRefreshToken(accessTokenDO.getRefreshToken()));
+        assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()));
+    }
+
+
+    @Test
+    public void testGetAccessTokenPage() {
+        // mock 数据
+        OAuth2AccessTokenDO dbAccessToken = randomPojo(OAuth2AccessTokenDO.class, o -> { // 等会查询到
+            o.setUserId(10L);
+            o.setUserType(1);
+            o.setClientId("test_client");
+            o.setExpiresTime(DateUtils.addTime(Duration.ofDays(1)));
+        });
+        oauth2AccessTokenMapper.insert(dbAccessToken);
+        // 测试 userId 不匹配
+        oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserId(20L)));
+        // 测试 userType 不匹配
+        oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserType(2)));
+        // 测试 userType 不匹配
+        oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setClientId("it_client")));
+        // 测试 expireTime 不匹配
+        oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setExpiresTime(new Date())));
+        // 准备参数
+        OAuth2AccessTokenPageReqVO reqVO = new OAuth2AccessTokenPageReqVO();
+        reqVO.setUserId(10L);
+        reqVO.setUserType(1);
+        reqVO.setClientId("test");
+
+        // 调用
+        PageResult<OAuth2AccessTokenDO> pageResult = oauth2TokenService.getAccessTokenPage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbAccessToken, pageResult.getList().get(0));
+    }
+
+}

+ 60 - 52
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceTest.java

@@ -3,15 +3,17 @@ package cn.iocoder.yudao.module.system.service.permission;
 import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleCreateReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleExportReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
 import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMapper;
+import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum;
 import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum;
 import cn.iocoder.yudao.module.system.mq.producer.permission.RoleProducer;
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
@@ -20,6 +22,7 @@ import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
 
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
 import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.max;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
@@ -155,61 +158,66 @@ public class RoleServiceTest extends BaseDbUnitTest {
     }
 
     @Test
-    public void testGetRoles_success() {
-        Map<Long, RoleDO> idRoleMap = new HashMap<>();
-        // 验证查询状态为1的角色
-        RoleDO roleDO1 = createRoleDO("role1", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1);
-        roleMapper.insert(roleDO1);
-        idRoleMap.put(roleDO1.getId(), roleDO1);
-
-        RoleDO roleDO2 = createRoleDO("role2", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1);
-        roleMapper.insert(roleDO2);
-        idRoleMap.put(roleDO2.getId(), roleDO2);
-
-        // 以下是排除的角色
-        RoleDO roleDO3 = createRoleDO("role3", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 2);
-        roleMapper.insert(roleDO3);
-
-        //调用
-        List<RoleDO> roles = roleService.getRoles(Arrays.asList(1));
-
-        //断言
-        assertEquals(2, roles.size());
-        roles.stream().forEach(r -> assertPojoEquals(idRoleMap.get(r.getId()), r));
+    public void testGetRoles() {
+        // mock 数据
+        RoleDO dbRole = randomPojo(RoleDO.class, o -> { // 等会查询到
+            o.setName("土豆");
+            o.setCode("tudou");
+            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
+            o.setCreateTime(DateUtils.buildTime(2022, 2, 8));
+        });
+        roleMapper.insert(dbRole);
+        // 测试 name 不匹配
+        roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setName("红薯")));
+        // 测试 code 不匹配
+        roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCode("hong")));
+        // 测试 createTime 不匹配
+        roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCreateTime(DateUtils.buildTime(2022, 2, 16))));
+        // 准备参数
+        RoleExportReqVO reqVO = new RoleExportReqVO();
+        reqVO.setName("土豆");
+        reqVO.setCode("tu");
+        reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        reqVO.setBeginTime(DateUtils.buildTime(2022, 2, 1));
+        reqVO.setEndTime(DateUtils.buildTime(2022, 2, 12));
 
+        // 调用
+        List<RoleDO> list = roleService.getRoleList(reqVO);
+        // 断言
+        assertEquals(1, list.size());
+        assertPojoEquals(dbRole, list.get(0));
     }
 
     @Test
-    public void testGetRolePage_success() {
-        Map<Long, RoleDO> idRoleMap = new HashMap<>();
-        // 验证名称包含"role", 状态为1,code为"code"的角色
-        // 第一页
-        RoleDO roleDO = createRoleDO("role1", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "code");
-        roleMapper.insert(roleDO);
-        idRoleMap.put(roleDO.getId(), roleDO);
-        // 第二页
-        roleDO = createRoleDO("role2", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "code");
-        roleMapper.insert(roleDO);
-
-        // 以下是排除的角色
-        roleDO = createRoleDO("role3", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 2, "code");
-        roleMapper.insert(roleDO);
-        roleDO = createRoleDO("role4", RoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "xxxxx");
-        roleMapper.insert(roleDO);
-
-        //调用
-        RolePageReqVO reqVO = randomPojo(RolePageReqVO.class, o -> {
-            o.setName("role");
-            o.setCode("code");
-            o.setStatus(1);
-            o.setPageNo(1);
-            o.setPageSize(1);
-            o.setBeginTime(null);
-            o.setEndTime(null);
+    public void testGetRolePage() {
+        // mock 数据
+        RoleDO dbRole = randomPojo(RoleDO.class, o -> { // 等会查询到
+            o.setName("土豆");
+            o.setCode("tudou");
+            o.setStatus(CommonStatusEnum.ENABLE.getStatus());
+            o.setCreateTime(DateUtils.buildTime(2022, 2, 8));
         });
-        PageResult<RoleDO> result = roleService.getRolePage(reqVO);
-        assertEquals(2, result.getTotal());
-        result.getList().stream().forEach(r -> assertPojoEquals(idRoleMap.get(r.getId()), r));
+        roleMapper.insert(dbRole);
+        // 测试 name 不匹配
+        roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setName("红薯")));
+        // 测试 code 不匹配
+        roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCode("hong")));
+        // 测试 createTime 不匹配
+        roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCreateTime(DateUtils.buildTime(2022, 2, 16))));
+        // 准备参数
+        RolePageReqVO reqVO = new RolePageReqVO();
+        reqVO.setName("土豆");
+        reqVO.setCode("tu");
+        reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        reqVO.setBeginTime(DateUtils.buildTime(2022, 2, 1));
+        reqVO.setEndTime(DateUtils.buildTime(2022, 2, 12));
+
+        // 调用
+        PageResult<RoleDO> pageResult = roleService.getRolePage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbRole, pageResult.getList().get(0));
     }
 
     @Test

+ 4 - 0
yudao-module-system/yudao-module-system-biz/src/test/resources/sql/clean.sql

@@ -21,3 +21,7 @@ DELETE FROM "system_tenant";
 DELETE FROM "system_tenant_package";
 DELETE FROM "system_sensitive_word";
 DELETE FROM "system_oauth2_client";
+DELETE FROM "system_oauth2_approve";
+DELETE FROM "system_oauth2_access_token";
+DELETE FROM "system_oauth2_refresh_token";
+DELETE FROM "system_oauth2_code";

+ 71 - 1
yudao-module-system/yudao-module-system-biz/src/test/resources/sql/create_tables.sql

@@ -482,9 +482,9 @@ CREATE TABLE IF NOT EXISTS "system_oauth2_client" (
   "access_token_validity_seconds" int NOT NULL,
   "refresh_token_validity_seconds" int NOT NULL,
   "redirect_uris" varchar NOT NULL,
-  "auto_approve" bit NOT NULL DEFAULT FALSE,
   "authorized_grant_types" varchar NOT NULL,
   "scopes" varchar NOT NULL DEFAULT '',
+  "auto_approve_scopes" varchar NOT NULL DEFAULT '',
   "authorities" varchar NOT NULL DEFAULT '',
   "resource_ids" varchar NOT NULL DEFAULT '',
   "additional_information" varchar NOT NULL DEFAULT '',
@@ -495,3 +495,73 @@ CREATE TABLE IF NOT EXISTS "system_oauth2_client" (
   "deleted" bit NOT NULL DEFAULT FALSE,
   PRIMARY KEY ("id")
 ) COMMENT 'OAuth2 客户端表';
+
+CREATE TABLE IF NOT EXISTS "system_oauth2_approve" (
+  "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+  "user_id" bigint NOT NULL,
+  "user_type" tinyint NOT NULL,
+  "client_id" varchar NOT NULL,
+  "scope" varchar NOT NULL,
+  "approved" bit NOT NULL DEFAULT FALSE,
+  "expires_time" datetime NOT NULL,
+  "creator" varchar DEFAULT '',
+  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  "updater" varchar DEFAULT '',
+  "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  "deleted" bit NOT NULL DEFAULT FALSE,
+  PRIMARY KEY ("id")
+) COMMENT 'OAuth2 批准表';
+
+CREATE TABLE IF NOT EXISTS "system_oauth2_access_token" (
+   "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+   "user_id" bigint NOT NULL,
+   "user_type" tinyint NOT NULL,
+   "access_token" varchar NOT NULL,
+   "refresh_token" varchar NOT NULL,
+   "client_id" varchar NOT NULL,
+   "scopes" varchar NOT NULL,
+   "approved" bit NOT NULL DEFAULT FALSE,
+   "expires_time" datetime NOT NULL,
+   "creator" varchar DEFAULT '',
+   "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+   "updater" varchar DEFAULT '',
+   "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+   "deleted" bit NOT NULL DEFAULT FALSE,
+   "tenant_id" bigint NOT NULL,
+   PRIMARY KEY ("id")
+) COMMENT 'OAuth2 访问令牌';
+
+CREATE TABLE IF NOT EXISTS "system_oauth2_refresh_token" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id" bigint NOT NULL,
+    "user_type" tinyint NOT NULL,
+    "refresh_token" varchar NOT NULL,
+    "client_id" varchar NOT NULL,
+    "scopes" varchar NOT NULL,
+    "approved" bit NOT NULL DEFAULT FALSE,
+    "expires_time" datetime NOT NULL,
+    "creator" varchar DEFAULT '',
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater" varchar DEFAULT '',
+    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted" bit NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
+) COMMENT 'OAuth2 刷新令牌';
+
+CREATE TABLE IF NOT EXISTS "system_oauth2_code" (
+     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+     "user_id" bigint NOT NULL,
+     "user_type" tinyint NOT NULL,
+     "code" varchar NOT NULL,
+     "client_id" varchar NOT NULL,
+     "scopes" varchar NOT NULL,
+     "expires_time" datetime NOT NULL,
+     "redirect_uri" varchar NOT NULL,
+     "state" varchar NOT NULL,
+     "creator" varchar DEFAULT '',
+     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+     "updater" varchar DEFAULT '',
+     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+     "deleted" bit NOT NULL DEFAULT FALSE,
+     PRIMARY KEY ("id")
+) COMMENT 'OAuth2 刷新令牌';

+ 1 - 1
yudao-ui-admin/src/views/sso.vue

@@ -37,7 +37,7 @@
               <el-form-item style="width:100%;">
                 <el-button :loading="loading" size="medium" type="primary" style="width:60%;"
                            @click.native.prevent="handleAuthorize(true)">
-                  <span v-if="!loading">统一授权</span>
+                  <span v-if="!loading">同意授权</span>
                   <span v-else>授 权 中...</span>
                 </el-button>
                 <el-button size="medium" style="width:36%"

+ 10 - 4
yudao-ui-admin/src/views/system/oauth2/client/index.vue

@@ -103,9 +103,10 @@
         <el-form-item label="刷新令牌的有效期" prop="refreshTokenValiditySeconds">
           <el-input-number v-model="form.refreshTokenValiditySeconds" placeholder="单位:秒" />
         </el-form-item>
-        <el-form-item label="可重定向的 URI 地址" prop="redirectUris">
-          <el-select v-model="form.redirectUris" multiple filterable allow-create placeholder="请输入可重定向的 URI 地址" style="width: 500px" >
-            <el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
+        <el-form-item label="授权类型" prop="authorizedGrantTypes">
+          <el-select v-model="form.authorizedGrantTypes" multiple filterable placeholder="请输入授权类型" style="width: 500px" >
+            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
           </el-select>
         </el-form-item>
         <el-form-item label="授权范围" prop="scopes">
@@ -113,11 +114,16 @@
             <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
           </el-select>
         </el-form-item>
-        <el-form-item label="自动授权" prop="autoApproveScopes">
+        <el-form-item label="自动授权范围" prop="autoApproveScopes">
           <el-select v-model="form.autoApproveScopes" multiple filterable placeholder="请输入授权范围" style="width: 500px" >
             <el-option v-for="scope in form.scopes" :key="scope" :label="scope" :value="scope"/>
           </el-select>
         </el-form-item>
+        <el-form-item label="可重定向的 URI 地址" prop="redirectUris">
+          <el-select v-model="form.redirectUris" multiple filterable allow-create placeholder="请输入可重定向的 URI 地址" style="width: 500px" >
+            <el-option v-for="redirectUri in form.redirectUris" :key="redirectUri" :label="redirectUri" :value="redirectUri"/>
+          </el-select>
+        </el-form-item>
         <el-form-item label="权限" prop="authorities">
           <el-select v-model="form.authorities" multiple filterable allow-create placeholder="请输入权限" style="width: 500px" >
             <el-option v-for="authority in form.authorities" :key="authority" :label="authority" :value="authority"/>

Some files were not shown because too many files changed in this diff