diff --git a/others/powerjob-mysql.sql b/others/powerjob-mysql.sql index 27568b18..6e31428a 100644 --- a/others/powerjob-mysql.sql +++ b/others/powerjob-mysql.sql @@ -1,20 +1,22 @@ /* - 官方 SQL 仅基于特定版本(MySQL8)导出,不一定兼容其他数据库,也不一定兼容其他版本。此 SQL 仅供参考。 - 如果您的数据库无法使用此 SQL,建议使用 SpringDataJPA 自带的建表能力,先在开发环境直连测试库自动建表,然后自行导出相关的 SQL 即可 +官方 SQL 仅基于特定版本(MySQL8)导出,不一定兼容其他数据库,也不一定兼容其他版本。此 SQL 仅供参考。 +如果您的数据库无法使用此 SQL,建议使用 SpringDataJPA 自带的建表能力,先在开发环境直连测试库自动建表,然后自行导出相关的 SQL 即可。 + */ +/* Navicat Premium Data Transfer Source Server : Local@3306 Source Server Type : MySQL Source Server Version : 80300 (8.3.0) Source Host : localhost:3306 - Source Schema : powerjob4 + Source Schema : powerjob5 Target Server Type : MySQL Target Server Version : 80300 (8.3.0) File Encoding : 65001 - Date: 02/03/2024 18:51:36 + Date: 11/08/2024 23:23:30 */ SET NAMES utf8mb4; @@ -27,10 +29,16 @@ DROP TABLE IF EXISTS `app_info`; CREATE TABLE `app_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `app_name` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, `current_server` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, `gmt_create` datetime(6) DEFAULT NULL, `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `namespace_id` bigint DEFAULT NULL, `password` varchar(255) DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uidx01_app_info` (`app_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; @@ -125,6 +133,27 @@ CREATE TABLE `job_info` ( KEY `idx01_job_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +-- ---------------------------- +-- Table structure for namespace +-- ---------------------------- +DROP TABLE IF EXISTS `namespace`; +CREATE TABLE `namespace` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `code` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, + `dept` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `status` int DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `token` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_namespace` (`code`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + -- ---------------------------- -- Table structure for oms_lock -- ---------------------------- @@ -138,6 +167,21 @@ CREATE TABLE `oms_lock` ( `ownerip` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uidx01_oms_lock` (`lock_name`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for pwjb_user_info +-- ---------------------------- +DROP TABLE IF EXISTS `pwjb_user_info`; +CREATE TABLE `pwjb_user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- @@ -154,24 +198,62 @@ CREATE TABLE `server_info` ( KEY `idx01_server_info` (`gmt_modified`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +-- ---------------------------- +-- Table structure for sundry +-- ---------------------------- +DROP TABLE IF EXISTS `sundry`; +CREATE TABLE `sundry` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `pkey` varchar(255) DEFAULT NULL, + `skey` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_sundry` (`pkey`,`skey`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + -- ---------------------------- -- Table structure for user_info -- ---------------------------- DROP TABLE IF EXISTS `user_info`; CREATE TABLE `user_info` ( `id` bigint NOT NULL AUTO_INCREMENT, + `account_type` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `extra` varchar(255) DEFAULT NULL, `gmt_create` datetime(6) DEFAULT NULL, `gmt_modified` datetime(6) DEFAULT NULL, + `nick` varchar(255) DEFAULT NULL, + `origin_username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `phone` varchar(255) DEFAULT NULL, + `status` int DEFAULT NULL, + `token_login_verify_info` varchar(255) DEFAULT NULL, `username` varchar(255) DEFAULT NULL, `web_hook` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), - KEY `uidx01_user_info` (`username`), + UNIQUE KEY `uidx01_user_name` (`username`), KEY `uidx02_user_info` (`email`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for user_role +-- ---------------------------- +DROP TABLE IF EXISTS `user_role`; +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `role` int DEFAULT NULL, + `scope` int DEFAULT NULL, + `target` bigint DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `uidx01_user_id` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for workflow_info @@ -244,4 +326,3 @@ CREATE TABLE `workflow_node_info` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; SET FOREIGN_KEY_CHECKS = 1; - diff --git a/others/sql/schema/powerjob_mysql_4.3.9.sql b/others/sql/schema/powerjob_mysql_4.3.9.sql new file mode 100644 index 00000000..b6a790c9 --- /dev/null +++ b/others/sql/schema/powerjob_mysql_4.3.9.sql @@ -0,0 +1,243 @@ +/* + Navicat Premium Data Transfer + + Source Server : Local@3306 + Source Server Type : MySQL + Source Server Version : 80300 (8.3.0) + Source Host : localhost:3306 + Source Schema : powerjob4 + + Target Server Type : MySQL + Target Server Version : 80300 (8.3.0) + File Encoding : 65001 + + Date: 02/03/2024 18:51:36 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for app_info +-- ---------------------------- +DROP TABLE IF EXISTS `app_info`; +CREATE TABLE `app_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_name` varchar(255) DEFAULT NULL, + `current_server` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_app_info` (`app_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for container_info +-- ---------------------------- +DROP TABLE IF EXISTS `container_info`; +CREATE TABLE `container_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint DEFAULT NULL, + `container_name` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `last_deploy_time` datetime(6) DEFAULT NULL, + `source_info` varchar(255) DEFAULT NULL, + `source_type` int DEFAULT NULL, + `status` int DEFAULT NULL, + `version` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_container_info` (`app_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for instance_info +-- ---------------------------- +DROP TABLE IF EXISTS `instance_info`; +CREATE TABLE `instance_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `actual_trigger_time` bigint DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `expected_trigger_time` bigint DEFAULT NULL, + `finished_time` bigint DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `instance_id` bigint DEFAULT NULL, + `instance_params` longtext, + `job_id` bigint DEFAULT NULL, + `job_params` longtext, + `last_report_time` bigint DEFAULT NULL, + `result` longtext, + `running_times` bigint DEFAULT NULL, + `status` int DEFAULT NULL, + `task_tracker_address` varchar(255) DEFAULT NULL, + `type` int DEFAULT NULL, + `wf_instance_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_instance_info` (`job_id`,`status`), + KEY `idx02_instance_info` (`app_id`,`status`), + KEY `idx03_instance_info` (`instance_id`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for job_info +-- ---------------------------- +DROP TABLE IF EXISTS `job_info`; +CREATE TABLE `job_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `advanced_runtime_config` varchar(255) DEFAULT NULL, + `alarm_config` varchar(255) DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `concurrency` int DEFAULT NULL, + `designated_workers` varchar(255) DEFAULT NULL, + `dispatch_strategy` int DEFAULT NULL, + `dispatch_strategy_config` varchar(255) DEFAULT NULL, + `execute_type` int DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `instance_retry_num` int DEFAULT NULL, + `instance_time_limit` bigint DEFAULT NULL, + `job_description` varchar(255) DEFAULT NULL, + `job_name` varchar(255) DEFAULT NULL, + `job_params` longtext, + `lifecycle` varchar(255) DEFAULT NULL, + `log_config` varchar(255) DEFAULT NULL, + `max_instance_num` int DEFAULT NULL, + `max_worker_count` int DEFAULT NULL, + `min_cpu_cores` double NOT NULL, + `min_disk_space` double NOT NULL, + `min_memory_space` double NOT NULL, + `next_trigger_time` bigint DEFAULT NULL, + `notify_user_ids` varchar(255) DEFAULT NULL, + `processor_info` varchar(255) DEFAULT NULL, + `processor_type` int DEFAULT NULL, + `status` int DEFAULT NULL, + `tag` varchar(255) DEFAULT NULL, + `task_retry_num` int DEFAULT NULL, + `time_expression` varchar(255) DEFAULT NULL, + `time_expression_type` int DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_job_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for oms_lock +-- ---------------------------- +DROP TABLE IF EXISTS `oms_lock`; +CREATE TABLE `oms_lock` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `lock_name` varchar(255) DEFAULT NULL, + `max_lock_time` bigint DEFAULT NULL, + `ownerip` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_oms_lock` (`lock_name`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for server_info +-- ---------------------------- +DROP TABLE IF EXISTS `server_info`; +CREATE TABLE `server_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `ip` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_server_info` (`ip`), + KEY `idx01_server_info` (`gmt_modified`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for user_info +-- ---------------------------- +DROP TABLE IF EXISTS `user_info`; +CREATE TABLE `user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `email` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `phone` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + `web_hook` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `uidx01_user_info` (`username`), + KEY `uidx02_user_info` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_info`; +CREATE TABLE `workflow_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `lifecycle` varchar(255) DEFAULT NULL, + `max_wf_instance_num` int DEFAULT NULL, + `next_trigger_time` bigint DEFAULT NULL, + `notify_user_ids` varchar(255) DEFAULT NULL, + `pedag` longtext, + `status` int DEFAULT NULL, + `time_expression` varchar(255) DEFAULT NULL, + `time_expression_type` int DEFAULT NULL, + `wf_description` varchar(255) DEFAULT NULL, + `wf_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_workflow_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_instance_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_instance_info`; +CREATE TABLE `workflow_instance_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `actual_trigger_time` bigint DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `dag` longtext, + `expected_trigger_time` bigint DEFAULT NULL, + `finished_time` bigint DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `parent_wf_instance_id` bigint DEFAULT NULL, + `result` longtext, + `status` int DEFAULT NULL, + `wf_context` longtext, + `wf_init_params` longtext, + `wf_instance_id` bigint DEFAULT NULL, + `workflow_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_wf_instance` (`wf_instance_id`), + KEY `idx01_wf_instance` (`workflow_id`,`status`,`app_id`,`expected_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_node_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_node_info`; +CREATE TABLE `workflow_node_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint NOT NULL, + `enable` bit(1) NOT NULL, + `extra` longtext, + `gmt_create` datetime(6) NOT NULL, + `gmt_modified` datetime(6) NOT NULL, + `job_id` bigint DEFAULT NULL, + `node_name` varchar(255) DEFAULT NULL, + `node_params` longtext, + `skip_when_failed` bit(1) NOT NULL, + `type` int DEFAULT NULL, + `workflow_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_workflow_node_info` (`workflow_id`,`gmt_create`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/others/sql/schema/powerjob_mysql_5.0.1.sql b/others/sql/schema/powerjob_mysql_5.0.1.sql new file mode 100644 index 00000000..da04575c --- /dev/null +++ b/others/sql/schema/powerjob_mysql_5.0.1.sql @@ -0,0 +1,323 @@ +/* + Navicat Premium Data Transfer + + Source Server : Local@3306 + Source Server Type : MySQL + Source Server Version : 80300 (8.3.0) + Source Host : localhost:3306 + Source Schema : powerjob5 + + Target Server Type : MySQL + Target Server Version : 80300 (8.3.0) + File Encoding : 65001 + + Date: 16/03/2024 22:07:31 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for app_info +-- ---------------------------- +DROP TABLE IF EXISTS `app_info`; +CREATE TABLE `app_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_name` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, + `current_server` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `namespace_id` bigint DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_app_info` (`app_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for container_info +-- ---------------------------- +DROP TABLE IF EXISTS `container_info`; +CREATE TABLE `container_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint DEFAULT NULL, + `container_name` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `last_deploy_time` datetime(6) DEFAULT NULL, + `source_info` varchar(255) DEFAULT NULL, + `source_type` int DEFAULT NULL, + `status` int DEFAULT NULL, + `version` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_container_info` (`app_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for instance_info +-- ---------------------------- +DROP TABLE IF EXISTS `instance_info`; +CREATE TABLE `instance_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `actual_trigger_time` bigint DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `expected_trigger_time` bigint DEFAULT NULL, + `finished_time` bigint DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `instance_id` bigint DEFAULT NULL, + `instance_params` longtext, + `job_id` bigint DEFAULT NULL, + `job_params` longtext, + `last_report_time` bigint DEFAULT NULL, + `result` longtext, + `running_times` bigint DEFAULT NULL, + `status` int DEFAULT NULL, + `task_tracker_address` varchar(255) DEFAULT NULL, + `type` int DEFAULT NULL, + `wf_instance_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_instance_info` (`job_id`,`status`), + KEY `idx02_instance_info` (`app_id`,`status`), + KEY `idx03_instance_info` (`instance_id`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for job_info +-- ---------------------------- +DROP TABLE IF EXISTS `job_info`; +CREATE TABLE `job_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `advanced_runtime_config` varchar(255) DEFAULT NULL, + `alarm_config` varchar(255) DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `concurrency` int DEFAULT NULL, + `designated_workers` varchar(255) DEFAULT NULL, + `dispatch_strategy` int DEFAULT NULL, + `dispatch_strategy_config` varchar(255) DEFAULT NULL, + `execute_type` int DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `instance_retry_num` int DEFAULT NULL, + `instance_time_limit` bigint DEFAULT NULL, + `job_description` varchar(255) DEFAULT NULL, + `job_name` varchar(255) DEFAULT NULL, + `job_params` longtext, + `lifecycle` varchar(255) DEFAULT NULL, + `log_config` varchar(255) DEFAULT NULL, + `max_instance_num` int DEFAULT NULL, + `max_worker_count` int DEFAULT NULL, + `min_cpu_cores` double NOT NULL, + `min_disk_space` double NOT NULL, + `min_memory_space` double NOT NULL, + `next_trigger_time` bigint DEFAULT NULL, + `notify_user_ids` varchar(255) DEFAULT NULL, + `processor_info` varchar(255) DEFAULT NULL, + `processor_type` int DEFAULT NULL, + `status` int DEFAULT NULL, + `tag` varchar(255) DEFAULT NULL, + `task_retry_num` int DEFAULT NULL, + `time_expression` varchar(255) DEFAULT NULL, + `time_expression_type` int DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_job_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for namespace +-- ---------------------------- +DROP TABLE IF EXISTS `namespace`; +CREATE TABLE `namespace` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `code` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, + `dept` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `status` int DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `token` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_namespace` (`code`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for oms_lock +-- ---------------------------- +DROP TABLE IF EXISTS `oms_lock`; +CREATE TABLE `oms_lock` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `lock_name` varchar(255) DEFAULT NULL, + `max_lock_time` bigint DEFAULT NULL, + `ownerip` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_oms_lock` (`lock_name`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for pwjb_user_info +-- ---------------------------- +DROP TABLE IF EXISTS `pwjb_user_info`; +CREATE TABLE `pwjb_user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_username` (`username`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for server_info +-- ---------------------------- +DROP TABLE IF EXISTS `server_info`; +CREATE TABLE `server_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `ip` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_server_info` (`ip`), + KEY `idx01_server_info` (`gmt_modified`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for sundry +-- ---------------------------- +DROP TABLE IF EXISTS `sundry`; +CREATE TABLE `sundry` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `pkey` varchar(255) DEFAULT NULL, + `skey` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_sundry` (`pkey`,`skey`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for user_info +-- ---------------------------- +DROP TABLE IF EXISTS `user_info`; +CREATE TABLE `user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `account_type` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `nick` varchar(255) DEFAULT NULL, + `origin_username` varchar(255) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `phone` varchar(255) DEFAULT NULL, + `status` int DEFAULT NULL, + `token_login_verify_info` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + `web_hook` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_user_name` (`username`), + KEY `uidx02_user_info` (`email`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for user_role +-- ---------------------------- +DROP TABLE IF EXISTS `user_role`; +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `role` int DEFAULT NULL, + `scope` int DEFAULT NULL, + `target` bigint DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `uidx01_user_id` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_info`; +CREATE TABLE `workflow_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `lifecycle` varchar(255) DEFAULT NULL, + `max_wf_instance_num` int DEFAULT NULL, + `next_trigger_time` bigint DEFAULT NULL, + `notify_user_ids` varchar(255) DEFAULT NULL, + `pedag` longtext, + `status` int DEFAULT NULL, + `time_expression` varchar(255) DEFAULT NULL, + `time_expression_type` int DEFAULT NULL, + `wf_description` varchar(255) DEFAULT NULL, + `wf_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_workflow_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_instance_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_instance_info`; +CREATE TABLE `workflow_instance_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `actual_trigger_time` bigint DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `dag` longtext, + `expected_trigger_time` bigint DEFAULT NULL, + `finished_time` bigint DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `parent_wf_instance_id` bigint DEFAULT NULL, + `result` longtext, + `status` int DEFAULT NULL, + `wf_context` longtext, + `wf_init_params` longtext, + `wf_instance_id` bigint DEFAULT NULL, + `workflow_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_wf_instance` (`wf_instance_id`), + KEY `idx01_wf_instance` (`workflow_id`,`status`,`app_id`,`expected_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_node_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_node_info`; +CREATE TABLE `workflow_node_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint NOT NULL, + `enable` bit(1) NOT NULL, + `extra` longtext, + `gmt_create` datetime(6) NOT NULL, + `gmt_modified` datetime(6) NOT NULL, + `job_id` bigint DEFAULT NULL, + `node_name` varchar(255) DEFAULT NULL, + `node_params` longtext, + `skip_when_failed` bit(1) NOT NULL, + `type` int DEFAULT NULL, + `workflow_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_workflow_node_info` (`workflow_id`,`gmt_create`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/others/sql/schema/powerjob_mysql_5.1.0.sql b/others/sql/schema/powerjob_mysql_5.1.0.sql new file mode 100644 index 00000000..0d594a8c --- /dev/null +++ b/others/sql/schema/powerjob_mysql_5.1.0.sql @@ -0,0 +1,323 @@ +/* + Navicat Premium Data Transfer + + Source Server : Local@3306 + Source Server Type : MySQL + Source Server Version : 80300 (8.3.0) + Source Host : localhost:3306 + Source Schema : powerjob5 + + Target Server Type : MySQL + Target Server Version : 80300 (8.3.0) + File Encoding : 65001 + + Date: 11/08/2024 23:23:30 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for app_info +-- ---------------------------- +DROP TABLE IF EXISTS `app_info`; +CREATE TABLE `app_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_name` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, + `current_server` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `namespace_id` bigint DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_app_info` (`app_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for container_info +-- ---------------------------- +DROP TABLE IF EXISTS `container_info`; +CREATE TABLE `container_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint DEFAULT NULL, + `container_name` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `last_deploy_time` datetime(6) DEFAULT NULL, + `source_info` varchar(255) DEFAULT NULL, + `source_type` int DEFAULT NULL, + `status` int DEFAULT NULL, + `version` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_container_info` (`app_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for instance_info +-- ---------------------------- +DROP TABLE IF EXISTS `instance_info`; +CREATE TABLE `instance_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `actual_trigger_time` bigint DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `expected_trigger_time` bigint DEFAULT NULL, + `finished_time` bigint DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `instance_id` bigint DEFAULT NULL, + `instance_params` longtext, + `job_id` bigint DEFAULT NULL, + `job_params` longtext, + `last_report_time` bigint DEFAULT NULL, + `result` longtext, + `running_times` bigint DEFAULT NULL, + `status` int DEFAULT NULL, + `task_tracker_address` varchar(255) DEFAULT NULL, + `type` int DEFAULT NULL, + `wf_instance_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_instance_info` (`job_id`,`status`), + KEY `idx02_instance_info` (`app_id`,`status`), + KEY `idx03_instance_info` (`instance_id`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for job_info +-- ---------------------------- +DROP TABLE IF EXISTS `job_info`; +CREATE TABLE `job_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `advanced_runtime_config` varchar(255) DEFAULT NULL, + `alarm_config` varchar(255) DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `concurrency` int DEFAULT NULL, + `designated_workers` varchar(255) DEFAULT NULL, + `dispatch_strategy` int DEFAULT NULL, + `dispatch_strategy_config` varchar(255) DEFAULT NULL, + `execute_type` int DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `instance_retry_num` int DEFAULT NULL, + `instance_time_limit` bigint DEFAULT NULL, + `job_description` varchar(255) DEFAULT NULL, + `job_name` varchar(255) DEFAULT NULL, + `job_params` longtext, + `lifecycle` varchar(255) DEFAULT NULL, + `log_config` varchar(255) DEFAULT NULL, + `max_instance_num` int DEFAULT NULL, + `max_worker_count` int DEFAULT NULL, + `min_cpu_cores` double NOT NULL, + `min_disk_space` double NOT NULL, + `min_memory_space` double NOT NULL, + `next_trigger_time` bigint DEFAULT NULL, + `notify_user_ids` varchar(255) DEFAULT NULL, + `processor_info` varchar(255) DEFAULT NULL, + `processor_type` int DEFAULT NULL, + `status` int DEFAULT NULL, + `tag` varchar(255) DEFAULT NULL, + `task_retry_num` int DEFAULT NULL, + `time_expression` varchar(255) DEFAULT NULL, + `time_expression_type` int DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_job_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for namespace +-- ---------------------------- +DROP TABLE IF EXISTS `namespace`; +CREATE TABLE `namespace` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `code` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, + `dept` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `status` int DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `token` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_namespace` (`code`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for oms_lock +-- ---------------------------- +DROP TABLE IF EXISTS `oms_lock`; +CREATE TABLE `oms_lock` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `lock_name` varchar(255) DEFAULT NULL, + `max_lock_time` bigint DEFAULT NULL, + `ownerip` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_oms_lock` (`lock_name`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for pwjb_user_info +-- ---------------------------- +DROP TABLE IF EXISTS `pwjb_user_info`; +CREATE TABLE `pwjb_user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_username` (`username`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for server_info +-- ---------------------------- +DROP TABLE IF EXISTS `server_info`; +CREATE TABLE `server_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `ip` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_server_info` (`ip`), + KEY `idx01_server_info` (`gmt_modified`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for sundry +-- ---------------------------- +DROP TABLE IF EXISTS `sundry`; +CREATE TABLE `sundry` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `pkey` varchar(255) DEFAULT NULL, + `skey` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_sundry` (`pkey`,`skey`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for user_info +-- ---------------------------- +DROP TABLE IF EXISTS `user_info`; +CREATE TABLE `user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `account_type` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `nick` varchar(255) DEFAULT NULL, + `origin_username` varchar(255) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `phone` varchar(255) DEFAULT NULL, + `status` int DEFAULT NULL, + `token_login_verify_info` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + `web_hook` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_user_name` (`username`), + KEY `uidx02_user_info` (`email`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for user_role +-- ---------------------------- +DROP TABLE IF EXISTS `user_role`; +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `role` int DEFAULT NULL, + `scope` int DEFAULT NULL, + `target` bigint DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `uidx01_user_id` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_info`; +CREATE TABLE `workflow_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `lifecycle` varchar(255) DEFAULT NULL, + `max_wf_instance_num` int DEFAULT NULL, + `next_trigger_time` bigint DEFAULT NULL, + `notify_user_ids` varchar(255) DEFAULT NULL, + `pedag` longtext, + `status` int DEFAULT NULL, + `time_expression` varchar(255) DEFAULT NULL, + `time_expression_type` int DEFAULT NULL, + `wf_description` varchar(255) DEFAULT NULL, + `wf_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_workflow_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_instance_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_instance_info`; +CREATE TABLE `workflow_instance_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `actual_trigger_time` bigint DEFAULT NULL, + `app_id` bigint DEFAULT NULL, + `dag` longtext, + `expected_trigger_time` bigint DEFAULT NULL, + `finished_time` bigint DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `parent_wf_instance_id` bigint DEFAULT NULL, + `result` longtext, + `status` int DEFAULT NULL, + `wf_context` longtext, + `wf_init_params` longtext, + `wf_instance_id` bigint DEFAULT NULL, + `workflow_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_wf_instance` (`wf_instance_id`), + KEY `idx01_wf_instance` (`workflow_id`,`status`,`app_id`,`expected_trigger_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for workflow_node_info +-- ---------------------------- +DROP TABLE IF EXISTS `workflow_node_info`; +CREATE TABLE `workflow_node_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint NOT NULL, + `enable` bit(1) NOT NULL, + `extra` longtext, + `gmt_create` datetime(6) NOT NULL, + `gmt_modified` datetime(6) NOT NULL, + `job_id` bigint DEFAULT NULL, + `node_name` varchar(255) DEFAULT NULL, + `node_params` longtext, + `skip_when_failed` bit(1) NOT NULL, + `type` int DEFAULT NULL, + `workflow_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx01_workflow_node_info` (`workflow_id`,`gmt_create`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/others/sql/upgrade/README.md b/others/sql/upgrade/README.md new file mode 100644 index 00000000..ea511137 --- /dev/null +++ b/others/sql/upgrade/README.md @@ -0,0 +1,7 @@ +由于存在不同数据库、不同版本的升级,官方能给出的 upgrade SQL 相对有限,大家可参考以下方式自行生成升级 SQL: + +- 【官方脚本】参考官方每个版本的数据库全库建表文件(项目 others - sql - schema),自行进行字段 DIFF + +- 【自己动手版】导出当前您的 powerjob 数据库表结构,同时创建一个测试库,让 5.x 版本的 server 直连该测试库,自动建表。分别拿到两个版本的表结构 SQL 后,借用工具生产 update SQL 即可(navigate 等数据库管理软件均支持结构对比) + +参考文档:https://www.yuque.com/powerjob/guidence/upgrade \ No newline at end of file diff --git a/others/sql/upgrade/v4.3.x-v5.0.x.sql b/others/sql/upgrade/v4.3.x-v5.0.x.sql new file mode 100644 index 00000000..ee9f2a12 --- /dev/null +++ b/others/sql/upgrade/v4.3.x-v5.0.x.sql @@ -0,0 +1,88 @@ +-- Upgrade SQL FROM 4.1.x to 4.2.x +-- ---------------------------- +-- Table change for app_info +-- ---------------------------- +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE `app_info` ADD COLUMN `creator` bigint NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `extra` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `modifier` bigint NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `namespace_id` bigint NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `tags` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `title` varchar(255) NULL DEFAULT NULL; + +-- ---------------------------- +-- Table change for user_info +-- ---------------------------- +ALTER TABLE `user_info` ADD COLUMN `account_type` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD COLUMN `nick` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD COLUMN `origin_username` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD COLUMN `token_login_verify_info` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD UNIQUE INDEX `uidx01_user_name`(`username` ASC) USING BTREE; + +-- ---------------------------- +-- new table 'namespace' +-- ---------------------------- +CREATE TABLE `namespace` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `code` varchar(255) NULL DEFAULT NULL, + `creator` bigint NULL DEFAULT NULL, + `dept` varchar(255) NULL DEFAULT NULL, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `modifier` bigint NULL DEFAULT NULL, + `name` varchar(255) NULL DEFAULT NULL, + `status` int NULL DEFAULT NULL, + `tags` varchar(255) NULL DEFAULT NULL, + `token` varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uidx01_namespace`(`code` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + + +-- ---------------------------- +-- new table 'pwjb_user_info' +-- ---------------------------- +CREATE TABLE `pwjb_user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `password` varchar(255) NULL DEFAULT NULL, + `username` varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uidx01_username`(`username` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- new table 'sundry' +-- ---------------------------- +CREATE TABLE `sundry` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` varchar(255) NULL DEFAULT NULL, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `pkey` varchar(255) NULL DEFAULT NULL, + `skey` varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uidx01_sundry`(`pkey` ASC, `skey` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + + +-- ---------------------------- +-- new table 'user_role' +-- ---------------------------- +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `role` int NULL DEFAULT NULL, + `scope` int NULL DEFAULT NULL, + `target` bigint NULL DEFAULT NULL, + `user_id` bigint NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `uidx01_user_id`(`user_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; \ No newline at end of file diff --git a/pom.xml b/pom.xml index f9bf1b28..5780f8b5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ tech.powerjob powerjob - 4.3.9 + 5.1.0 pom powerjob http://www.powerjob.tech diff --git a/powerjob-client/pom.xml b/powerjob-client/pom.xml index ad852573..0219cfb5 100644 --- a/powerjob-client/pom.xml +++ b/powerjob-client/pom.xml @@ -5,18 +5,19 @@ powerjob tech.powerjob - 4.3.9 + 5.1.0 4.0.0 powerjob-client - 4.3.9 + 5.1.0 jar 5.9.1 + 1.2.13 1.2.83 - 4.3.9 + 5.1.0 3.2.4 @@ -44,6 +45,13 @@ ${junit.version} test + + + ch.qos.logback + logback-classic + ${logback.version} + test + diff --git a/powerjob-client/src/main/java/tech/powerjob/client/ClientConfig.java b/powerjob-client/src/main/java/tech/powerjob/client/ClientConfig.java new file mode 100644 index 00000000..abe29e87 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/ClientConfig.java @@ -0,0 +1,71 @@ +package tech.powerjob.client; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import tech.powerjob.client.common.Protocol; +import tech.powerjob.client.extension.ClientExtension; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 客户端配置 + * + * @author 程序帕鲁 + * @since 2024/2/20 + */ +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class ClientConfig implements Serializable { + + /** + * 执行器 AppName + */ + private String appName; + + /** + * 执行器密码 + */ + private String password; + + /** + * 地址列表,支持格式: + * - IP:Port, eg: 192.168.1.1:7700 + * - 域名, eg: powerjob.apple-inc.com + */ + private List addressList; + + /** + * 客户端通讯协议 + */ + private Protocol protocol = Protocol.HTTP; + + /** + * 连接超时时间 + */ + private Integer connectionTimeout; + /** + * 指定了等待服务器响应数据的最长时间。更具体地说,这是从服务器开始返回响应数据(包括HTTP头和数据)后,客户端读取数据的超时时间 + */ + private Integer readTimeout; + /** + * 指定了向服务器发送数据的最长时间。这是从客户端开始发送数据(如POST请求的正文)到数据完全发送出去的时间 + */ + private Integer writeTimeout; + + /** + * 默认携带的请求头 + * 用于流量被基础设施识别 + */ + private Map defaultHeaders; + + /** + * 客户端行为扩展 + */ + private ClientExtension clientExtension; +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/PowerJobClient.java b/powerjob-client/src/main/java/tech/powerjob/client/PowerJobClient.java index e0240e5c..f2e668ce 100644 --- a/powerjob-client/src/main/java/tech/powerjob/client/PowerJobClient.java +++ b/powerjob-client/src/main/java/tech/powerjob/client/PowerJobClient.java @@ -1,28 +1,32 @@ package tech.powerjob.client; import com.alibaba.fastjson.JSON; -import tech.powerjob.common.enums.InstanceStatus; -import tech.powerjob.common.OmsConstant; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; +import tech.powerjob.client.service.PowerRequestBody; +import tech.powerjob.client.service.RequestService; +import tech.powerjob.client.service.impl.ClusterRequestServiceOkHttp3Impl; import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.common.enums.EncryptType; +import tech.powerjob.common.enums.InstanceStatus; import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.request.http.SaveWorkflowNodeRequest; import tech.powerjob.common.request.http.SaveWorkflowRequest; import tech.powerjob.common.request.query.JobInfoQuery; import tech.powerjob.common.response.*; -import tech.powerjob.common.utils.CommonUtils; -import tech.powerjob.common.utils.HttpUtils; import tech.powerjob.common.serialize.JsonUtils; -import com.google.common.collect.Lists; -import lombok.extern.slf4j.Slf4j; -import okhttp3.FormBody; -import okhttp3.MediaType; -import okhttp3.RequestBody; -import org.apache.commons.lang3.StringUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.common.utils.DigestUtils; +import java.io.Closeable; import java.io.IOException; import java.util.List; -import java.util.Objects; +import java.util.Map; import static tech.powerjob.client.TypeStore.*; @@ -33,14 +37,44 @@ import static tech.powerjob.client.TypeStore.*; * @since 2020/4/15 */ @Slf4j -public class PowerJobClient implements IPowerJobClient { +public class PowerJobClient implements IPowerJobClient, Closeable { private Long appId; - private String currentAddress; - private final List allAddress; + + private final RequestService requestService; - private static final String URL_PATTERN = "http://%s%s%s"; + public PowerJobClient(ClientConfig config) { + + List addressList = config.getAddressList(); + String appName = config.getAppName(); + CommonUtils.requireNonNull(addressList, "addressList can't be null!"); + CommonUtils.requireNonNull(appName, "appName can't be null"); + + this.requestService = new ClusterRequestServiceOkHttp3Impl(config); + + AppAuthRequest appAuthRequest = new AppAuthRequest(); + appAuthRequest.setAppName(appName); + appAuthRequest.setEncryptedPassword(DigestUtils.md5(config.getPassword())); + appAuthRequest.setEncryptType(EncryptType.MD5.getCode()); + + String assertResponse = requestService.request(OpenAPIConstant.AUTH_APP, PowerRequestBody.newJsonRequestBody(appAuthRequest)); + + if (StringUtils.isNotEmpty(assertResponse)) { + ResultDTO resultDTO = JSON.parseObject(assertResponse, APP_AUTH_RESULT_TYPE); + if (resultDTO.isSuccess()) { + appId = resultDTO.getData().getAppId(); + } else { + throw new PowerJobException(resultDTO.getMessage()); + } + } + + if (appId == null) { + throw new PowerJobException("appId is null, please check your config"); + } + + log.info("[PowerJobClient] [INIT] {}'s PowerJobClient bootstrap successfully", appName); + } /** * Init PowerJobClient with domain, appName and password. * @@ -49,7 +83,7 @@ public class PowerJobClient implements IPowerJobClient { * @param password password of the application */ public PowerJobClient(String domain, String appName, String password) { - this(Lists.newArrayList(domain), appName, password); + this(new ClientConfig().setAppName(appName).setPassword(password).setAddressList(Lists.newArrayList(domain))); } @@ -61,48 +95,7 @@ public class PowerJobClient implements IPowerJobClient { * @param password password of the application */ public PowerJobClient(List addressList, String appName, String password) { - - CommonUtils.requireNonNull(addressList, "addressList can't be null!"); - CommonUtils.requireNonNull(appName, "appName can't be null"); - - allAddress = addressList; - for (String addr : addressList) { - String url = getUrl(OpenAPIConstant.ASSERT, addr); - try { - String result = assertApp(appName, password, url); - if (StringUtils.isNotEmpty(result)) { - ResultDTO resultDTO = JSON.parseObject(result, LONG_RESULT_TYPE); - if (resultDTO.isSuccess()) { - appId = resultDTO.getData(); - currentAddress = addr; - break; - } else { - throw new PowerJobException(resultDTO.getMessage()); - } - } - } catch (IOException ignore) { - // - } - } - - if (StringUtils.isEmpty(currentAddress)) { - throw new PowerJobException("no server available for PowerJobClient"); - } - log.info("[PowerJobClient] {}'s PowerJobClient bootstrap successfully, using server: {}", appName, currentAddress); - } - - private static String assertApp(String appName, String password, String url) throws IOException { - FormBody.Builder builder = new FormBody.Builder() - .add("appName", appName); - if (password != null) { - builder.add("password", password); - } - return HttpUtils.post(url, builder.build()); - } - - - private static String getUrl(String path, String address) { - return String.format(URL_PATTERN, address, OpenAPIConstant.WEB_PATH, path); + this(new ClientConfig().setAppName(appName).setPassword(password).setAddressList(addressList)); } /* ************* Job 区 ************* */ @@ -118,9 +111,7 @@ public class PowerJobClient implements IPowerJobClient { public ResultDTO saveJob(SaveJobInfoRequest request) { request.setAppId(appId); - MediaType jsonType = MediaType.parse(OmsConstant.JSON_MEDIA_TYPE); - String json = JSON.toJSONString(request); - String post = postHA(OpenAPIConstant.SAVE_JOB, RequestBody.create(jsonType, json)); + String post = requestService.request(OpenAPIConstant.SAVE_JOB, PowerRequestBody.newJsonRequestBody(request)); return JSON.parseObject(post, LONG_RESULT_TYPE); } @@ -133,21 +124,20 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO copyJob(Long jobId) { - RequestBody body = new FormBody.Builder() - .add("jobId", jobId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.COPY_JOB, body); + Map param = Maps.newHashMap(); + param.put("jobId", jobId.toString()); + param.put("appId", appId.toString()); + + String post = requestService.request(OpenAPIConstant.COPY_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, LONG_RESULT_TYPE); } @Override public ResultDTO exportJob(Long jobId) { - RequestBody body = new FormBody.Builder() - .add("jobId", jobId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.EXPORT_JOB, body); + Map param = Maps.newHashMap(); + param.put("jobId", jobId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.EXPORT_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, SAVE_JOB_INFO_REQUEST_RESULT_TYPE); } @@ -159,11 +149,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO fetchJob(Long jobId) { - RequestBody body = new FormBody.Builder() - .add("jobId", jobId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.FETCH_JOB, body); + Map param = Maps.newHashMap(); + param.put("jobId", jobId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.FETCH_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, JOB_RESULT_TYPE); } @@ -174,10 +163,9 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO> fetchAllJob() { - RequestBody body = new FormBody.Builder() - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.FETCH_ALL_JOB, body); + Map param = Maps.newHashMap(); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.FETCH_ALL_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, LIST_JOB_RESULT_TYPE); } @@ -190,9 +178,7 @@ public class PowerJobClient implements IPowerJobClient { @Override public ResultDTO> queryJob(JobInfoQuery powerQuery) { powerQuery.setAppIdEq(appId); - MediaType jsonType = MediaType.parse(OmsConstant.JSON_MEDIA_TYPE); - String json = JsonUtils.toJSONStringUnsafe(powerQuery); - String post = postHA(OpenAPIConstant.QUERY_JOB, RequestBody.create(jsonType, json)); + String post = requestService.request(OpenAPIConstant.QUERY_JOB, PowerRequestBody.newJsonRequestBody(powerQuery)); return JSON.parseObject(post, LIST_JOB_RESULT_TYPE); } @@ -204,11 +190,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO disableJob(Long jobId) { - RequestBody body = new FormBody.Builder() - .add("jobId", jobId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.DISABLE_JOB, body); + Map param = Maps.newHashMap(); + param.put("jobId", jobId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.DISABLE_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -220,11 +205,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO enableJob(Long jobId) { - RequestBody body = new FormBody.Builder() - .add("jobId", jobId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.ENABLE_JOB, body); + Map param = Maps.newHashMap(); + param.put("jobId", jobId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.ENABLE_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -236,11 +220,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO deleteJob(Long jobId) { - RequestBody body = new FormBody.Builder() - .add("jobId", jobId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.DELETE_JOB, body); + Map param = Maps.newHashMap(); + param.put("jobId", jobId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.DELETE_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -254,15 +237,16 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO runJob(Long jobId, String instanceParams, long delayMS) { - FormBody.Builder builder = new FormBody.Builder() - .add("jobId", jobId.toString()) - .add("appId", appId.toString()) - .add("delay", String.valueOf(delayMS)); + + Map param = Maps.newHashMap(); + param.put("jobId", jobId.toString()); + param.put("appId", appId.toString()); + param.put("delay", String.valueOf(delayMS)); if (StringUtils.isNotEmpty(instanceParams)) { - builder.add("instanceParams", instanceParams); + param.put("instanceParams", instanceParams); } - String post = postHA(OpenAPIConstant.RUN_JOB, builder.build()); + String post = requestService.request(OpenAPIConstant.RUN_JOB, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, LONG_RESULT_TYPE); } @@ -280,11 +264,12 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO stopInstance(Long instanceId) { - RequestBody body = new FormBody.Builder() - .add("instanceId", instanceId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.STOP_INSTANCE, body); + + Map param = Maps.newHashMap(); + param.put("instanceId", instanceId.toString()); + param.put("appId", appId.toString()); + + String post = requestService.request(OpenAPIConstant.STOP_INSTANCE, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -297,11 +282,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO cancelInstance(Long instanceId) { - RequestBody body = new FormBody.Builder() - .add("instanceId", instanceId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.CANCEL_INSTANCE, body); + Map param = Maps.newHashMap(); + param.put("instanceId", instanceId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.CANCEL_INSTANCE, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -314,11 +298,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO retryInstance(Long instanceId) { - RequestBody body = new FormBody.Builder() - .add("instanceId", instanceId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.RETRY_INSTANCE, body); + Map param = Maps.newHashMap(); + param.put("instanceId", instanceId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.RETRY_INSTANCE, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -330,10 +313,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO fetchInstanceStatus(Long instanceId) { - RequestBody body = new FormBody.Builder() - .add("instanceId", instanceId.toString()) - .build(); - String post = postHA(OpenAPIConstant.FETCH_INSTANCE_STATUS, body); + Map param = Maps.newHashMap(); + param.put("instanceId", instanceId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.FETCH_INSTANCE_STATUS, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, INTEGER_RESULT_TYPE); } @@ -345,10 +328,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO fetchInstanceInfo(Long instanceId) { - RequestBody body = new FormBody.Builder() - .add("instanceId", instanceId.toString()) - .build(); - String post = postHA(OpenAPIConstant.FETCH_INSTANCE_INFO, body); + Map param = Maps.newHashMap(); + param.put("instanceId", instanceId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.FETCH_INSTANCE_INFO, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, INSTANCE_RESULT_TYPE); } @@ -364,10 +347,9 @@ public class PowerJobClient implements IPowerJobClient { @Override public ResultDTO saveWorkflow(SaveWorkflowRequest request) { request.setAppId(appId); - MediaType jsonType = MediaType.parse(OmsConstant.JSON_MEDIA_TYPE); // 中坑记录:用 FastJSON 序列化会导致 Server 接收时 pEWorkflowDAG 为 null,无语.jpg String json = JsonUtils.toJSONStringUnsafe(request); - String post = postHA(OpenAPIConstant.SAVE_WORKFLOW, RequestBody.create(jsonType, json)); + String post = requestService.request(OpenAPIConstant.SAVE_WORKFLOW, PowerRequestBody.newJsonRequestBody(json)); return JSON.parseObject(post, LONG_RESULT_TYPE); } @@ -379,11 +361,12 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO copyWorkflow(Long workflowId) { - RequestBody body = new FormBody.Builder() - .add("workflowId", workflowId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.COPY_WORKFLOW, body); + + Map param = Maps.newHashMap(); + param.put("workflowId", workflowId.toString()); + param.put("appId", appId.toString()); + + String post = requestService.request(OpenAPIConstant.COPY_WORKFLOW, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, LONG_RESULT_TYPE); } @@ -399,9 +382,9 @@ public class PowerJobClient implements IPowerJobClient { for (SaveWorkflowNodeRequest saveWorkflowNodeRequest : requestList) { saveWorkflowNodeRequest.setAppId(appId); } - MediaType jsonType = MediaType.parse(OmsConstant.JSON_MEDIA_TYPE); + String json = JsonUtils.toJSONStringUnsafe(requestList); - String post = postHA(OpenAPIConstant.SAVE_WORKFLOW_NODE, RequestBody.create(jsonType, json)); + String post = requestService.request(OpenAPIConstant.SAVE_WORKFLOW_NODE, PowerRequestBody.newJsonRequestBody(json)); return JSON.parseObject(post, WF_NODE_LIST_RESULT_TYPE); } @@ -415,11 +398,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO fetchWorkflow(Long workflowId) { - RequestBody body = new FormBody.Builder() - .add("workflowId", workflowId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.FETCH_WORKFLOW, body); + Map param = Maps.newHashMap(); + param.put("workflowId", workflowId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.FETCH_WORKFLOW, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, WF_RESULT_TYPE); } @@ -431,11 +413,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO disableWorkflow(Long workflowId) { - RequestBody body = new FormBody.Builder() - .add("workflowId", workflowId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.DISABLE_WORKFLOW, body); + Map param = Maps.newHashMap(); + param.put("workflowId", workflowId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.DISABLE_WORKFLOW, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -447,11 +428,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO enableWorkflow(Long workflowId) { - RequestBody body = new FormBody.Builder() - .add("workflowId", workflowId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.ENABLE_WORKFLOW, body); + Map param = Maps.newHashMap(); + param.put("workflowId", workflowId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.ENABLE_WORKFLOW, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -463,11 +443,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO deleteWorkflow(Long workflowId) { - RequestBody body = new FormBody.Builder() - .add("workflowId", workflowId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.DELETE_WORKFLOW, body); + Map param = Maps.newHashMap(); + param.put("workflowId", workflowId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.DELETE_WORKFLOW, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -481,14 +460,17 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO runWorkflow(Long workflowId, String initParams, long delayMS) { - FormBody.Builder builder = new FormBody.Builder() - .add("workflowId", workflowId.toString()) - .add("appId", appId.toString()) - .add("delay", String.valueOf(delayMS)); + + Map param = Maps.newHashMap(); + param.put("workflowId", workflowId.toString()); + param.put("appId", appId.toString()); + param.put("delay", String.valueOf(delayMS)); + + if (StringUtils.isNotEmpty(initParams)) { - builder.add("initParams", initParams); + param.put("initParams", initParams); } - String post = postHA(OpenAPIConstant.RUN_WORKFLOW, builder.build()); + String post = requestService.request(OpenAPIConstant.RUN_WORKFLOW, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, LONG_RESULT_TYPE); } @@ -506,11 +488,12 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO stopWorkflowInstance(Long wfInstanceId) { - RequestBody body = new FormBody.Builder() - .add("wfInstanceId", wfInstanceId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.STOP_WORKFLOW_INSTANCE, body); + + Map param = Maps.newHashMap(); + param.put("wfInstanceId", wfInstanceId.toString()); + param.put("appId", appId.toString()); + + String post = requestService.request(OpenAPIConstant.STOP_WORKFLOW_INSTANCE, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -522,11 +505,10 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO retryWorkflowInstance(Long wfInstanceId) { - RequestBody body = new FormBody.Builder() - .add("wfInstanceId", wfInstanceId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.RETRY_WORKFLOW_INSTANCE, body); + Map param = Maps.newHashMap(); + param.put("wfInstanceId", wfInstanceId.toString()); + param.put("appId", appId.toString()); + String post = requestService.request(OpenAPIConstant.RETRY_WORKFLOW_INSTANCE, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -539,12 +521,13 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO markWorkflowNodeAsSuccess(Long wfInstanceId, Long nodeId) { - RequestBody body = new FormBody.Builder() - .add("wfInstanceId", wfInstanceId.toString()) - .add("nodeId", nodeId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.MARK_WORKFLOW_NODE_AS_SUCCESS, body); + + Map param = Maps.newHashMap(); + param.put("wfInstanceId", wfInstanceId.toString()); + param.put("appId", appId.toString()); + param.put("nodeId", nodeId.toString()); + + String post = requestService.request(OpenAPIConstant.MARK_WORKFLOW_NODE_AS_SUCCESS, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, VOID_RESULT_TYPE); } @@ -556,47 +539,17 @@ public class PowerJobClient implements IPowerJobClient { */ @Override public ResultDTO fetchWorkflowInstanceInfo(Long wfInstanceId) { - RequestBody body = new FormBody.Builder() - .add("wfInstanceId", wfInstanceId.toString()) - .add("appId", appId.toString()) - .build(); - String post = postHA(OpenAPIConstant.FETCH_WORKFLOW_INSTANCE_INFO, body); + + Map param = Maps.newHashMap(); + param.put("wfInstanceId", wfInstanceId.toString()); + param.put("appId", appId.toString()); + + String post = requestService.request(OpenAPIConstant.FETCH_WORKFLOW_INSTANCE_INFO, PowerRequestBody.newFormRequestBody(param)); return JSON.parseObject(post, WF_INSTANCE_RESULT_TYPE); } - - private String postHA(String path, RequestBody requestBody) { - - // 先尝试默认地址 - String url = getUrl(path, currentAddress); - try { - String res = HttpUtils.post(url, requestBody); - if (StringUtils.isNotEmpty(res)) { - return res; - } - } catch (IOException e) { - log.warn("[PowerJobClient] request url:{} failed, reason is {}.", url, e.toString()); - } - - // 失败,开始重试 - for (String addr : allAddress) { - if (Objects.equals(addr, currentAddress)) { - continue; - } - url = getUrl(path, addr); - try { - String res = HttpUtils.post(url, requestBody); - if (StringUtils.isNotEmpty(res)) { - log.warn("[PowerJobClient] server change: from({}) -> to({}).", currentAddress, addr); - currentAddress = addr; - return res; - } - } catch (IOException e) { - log.warn("[PowerJobClient] request url:{} failed, reason is {}.", url, e.toString()); - } - } - - log.error("[PowerJobClient] do post for path: {} failed because of no server available in {}.", path, allAddress); - throw new PowerJobException("no server available when send post request"); + @Override + public void close() throws IOException { + requestService.close(); } } diff --git a/powerjob-client/src/main/java/tech/powerjob/client/TypeStore.java b/powerjob-client/src/main/java/tech/powerjob/client/TypeStore.java index 15c8e285..c3c23406 100644 --- a/powerjob-client/src/main/java/tech/powerjob/client/TypeStore.java +++ b/powerjob-client/src/main/java/tech/powerjob/client/TypeStore.java @@ -1,6 +1,7 @@ package tech.powerjob.client; import com.alibaba.fastjson.TypeReference; +import tech.powerjob.client.module.AppAuthResult; import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.response.*; @@ -14,6 +15,7 @@ import java.util.List; */ public class TypeStore { + public static final TypeReference> APP_AUTH_RESULT_TYPE = new TypeReference>(){}; public static final TypeReference> VOID_RESULT_TYPE = new TypeReference>(){}; public static final TypeReference> INTEGER_RESULT_TYPE = new TypeReference>(){}; diff --git a/powerjob-client/src/main/java/tech/powerjob/client/common/Protocol.java b/powerjob-client/src/main/java/tech/powerjob/client/common/Protocol.java new file mode 100644 index 00000000..5168a642 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/common/Protocol.java @@ -0,0 +1,28 @@ +package tech.powerjob.client.common; + +import lombok.Getter; + +/** + * Protocol + * + * @author tjq + * @since 2024/2/20 + */ +@Getter +public enum Protocol { + + HTTP("http"), + + HTTPS("https"); + + private final String protocol; + + Protocol(String protocol) { + this.protocol = protocol; + } + + @Override + public String toString() { + return protocol; + } +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/extension/ClientExtension.java b/powerjob-client/src/main/java/tech/powerjob/client/extension/ClientExtension.java new file mode 100644 index 00000000..28e98f6a --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/extension/ClientExtension.java @@ -0,0 +1,19 @@ +package tech.powerjob.client.extension; + +import java.util.List; + +/** + * 扩展服务 + * + * @author tjq + * @since 2024/8/11 + */ +public interface ClientExtension { + + /** + * 动态提供地址,适用于 server 部署在动态集群上的场景 + * @param context 上下文 + * @return 地址,格式要求同 ClientConfig#addressList + */ + List addressProvider(ExtensionContext context); +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/extension/ExtensionContext.java b/powerjob-client/src/main/java/tech/powerjob/client/extension/ExtensionContext.java new file mode 100644 index 00000000..c1d36b97 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/extension/ExtensionContext.java @@ -0,0 +1,10 @@ +package tech.powerjob.client.extension; + +/** + * 扩展上下文 + * + * @author tjq + * @since 2024/8/11 + */ +public class ExtensionContext { +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthRequest.java b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthRequest.java new file mode 100644 index 00000000..0e8c84dc --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthRequest.java @@ -0,0 +1,39 @@ +package tech.powerjob.client.module; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.Map; + +/** + * App 鉴权请求 + * + * @author tjq + * @since 2024/2/19 + */ +@Getter +@Setter +@ToString +public class AppAuthRequest implements Serializable { + + /** + * 应用名称 + */ + private String appName; + /** + * 加密后密码 + */ + private String encryptedPassword; + + /** + * 加密类型 + */ + private String encryptType; + + /** + * 额外参数,方便开发者传递其他参数 + */ + private Map extra; +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthResult.java b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthResult.java new file mode 100644 index 00000000..1e402994 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/module/AppAuthResult.java @@ -0,0 +1,30 @@ +package tech.powerjob.client.module; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.Map; + +/** + * App 鉴权响应 + * + * @author tjq + * @since 2024/2/21 + */ +@Getter +@Setter +@ToString +public class AppAuthResult implements Serializable { + + private Long appId; + + private String token; + + /** + * 额外参数 + * 有安全需求的开发者可执行扩展 + */ + private Map extra; +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/HttpResponse.java b/powerjob-client/src/main/java/tech/powerjob/client/service/HttpResponse.java new file mode 100644 index 00000000..5b1b68e6 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/HttpResponse.java @@ -0,0 +1,26 @@ +package tech.powerjob.client.service; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Map; + +/** + * HTTP 响应 + * + * @author tjq + * @since 2024/8/10 + */ +@Data +@Accessors(chain = true) +public class HttpResponse implements Serializable { + + private boolean success; + + private int code; + + private String response; + + private Map headers; +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/PowerRequestBody.java b/powerjob-client/src/main/java/tech/powerjob/client/service/PowerRequestBody.java new file mode 100644 index 00000000..7b0ef102 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/PowerRequestBody.java @@ -0,0 +1,47 @@ +package tech.powerjob.client.service; + +import com.google.common.collect.Maps; +import lombok.Getter; +import tech.powerjob.common.enums.MIME; + +import java.util.Map; + +/** + * 请求体 + * + * @author tjq + * @since 2024/8/10 + */ +@Getter +public class PowerRequestBody { + + private MIME mime; + + private Object payload; + + private final Map headers = Maps.newHashMap(); + + private PowerRequestBody() { + } + + public static PowerRequestBody newJsonRequestBody(Object data) { + PowerRequestBody powerRequestBody = new PowerRequestBody(); + powerRequestBody.mime = MIME.APPLICATION_JSON; + powerRequestBody.payload = data; + return powerRequestBody; + } + + public static PowerRequestBody newFormRequestBody(Map form) { + PowerRequestBody powerRequestBody = new PowerRequestBody(); + powerRequestBody.mime = MIME.APPLICATION_FORM; + powerRequestBody.payload = form; + return powerRequestBody; + } + + public void addHeaders(Map hs) { + if (hs == null || hs.isEmpty()) { + return; + } + this.headers.putAll(hs); + } +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/RequestService.java b/powerjob-client/src/main/java/tech/powerjob/client/service/RequestService.java new file mode 100644 index 00000000..14c2c8c6 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/RequestService.java @@ -0,0 +1,15 @@ +package tech.powerjob.client.service; + +import java.io.Closeable; + +/** + * 请求服务 + * + * @author tjq + * @since 2024/2/20 + */ +public interface RequestService extends Closeable { + + + String request(String path, PowerRequestBody powerRequestBody); +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/impl/AppAuthClusterRequestService.java b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/AppAuthClusterRequestService.java new file mode 100644 index 00000000..241460a6 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/AppAuthClusterRequestService.java @@ -0,0 +1,107 @@ +package tech.powerjob.client.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Maps; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import tech.powerjob.client.ClientConfig; +import tech.powerjob.client.TypeStore; +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; +import tech.powerjob.client.service.HttpResponse; +import tech.powerjob.client.service.PowerRequestBody; +import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.common.enums.EncryptType; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.utils.DigestUtils; +import tech.powerjob.common.utils.MapUtils; + +import java.util.Map; + +/** + * 封装鉴权相关逻辑 + * + * @author tjq + * @since 2024/2/21 + */ +@Slf4j +abstract class AppAuthClusterRequestService extends ClusterRequestService { + + protected AppAuthResult appAuthResult; + + public AppAuthClusterRequestService(ClientConfig config) { + super(config); + } + + + @Override + public String request(String path, PowerRequestBody powerRequestBody) { + // 若不存在 appAuthResult,则首先进行鉴权 + if (appAuthResult == null) { + refreshAppAuthResult(); + } + + HttpResponse httpResponse = doRequest(path, powerRequestBody); + + // 如果 auth 成功,则代表请求有效,直接返回 + String authStatus = MapUtils.getString(httpResponse.getHeaders(), OpenAPIConstant.RESPONSE_HEADER_AUTH_STATUS); + if (Boolean.TRUE.toString().equalsIgnoreCase(authStatus)) { + return httpResponse.getResponse(); + } + + // 否则请求无效,刷新鉴权后重新请求 + log.warn("[PowerJobClient] auth failed[authStatus: {}], try to refresh the auth info", authStatus); + refreshAppAuthResult(); + httpResponse = doRequest(path, powerRequestBody); + + // 只要请求不失败,直接返回(如果鉴权失败则返回鉴权错误信息,server 保证 response 永远非空) + return httpResponse.getResponse(); + } + + private HttpResponse doRequest(String path, PowerRequestBody powerRequestBody) { + + // 添加鉴权信息 + Map authHeaders = buildAuthHeader(); + powerRequestBody.addHeaders(authHeaders); + + HttpResponse httpResponse = clusterHaRequest(path, powerRequestBody); + + // 任何请求不成功,都直接报错 + if (!httpResponse.isSuccess()) { + throw new PowerJobException("REMOTE_SERVER_INNER_EXCEPTION"); + } + return httpResponse; + } + + private Map buildAuthHeader() { + Map authHeader = Maps.newHashMap(); + authHeader.put(OpenAPIConstant.REQUEST_HEADER_APP_ID, String.valueOf(appAuthResult.getAppId())); + authHeader.put(OpenAPIConstant.REQUEST_HEADER_ACCESS_TOKEN, appAuthResult.getToken()); + return authHeader; + } + + @SneakyThrows + private void refreshAppAuthResult() { + AppAuthRequest appAuthRequest = buildAppAuthRequest(); + HttpResponse httpResponse = clusterHaRequest(OpenAPIConstant.AUTH_APP, PowerRequestBody.newJsonRequestBody(appAuthRequest)); + if (!httpResponse.isSuccess()) { + throw new PowerJobException("AUTH_APP_EXCEPTION!"); + } + ResultDTO authResultDTO = JSONObject.parseObject(httpResponse.getResponse(), TypeStore.APP_AUTH_RESULT_TYPE); + if (!authResultDTO.isSuccess()) { + throw new PowerJobException("AUTH_FAILED_" + authResultDTO.getMessage()); + } + + log.warn("[PowerJobClient] refresh auth info successfully!"); + this.appAuthResult = authResultDTO.getData(); + } + + protected AppAuthRequest buildAppAuthRequest() { + AppAuthRequest appAuthRequest = new AppAuthRequest(); + appAuthRequest.setAppName(config.getAppName()); + appAuthRequest.setEncryptedPassword(DigestUtils.md5(config.getPassword())); + appAuthRequest.setEncryptType(EncryptType.MD5.getCode()); + return appAuthRequest; + } +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestService.java b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestService.java new file mode 100644 index 00000000..d842201f --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestService.java @@ -0,0 +1,140 @@ +package tech.powerjob.client.service.impl; + +import lombok.extern.slf4j.Slf4j; +import tech.powerjob.client.ClientConfig; +import tech.powerjob.client.extension.ClientExtension; +import tech.powerjob.client.extension.ExtensionContext; +import tech.powerjob.client.service.HttpResponse; +import tech.powerjob.client.service.PowerRequestBody; +import tech.powerjob.client.service.RequestService; +import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.utils.CollectionUtils; + +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Objects; + +/** + * 集群请求服务 + * 封装网络相关通用逻辑 + * + * @author tjq + * @since 2024/2/21 + */ +@Slf4j +abstract class ClusterRequestService implements RequestService { + + protected final ClientConfig config; + + /** + * 当前地址(上次请求成功的地址) + */ + protected String currentAddress; + + /** + * 地址格式 + * 协议://域名/OpenAPI/子路径 + */ + protected static final String URL_PATTERN = "%s://%s%s%s"; + + /** + * 默认超时时间 + */ + protected static final Integer DEFAULT_TIMEOUT_SECONDS = 2; + + protected static final int HTTP_SUCCESS_CODE = 200; + + public ClusterRequestService(ClientConfig config) { + this.config = config; + this.currentAddress = config.getAddressList().get(0); + } + + /** + * 具体某一次 HTTP 请求的实现 + * @param url 完整请求地址 + * @param body 请求体 + * @return 响应 + * @throws IOException 异常 + */ + protected abstract HttpResponse sendHttpRequest(String url, PowerRequestBody body) throws IOException; + + /** + * 封装集群请求能力 + * @param path 请求 PATH + * @param powerRequestBody 请求体 + * @return 响应 + */ + protected HttpResponse clusterHaRequest(String path, PowerRequestBody powerRequestBody) { + + // 先尝试默认地址 + String url = getUrl(path, currentAddress); + try { + return sendHttpRequest(url, powerRequestBody); + } catch (IOException e) { + log.warn("[ClusterRequestService] request url:{} failed, reason is {}.", url, e.toString()); + } + + List addressList = fetchAddressList(); + + // 失败,开始重试 + for (String addr : addressList) { + if (Objects.equals(addr, currentAddress)) { + continue; + } + url = getUrl(path, addr); + try { + HttpResponse res = sendHttpRequest(url, powerRequestBody); + log.warn("[ClusterRequestService] server change: from({}) -> to({}).", currentAddress, addr); + currentAddress = addr; + return res; + } catch (IOException e) { + log.warn("[ClusterRequestService] request url:{} failed, reason is {}.", url, e.toString()); + } + } + + log.error("[ClusterRequestService] do post for path: {} failed because of no server available in {}.", path, addressList); + throw new PowerJobException("no server available when send post request"); + } + + private List fetchAddressList() { + + ClientExtension clientExtension = config.getClientExtension(); + if (clientExtension != null) { + List addressList = clientExtension.addressProvider(new ExtensionContext()); + if (!CollectionUtils.isEmpty(addressList)) { + return addressList; + } + } + + return config.getAddressList(); + } + + /** + * 不验证证书 + * X.509 是一个国际标准,定义了公钥证书的格式。这个标准是由国际电信联盟(ITU-T)制定的,用于公钥基础设施(PKI)中数字证书的创建和分发。X.509证书主要用于在公开网络上验证实体的身份,如服务器或客户端的身份验证过程中,确保通信双方是可信的。X.509证书广泛应用于多种安全协议中,包括SSL/TLS,它是实现HTTPS的基础。 + */ + protected static class NoVerifyX509TrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) { + } + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) { + // 不验证 + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + + + private String getUrl(String path, String address) { + String protocol = config.getProtocol().getProtocol(); + return String.format(URL_PATTERN, protocol, address, OpenAPIConstant.WEB_PATH, path); + } +} diff --git a/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestServiceOkHttp3Impl.java b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestServiceOkHttp3Impl.java new file mode 100644 index 00000000..ff24f8d9 --- /dev/null +++ b/powerjob-client/src/main/java/tech/powerjob/client/service/impl/ClusterRequestServiceOkHttp3Impl.java @@ -0,0 +1,148 @@ +package tech.powerjob.client.service.impl; + +import com.google.common.collect.Maps; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import tech.powerjob.client.ClientConfig; +import tech.powerjob.client.common.Protocol; +import tech.powerjob.client.service.HttpResponse; +import tech.powerjob.client.service.PowerRequestBody; +import tech.powerjob.common.OmsConstant; +import tech.powerjob.common.serialize.JsonUtils; + +import javax.net.ssl.*; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * desc + * + * @author tjq + * @since 2024/2/20 + */ +@Slf4j +public class ClusterRequestServiceOkHttp3Impl extends AppAuthClusterRequestService { + + private final OkHttpClient okHttpClient; + + + public ClusterRequestServiceOkHttp3Impl(ClientConfig config) { + super(config); + + // 初始化 HTTP 客户端 + if (Protocol.HTTPS.equals(config.getProtocol())) { + okHttpClient = initHttpsNoVerifyClient(); + } else { + okHttpClient = initHttpClient(); + } + } + + @Override + protected HttpResponse sendHttpRequest(String url, PowerRequestBody powerRequestBody) throws IOException { + + // 添加公共 header + powerRequestBody.addHeaders(config.getDefaultHeaders()); + + Object obj = powerRequestBody.getPayload(); + + RequestBody requestBody = null; + + switch (powerRequestBody.getMime()) { + case APPLICATION_JSON: + MediaType jsonType = MediaType.parse(OmsConstant.JSON_MEDIA_TYPE); + String body = obj instanceof String ? (String) obj : JsonUtils.toJSONStringUnsafe(obj); + requestBody = RequestBody.create(jsonType, body); + + break; + case APPLICATION_FORM: + FormBody.Builder formBuilder = new FormBody.Builder(); + Map formObj = (Map) obj; + formObj.forEach(formBuilder::add); + requestBody = formBuilder.build(); + } + + Request request = new Request.Builder() + .post(requestBody) + .headers(Headers.of(powerRequestBody.getHeaders())) + .url(url) + .build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + + int code = response.code(); + HttpResponse httpResponse = new HttpResponse() + .setCode(code) + .setSuccess(code == HTTP_SUCCESS_CODE); + + ResponseBody body = response.body(); + if (body != null) { + httpResponse.setResponse(body.string()); + } + + Headers respHeaders = response.headers(); + Set headerNames = respHeaders.names(); + Map respHeaderMap = Maps.newHashMap(); + headerNames.forEach(hdKey -> respHeaderMap.put(hdKey, respHeaders.get(hdKey))); + + httpResponse.setHeaders(respHeaderMap); + + return httpResponse; + } + } + + @SneakyThrows + private OkHttpClient initHttpClient() { + OkHttpClient.Builder okHttpBuilder = commonOkHttpBuilder(); + return okHttpBuilder.build(); + } + + @SneakyThrows + private OkHttpClient initHttpsNoVerifyClient() { + + X509TrustManager trustManager = new NoVerifyX509TrustManager(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom()); + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + OkHttpClient.Builder okHttpBuilder = commonOkHttpBuilder(); + + // 不需要校验证书 + okHttpBuilder.sslSocketFactory(sslSocketFactory, trustManager); + // 不校验 url中的 hostname + okHttpBuilder.hostnameVerifier((String hostname, SSLSession session) -> true); + + + return okHttpBuilder.build(); + } + + private OkHttpClient.Builder commonOkHttpBuilder() { + return new OkHttpClient.Builder() + // 设置读取超时时间 + .readTimeout(Optional.ofNullable(config.getReadTimeout()).orElse(DEFAULT_TIMEOUT_SECONDS), TimeUnit.SECONDS) + // 设置写的超时时间 + .writeTimeout(Optional.ofNullable(config.getWriteTimeout()).orElse(DEFAULT_TIMEOUT_SECONDS), TimeUnit.SECONDS) + // 设置连接超时时间 + .connectTimeout(Optional.ofNullable(config.getConnectionTimeout()).orElse(DEFAULT_TIMEOUT_SECONDS), TimeUnit.SECONDS) + .callTimeout(Optional.ofNullable(config.getConnectionTimeout()).orElse(DEFAULT_TIMEOUT_SECONDS), TimeUnit.SECONDS); + } + + @Override + public void close() throws IOException { + + // 关闭 Dispatcher + okHttpClient.dispatcher().executorService().shutdown(); + // 清理连接池 + okHttpClient.connectionPool().evictAll(); + // 清理缓存(如果有使用) + Cache cache = okHttpClient.cache(); + if (cache != null) { + cache.close(); + } + } +} diff --git a/powerjob-client/src/test/java/tech/powerjob/client/test/ClientInitializer.java b/powerjob-client/src/test/java/tech/powerjob/client/test/ClientInitializer.java index 8617ac67..4cad73ba 100644 --- a/powerjob-client/src/test/java/tech/powerjob/client/test/ClientInitializer.java +++ b/powerjob-client/src/test/java/tech/powerjob/client/test/ClientInitializer.java @@ -1,5 +1,6 @@ package tech.powerjob.client.test; +import com.google.common.collect.Lists; import org.junit.jupiter.api.BeforeAll; import tech.powerjob.client.IPowerJobClient; import tech.powerjob.client.PowerJobClient; @@ -16,6 +17,6 @@ public class ClientInitializer { @BeforeAll public static void initClient() throws Exception { - powerJobClient = new PowerJobClient("127.0.0.1:7700", "powerjob-worker-samples", "powerjob123"); + powerJobClient = new PowerJobClient(Lists.newArrayList("127.0.0.1:7700", "127.0.0.1:7701"), "powerjob-worker-samples", "powerjob123"); } } diff --git a/powerjob-client/src/test/java/tech/powerjob/client/test/TestClient.java b/powerjob-client/src/test/java/tech/powerjob/client/test/TestClient.java index ded4b51c..ac5658d6 100644 --- a/powerjob-client/src/test/java/tech/powerjob/client/test/TestClient.java +++ b/powerjob-client/src/test/java/tech/powerjob/client/test/TestClient.java @@ -1,6 +1,9 @@ package tech.powerjob.client.test; import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import tech.powerjob.client.PowerJobClient; import tech.powerjob.common.enums.ExecuteType; import tech.powerjob.common.enums.ProcessorType; @@ -9,11 +12,6 @@ import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.response.InstanceInfoDTO; import tech.powerjob.common.response.JobInfoDTO; import tech.powerjob.common.response.ResultDTO; -import lombok.SneakyThrows; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.TimeUnit; /** * Test cases for {@link PowerJobClient} @@ -22,17 +20,18 @@ import java.util.concurrent.TimeUnit; * @author Echo009 * @since 2020/4/15 */ +@Slf4j class TestClient extends ClientInitializer { - public static final long JOB_ID = 4L; + public static final long JOB_ID = 1L; @Test void testSaveJob() { SaveJobInfoRequest newJobInfo = new SaveJobInfoRequest(); newJobInfo.setId(JOB_ID); - newJobInfo.setJobName("omsOpenAPIJobccccc"); - newJobInfo.setJobDescription("test OpenAPI"); + newJobInfo.setJobName("omsOpenAPIJobccccc" + System.currentTimeMillis()); + newJobInfo.setJobDescription("test OpenAPI" + System.currentTimeMillis()); newJobInfo.setJobParams("{'aa':'bb'}"); newJobInfo.setTimeExpressionType(TimeExpressionType.CRON); newJobInfo.setTimeExpression("0 0 * * * ? "); @@ -45,8 +44,10 @@ class TestClient extends ClientInitializer { newJobInfo.setMinMemorySpace(1.2); newJobInfo.setMinDiskSpace(1.3); + log.info("[TestClient] [testSaveJob] SaveJobInfoRequest: {}", JSONObject.toJSONString(newJobInfo)); + ResultDTO resultDTO = powerJobClient.saveJob(newJobInfo); - System.out.println(JSONObject.toJSONString(resultDTO)); + log.info("[TestClient] [testSaveJob] result: {}", JSONObject.toJSONString(resultDTO)); Assertions.assertNotNull(resultDTO); } @@ -107,21 +108,21 @@ class TestClient extends ClientInitializer { @Test void testFetchInstanceInfo() { - ResultDTO res = powerJobClient.fetchInstanceInfo(205436386851946560L); + ResultDTO res = powerJobClient.fetchInstanceInfo(702482902331424832L); System.out.println(res); Assertions.assertNotNull(res); } @Test void testStopInstance() { - ResultDTO res = powerJobClient.stopInstance(205436995885858880L); + ResultDTO res = powerJobClient.stopInstance(702482902331424832L); System.out.println(res); Assertions.assertNotNull(res); } @Test void testFetchInstanceStatus() { - ResultDTO res = powerJobClient.fetchInstanceStatus(205436995885858880L); + ResultDTO res = powerJobClient.fetchInstanceStatus(702482902331424832L); System.out.println(res); Assertions.assertNotNull(res); } @@ -135,19 +136,19 @@ class TestClient extends ClientInitializer { Assertions.assertTrue(cancelRes.isSuccess()); } - @Test - @SneakyThrows - void testCancelInstanceInDatabase() { - ResultDTO startRes = powerJobClient.runJob(15L, "start by OhMyClient", 2000000); - System.out.println("runJob result: " + JSONObject.toJSONString(startRes)); - - // Restart server manually and clear all the data in time wheeler. - TimeUnit.MINUTES.sleep(1); - - ResultDTO cancelRes = powerJobClient.cancelInstance(startRes.getData()); - System.out.println("cancelJob result: " + JSONObject.toJSONString(cancelRes)); - Assertions.assertTrue(cancelRes.isSuccess()); - } +// @Test +// @SneakyThrows +// void testCancelInstanceInDatabase() { +// ResultDTO startRes = powerJobClient.runJob(15L, "start by OhMyClient", 2000000); +// System.out.println("runJob result: " + JSONObject.toJSONString(startRes)); +// +// // Restart server manually and clear all the data in time wheeler. +// TimeUnit.MINUTES.sleep(1); +// +// ResultDTO cancelRes = powerJobClient.cancelInstance(startRes.getData()); +// System.out.println("cancelJob result: " + JSONObject.toJSONString(cancelRes)); +// Assertions.assertTrue(cancelRes.isSuccess()); +// } @Test void testRetryInstance() { diff --git a/powerjob-client/src/test/java/tech/powerjob/client/test/TestClusterHA.java b/powerjob-client/src/test/java/tech/powerjob/client/test/TestClusterHA.java new file mode 100644 index 00000000..e10cc8d3 --- /dev/null +++ b/powerjob-client/src/test/java/tech/powerjob/client/test/TestClusterHA.java @@ -0,0 +1,35 @@ +package tech.powerjob.client.test; + +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import tech.powerjob.common.response.JobInfoDTO; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.utils.CommonUtils; + +/** + * 测试容灾能力 + * + * @author tjq + * @since 2024/8/11 + */ +@Slf4j +public class TestClusterHA extends ClientInitializer { + + @Test + void testHa() { + // 人工让 server 启停 + for (int i = 0; i < 1000000; i++) { + + CommonUtils.easySleep(100); + + ResultDTO jobInfoDTOResultDTO = powerJobClient.fetchJob(1L); + + log.info("[TestClusterHA] response: {}", JSONObject.toJSONString(jobInfoDTOResultDTO)); + + if (!jobInfoDTOResultDTO.isSuccess()) { + throw new RuntimeException("request failed!"); + } + } + } +} diff --git a/powerjob-client/src/test/java/tech/powerjob/client/test/TestWorkflow.java b/powerjob-client/src/test/java/tech/powerjob/client/test/TestWorkflow.java index 1b6206c2..b12a1562 100644 --- a/powerjob-client/src/test/java/tech/powerjob/client/test/TestWorkflow.java +++ b/powerjob-client/src/test/java/tech/powerjob/client/test/TestWorkflow.java @@ -29,7 +29,7 @@ import java.util.List; */ class TestWorkflow extends ClientInitializer { - private static final long WF_ID = 1; + private static final long WF_ID = 2; @Test void initTestData() { diff --git a/powerjob-common/pom.xml b/powerjob-common/pom.xml index 16765564..61a0986c 100644 --- a/powerjob-common/pom.xml +++ b/powerjob-common/pom.xml @@ -5,12 +5,12 @@ powerjob tech.powerjob - 4.3.9 + 5.1.0 4.0.0 powerjob-common - 4.3.9 + 5.1.0 jar @@ -20,7 +20,7 @@ 31.1-jre 3.14.9 5.3.0 - 2.14.0-rc1 + 2.14.3 5.9.0 @@ -77,6 +77,13 @@ jackson-databind ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + diff --git a/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java b/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java index f589e869..4d40d323 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java @@ -30,4 +30,6 @@ public class OmsConstant { public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; public static final String JSON_MEDIA_TYPE = "application/json; charset=utf-8"; + + public static final String NULL = "null"; } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java b/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java index 2b9da25a..2d7d8f55 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/OpenAPIConstant.java @@ -16,6 +16,8 @@ public class OpenAPIConstant { public static final String ASSERT = "/assert"; + public static final String AUTH_APP = "/authApp"; + /* ************* JOB 区 ************* */ public static final String SAVE_JOB = "/saveJob"; @@ -56,4 +58,12 @@ public class OpenAPIConstant { public static final String RETRY_WORKFLOW_INSTANCE = "/retryWfInstance"; public static final String FETCH_WORKFLOW_INSTANCE_INFO = "/fetchWfInstanceInfo"; public static final String MARK_WORKFLOW_NODE_AS_SUCCESS = "/markWorkflowNodeAsSuccess"; + + /* ************* 鉴权 ************* */ + + public static final String REQUEST_HEADER_ACCESS_TOKEN = "X-POWERJOB-ACCESS-TOKEN"; + + public static final String REQUEST_HEADER_APP_ID = "X-POWERJOB-APP-ID"; + + public static final String RESPONSE_HEADER_AUTH_STATUS = "X-POWERJOB-AUTH-PASSED"; } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/enums/EncryptType.java b/powerjob-common/src/main/java/tech/powerjob/common/enums/EncryptType.java new file mode 100644 index 00000000..3ce0eaa8 --- /dev/null +++ b/powerjob-common/src/main/java/tech/powerjob/common/enums/EncryptType.java @@ -0,0 +1,22 @@ +package tech.powerjob.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 加密类型 + * + * @author tjq + * @since 2024/8/10 + */ +@Getter +@AllArgsConstructor +public enum EncryptType { + + NONE("none"), + + MD5("md5") + ; + + private final String code; +} diff --git a/powerjob-common/src/main/java/tech/powerjob/common/enums/ErrorCodes.java b/powerjob-common/src/main/java/tech/powerjob/common/enums/ErrorCodes.java new file mode 100644 index 00000000..db0043c6 --- /dev/null +++ b/powerjob-common/src/main/java/tech/powerjob/common/enums/ErrorCodes.java @@ -0,0 +1,67 @@ +package tech.powerjob.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 鉴权错误信息 + * + * @author tjq + * @since 2024/2/11 + */ +@Getter +@AllArgsConstructor +public enum ErrorCodes { + + USER_NOT_LOGIN("-100", "UserNotLoggedIn"), + USER_NOT_EXIST("-101", "UserNotExist"), + USER_AUTH_FAILED("-102", "UserAuthFailed"), + /** + * 账户被停用 + */ + USER_DISABLED("-103", "UserDisabled"), + + + NO_PERMISSION("-200", "NoPermission"), + + /** + * 无效请求,一般是参数问题 + */ + INVALID_REQUEST("-300", "INVALID_REQUEST"), + + INCORRECT_PASSWORD("-400", "INCORRECT_PASSWORD"), + + /** + * 非法令牌 + */ + INVALID_TOKEN("-401", "INVALID_TOKEN"), + /** + * 无效 APP(无法找到 app) + */ + INVALID_APP("-402", "INVALID_APP"), + + /** + * 令牌过期 + */ + TOKEN_EXPIRED("-403", "TOKEN_EXPIRED"), + + /** + * 系统内部异常 + */ + SYSTEM_UNKNOWN_ERROR("-500", "SYS_UNKNOWN_ERROR"), + + /** + * OPENAPI 错误码号段 -10XX + */ + OPEN_API_AUTH_FAILED("-1002", "OPEN_API_AUTH_FAILED"), + + /** + * PowerJobClient 错误码号段 + */ + CLIENT_HTTP_REQUEST_FAILED("-2001", "CLIENT_HTTP_REQUEST_FAILED"), + + ; + + private final String code; + private final String msg; +} diff --git a/powerjob-common/src/main/java/tech/powerjob/common/enums/MIME.java b/powerjob-common/src/main/java/tech/powerjob/common/enums/MIME.java new file mode 100644 index 00000000..b5a15c78 --- /dev/null +++ b/powerjob-common/src/main/java/tech/powerjob/common/enums/MIME.java @@ -0,0 +1,22 @@ +package tech.powerjob.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 消息内容类型 + * + * @author tjq + * @since 2024/8/10 + */ +@Getter +@AllArgsConstructor +public enum MIME { + + APPLICATION_JSON("application/json; charset=utf-8"), + + APPLICATION_FORM("application/x-www-form-urlencoded") + ; + + private final String code; +} diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/constants/SwitchableStatus.java b/powerjob-common/src/main/java/tech/powerjob/common/enums/SwitchableStatus.java similarity index 84% rename from powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/constants/SwitchableStatus.java rename to powerjob-common/src/main/java/tech/powerjob/common/enums/SwitchableStatus.java index e143553c..e01300b6 100644 --- a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/constants/SwitchableStatus.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/enums/SwitchableStatus.java @@ -1,4 +1,4 @@ -package tech.powerjob.server.common.constants; +package tech.powerjob.common.enums; import lombok.AllArgsConstructor; import lombok.Getter; @@ -13,10 +13,16 @@ import lombok.Getter; @AllArgsConstructor public enum SwitchableStatus { /** - * + * 启用 */ ENABLE(1), + /** + * 关闭 + */ DISABLE(2), + /** + * 软删除 + */ DELETED(99); private final int v; diff --git a/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java b/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java index d9ecda6d..442e5bcd 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java @@ -1,13 +1,21 @@ package tech.powerjob.common.exception; +import lombok.Getter; +import lombok.Setter; +import tech.powerjob.common.enums.ErrorCodes; + /** * PowerJob 运行时异常 * * @author tjq * @since 2020/5/26 */ +@Setter +@Getter public class PowerJobException extends RuntimeException { + protected String code; + public PowerJobException() { } @@ -15,6 +23,11 @@ public class PowerJobException extends RuntimeException { super(message); } + public PowerJobException(ErrorCodes errorCode, String extraMsg) { + super(extraMsg == null ? errorCode.getMsg() : errorCode.getMsg().concat(":").concat(extraMsg)); + this.code = errorCode.getCode(); + } + public PowerJobException(String message, Throwable cause) { super(message, cause); } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/response/PowerResultDTO.java b/powerjob-common/src/main/java/tech/powerjob/common/response/PowerResultDTO.java new file mode 100644 index 00000000..e582f764 --- /dev/null +++ b/powerjob-common/src/main/java/tech/powerjob/common/response/PowerResultDTO.java @@ -0,0 +1,47 @@ +package tech.powerjob.common.response; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.exception.ExceptionUtils; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.common.exception.PowerJobException; + +/** + * 新的 Result,带状态码 + * + * @author 程序帕鲁 + * @since 2024/2/19 + */ +@Getter +@Setter +public class PowerResultDTO extends ResultDTO { + + private String code; + + public static PowerResultDTO s(T data) { + PowerResultDTO r = new PowerResultDTO<>(); + r.success = true; + r.data = data; + return r; + } + + public static PowerResultDTO f(String message) { + PowerResultDTO r = new PowerResultDTO<>(); + r.success = false; + r.message = message; + return r; + } + + public static PowerResultDTO f(Throwable t) { + PowerResultDTO f = f(ExceptionUtils.getStackTrace(t)); + f.setCode(ErrorCodes.SYSTEM_UNKNOWN_ERROR.getCode()); + return f; + } + + public static PowerResultDTO f(PowerJobException pje) { + PowerResultDTO f = f(pje.getMessage()); + f.setCode(pje.getCode()); + return f; + } + +} diff --git a/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java b/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java index 03e1dc6b..5cd90519 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java @@ -8,6 +8,11 @@ import org.apache.commons.lang3.exception.ExceptionUtils; /** * The result object returned by the request + *

+ * 低版本由于 Jackson 序列化配置问题,导致无法在此对象上新增任何字段了,否则会报错 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "code" (class tech.powerjob.common.response.ObjectResultDTO), not marked as ignorable (3 known properties: "data", "success", "message"]) + * at [Source: (String)"{"success":true,"code":null,"data":2,"message":null}"; line: 1, column: 28] (through reference chain: tech.powerjob.common.response.ObjectResultDTO["code"]) + *

+ * 短期内所有的新增字段需求,都通过新对象继承实现 * * @author tjq * @since 2020/3/30 @@ -17,9 +22,9 @@ import org.apache.commons.lang3.exception.ExceptionUtils; @ToString public class ResultDTO implements PowerSerializable { - private boolean success; - private T data; - private String message; + protected boolean success; + protected T data; + protected String message; public static ResultDTO success(T data) { ResultDTO r = new ResultDTO<>(); diff --git a/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java index b706186a..dbdab14d 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/serialize/JsonUtils.java @@ -2,7 +2,6 @@ package tech.powerjob.common.serialize; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; @@ -35,6 +34,13 @@ public class JsonUtils { static { JSON_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + // 非核心功能可降级,尽可能降低依赖冲突概率 + try { + JSON_MAPPER.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule()); + } catch (Exception e) { + log.warn("[JsonUtils] registerJavaTimeModule failed, PowerJob can't process Java 8 date/time type now!", e); + } } private static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference> () {}; @@ -65,8 +71,9 @@ public class JsonUtils { try { return JSON_MAPPER.writeValueAsString(obj); }catch (Exception e) { - throw new PowerJobException(e); + ExceptionUtils.rethrow(e); } + throw new ImpossibleException(); } public static byte[] toBytes(Object obj) { @@ -78,7 +85,7 @@ public class JsonUtils { return null; } - public static T parseObject(String json, Class clz) throws JsonProcessingException { + public static T parseObject(String json, Class clz) throws Exception { return JSON_MAPPER.readValue(json, clz); } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java index 498d5b73..d787f337 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java @@ -7,6 +7,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import java.util.Collection; +import java.util.Date; +import java.util.Map; import java.util.UUID; import java.util.function.Supplier; @@ -128,6 +130,16 @@ public class CommonUtils { throw new PowerJobException(msg); } } + if (obj instanceof Collection) { + if (CollectionUtils.isEmpty((Collection) obj)) { + throw new PowerJobException(msg); + } + } + if (obj instanceof Map) { + if (MapUtils.isEmpty((Map) obj)) { + throw new PowerJobException(msg); + } + } return obj; } @@ -147,6 +159,13 @@ public class CommonUtils { return OmsConstant.NONE; } + public static String formatTime(Date date) { + if (date == null) { + return OmsConstant.NONE; + } + return formatTime(date.getTime()); + } + /** * 格式化字符串,如果是 null 或空则显示 N/A * @param str 字符串 diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/DigestUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/DigestUtils.java new file mode 100644 index 00000000..4feeb90c --- /dev/null +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/DigestUtils.java @@ -0,0 +1,47 @@ +package tech.powerjob.common.utils; + +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; + +import java.math.BigInteger; +import java.security.MessageDigest; + +/** + * 加密工具 + * + * @author tjq + * @since 2023/3/25 + */ +public class DigestUtils { + + /** + * 32位小写 md5 + * @param input 输入 + * @return md5 + */ + @SneakyThrows + public static String md5(String input) { + + if (StringUtils.isEmpty(input)) { + return null; + } + + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(input.getBytes()); + byte[] byteArray = md5.digest(); + + BigInteger bigInt = new BigInteger(1, byteArray); + // 参数16表示16进制 + StringBuilder result = new StringBuilder(bigInt.toString(16)); + // 不足32位高位补零 + while(result.length() < 32) { + result.insert(0, "0"); + } + return result.toString(); + } + + public static String rePassword(String password, String salt) { + String f1 = String.format("%s_%s_z", salt, password); + return String.format("%s_%s_b", salt, md5(f1)); + } +} diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/HttpUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/HttpUtils.java index fa65ff83..ceb2af52 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/HttpUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/HttpUtils.java @@ -21,6 +21,7 @@ public class HttpUtils { client = new OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) .build(); } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/MapUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/MapUtils.java index 548f00b5..dd57bfcd 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/MapUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/MapUtils.java @@ -12,6 +12,26 @@ import java.util.Map; */ public class MapUtils { + public static String getString(Map map, K key) { + if (map != null) { + Object answer = map.get(key); + if (answer != null) { + return answer.toString(); + } + } + + return null; + } + + public static String getString(Map map, K key, String defaultValue) { + String answer = getString(map, key); + if (answer == null) { + answer = defaultValue; + } + + return answer; + } + public static Long getLong(Map map, K key, Long defaultValue) { Long answer = getLong(map, key); if (answer == null) { @@ -55,4 +75,12 @@ public class MapUtils { return null; } + + public static boolean isEmpty(Map map) { + return map == null || map.isEmpty(); + } + + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/NetUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/NetUtils.java index b6cc0e14..5b7ebbff 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/NetUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/NetUtils.java @@ -217,9 +217,10 @@ public class NetUtils { if (networkInterfaceChecker == null) { return false; } - log.info("[Net] try to choose NetworkInterface by NetworkInterfaceChecker, current NetworkInterface: {}", networkInterface); try { - return networkInterfaceChecker.ok(networkInterface, getFirstReachableInetAddress(networkInterface)); + boolean ok = networkInterfaceChecker.ok(networkInterface, getFirstReachableInetAddress(networkInterface)); + log.info("[Net] try to choose NetworkInterface by NetworkInterfaceChecker, current NetworkInterface[{}], ok: {}", networkInterface, ok); + return ok; } catch (Exception e) { log.warn("[Net] isPassedCheckerNetworkInterface failed, current networkInterface: {}", networkInterface, e); } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongSocketServer.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongSocketServer.java index d2a837c8..1ec93d74 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongSocketServer.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongSocketServer.java @@ -35,7 +35,13 @@ public class PingPongSocketServer implements PingPongServer { } // 接收连接,如果没有连接,accept() 方法会阻塞 try (Socket socket = serverSocket.accept();OutputStream outputStream = socket.getOutputStream();) { + + socket.setSoTimeout(2000); + socket.setKeepAlive(false); + outputStream.write(PingPongUtils.PONG.getBytes(StandardCharsets.UTF_8)); + // BufferedReader.readLine() 会等待直到遇到换行符(\n)或回车符(\r\n),才会返回一行内容。如果服务器发送的数据没有这些换行符,readLine() 会一直阻塞,直到超时 + outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } catch (Exception e) { if (!terminated) { diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongUtils.java index ee547254..821f7352 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/net/PingPongUtils.java @@ -30,6 +30,9 @@ public class PingPongUtils { try (Socket s = new Socket(targetIp, targetPort);InputStream is = s.getInputStream();OutputStream os = s.getOutputStream();BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + s.setSoTimeout(2000); + s.setKeepAlive(false); + // 发送 PING 请求 os.write(PING.getBytes(StandardCharsets.UTF_8)); os.flush(); diff --git a/powerjob-common/src/test/java/tech/powerjob/common/serialize/JsonUtilsTest.java b/powerjob-common/src/test/java/tech/powerjob/common/serialize/JsonUtilsTest.java new file mode 100644 index 00000000..3e8b029e --- /dev/null +++ b/powerjob-common/src/test/java/tech/powerjob/common/serialize/JsonUtilsTest.java @@ -0,0 +1,81 @@ +package tech.powerjob.common.serialize; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * test json utils + * + * @author tjq + * @since 2024/3/16 + */ +@Slf4j +class JsonUtilsTest { + + @Test + @SneakyThrows + void simpleTest() { + Person person = new Person().setName("mubao").setAge(18); + String jsonString = JsonUtils.toJSONString(person); + log.info("[JsonUtilsTest] person: {}, jsonString: {}", person, jsonString); + assert jsonString != null; + Person person2 = JsonUtils.parseObject(jsonString, Person.class); + assert person2.equals(person); + } + + @Test + @SneakyThrows + void testAdvanceApi() { + PersonPlus personPlus = new PersonPlus(); + personPlus.setName("gongbao").setAge(3); + personPlus.setBirthDay(LocalDateTime.now()); + + String jsonString = JsonUtils.toJSONString(personPlus); + PersonPlus personPlus2 = JsonUtils.parseObject(jsonString, PersonPlus.class); + assert personPlus2.equals(personPlus); + } + + @Test + @SneakyThrows + void testMoreOrLessFields() { + PersonPlus personPlus = new PersonPlus().setBirthDay(LocalDateTime.now()); + personPlus.setName("gongbao").setAge(3); + + String originJsonStr = JsonUtils.toJSONString(personPlus); + + Map personPlusMapMore = JsonUtils.parseMap(originJsonStr); + personPlusMapMore.put("extraKey", System.currentTimeMillis()); + + PersonPlus personPlusByMoreFieldsJsonStr = JsonUtils.parseObject(JsonUtils.toJSONString(personPlusMapMore), PersonPlus.class); + assert personPlusByMoreFieldsJsonStr.equals(personPlus); + + Map personPlusMapLess = JsonUtils.parseMap(originJsonStr); + personPlusMapLess.remove("birthDay"); + + PersonPlus personPlusByLessFieldsJsonStr = JsonUtils.parseObject(JsonUtils.toJSONString(personPlusMapLess), PersonPlus.class); + assert personPlusByLessFieldsJsonStr.getName().equals(personPlus.getName()); + assert personPlusByLessFieldsJsonStr.getAge().equals(personPlus.getAge()); + } + + @Data + @Accessors(chain = true) + static class Person implements Serializable { + private String name; + private Integer age; + } + + @Data + @Accessors(chain = true) + @EqualsAndHashCode(callSuper = true) + static class PersonPlus extends Person { + private LocalDateTime birthDay; + } +} \ No newline at end of file diff --git a/powerjob-common/src/test/java/tech/powerjob/common/utils/CommonUtilsTest.java b/powerjob-common/src/test/java/tech/powerjob/common/utils/CommonUtilsTest.java new file mode 100644 index 00000000..26453dd4 --- /dev/null +++ b/powerjob-common/src/test/java/tech/powerjob/common/utils/CommonUtilsTest.java @@ -0,0 +1,39 @@ +package tech.powerjob.common.utils; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.junit.jupiter.api.Test; +import tech.powerjob.common.exception.PowerJobException; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * CommonUtilsTest + * + * @author tjq + * @since 2024/8/11 + */ +class CommonUtilsTest { + + @Test + void testRequireNonNull() { + + assertThrowsExactly(PowerJobException.class, () -> CommonUtils.requireNonNull(null, "NULL_OBJ")); + assertThrowsExactly(PowerJobException.class, () -> CommonUtils.requireNonNull("", "EMPTY_STR")); + assertThrowsExactly(PowerJobException.class, () -> CommonUtils.requireNonNull(Lists.newArrayList(), "EMPTY_COLLECTION")); + assertThrowsExactly(PowerJobException.class, () -> CommonUtils.requireNonNull(Collections.emptyMap(), "EMPTY_MAP")); + + Map map = Maps.newHashMap(); + map.put("1", 1); + + CommonUtils.requireNonNull(1, "NORMAL"); + CommonUtils.requireNonNull("1", "NORMAL"); + CommonUtils.requireNonNull(Lists.newArrayList("1"), "NORMAL"); + CommonUtils.requireNonNull(map, "NORMAL"); + + } + +} \ No newline at end of file diff --git a/powerjob-official-processors/pom.xml b/powerjob-official-processors/pom.xml index 7a8d5550..26f3c1ab 100644 --- a/powerjob-official-processors/pom.xml +++ b/powerjob-official-processors/pom.xml @@ -5,12 +5,12 @@ powerjob tech.powerjob - 4.3.9 + 5.1.0 4.0.0 powerjob-official-processors - 4.3.9 + 5.1.0 jar @@ -20,7 +20,7 @@ 5.9.1 1.2.13 - 4.3.9 + 5.1.0 2.2.224 8.0.28 5.3.31 diff --git a/powerjob-remote/pom.xml b/powerjob-remote/pom.xml index c1629f5d..678e519b 100644 --- a/powerjob-remote/pom.xml +++ b/powerjob-remote/pom.xml @@ -5,7 +5,7 @@ powerjob tech.powerjob - 4.3.9 + 5.1.0 4.0.0 pom diff --git a/powerjob-remote/powerjob-remote-benchmark/pom.xml b/powerjob-remote/powerjob-remote-benchmark/pom.xml index 230a6e27..80eefbb9 100644 --- a/powerjob-remote/powerjob-remote-benchmark/pom.xml +++ b/powerjob-remote/powerjob-remote-benchmark/pom.xml @@ -5,7 +5,7 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.1.0 4.0.0 @@ -21,8 +21,8 @@ 1.2.13 2.7.18 - 4.3.9 - 4.3.9 + 5.1.0 + 5.1.0 3.9.0 4.2.9 diff --git a/powerjob-remote/powerjob-remote-framework/pom.xml b/powerjob-remote/powerjob-remote-framework/pom.xml index a244e551..b642af7c 100644 --- a/powerjob-remote/powerjob-remote-framework/pom.xml +++ b/powerjob-remote/powerjob-remote-framework/pom.xml @@ -5,11 +5,11 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.1.0 4.0.0 - 4.3.9 + 5.1.0 powerjob-remote-framework @@ -17,7 +17,7 @@ 8 UTF-8 - 4.3.9 + 5.1.0 0.10.2 diff --git a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/cs/CSInitializerConfig.java b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/cs/CSInitializerConfig.java index a64051fd..0546e263 100644 --- a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/cs/CSInitializerConfig.java +++ b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/cs/CSInitializerConfig.java @@ -19,7 +19,14 @@ import java.io.Serializable; @Accessors(chain = true) public class CSInitializerConfig implements Serializable { + /** + * 需要绑定的地址(本地) + */ private Address bindAddress; + /** + * 外部地址(需要 NAT 等情况存在) + */ + private Address externalAddress; private ServerType serverType; } diff --git a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/EngineConfig.java b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/EngineConfig.java index a9b82961..d038114c 100644 --- a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/EngineConfig.java +++ b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/EngineConfig.java @@ -30,6 +30,10 @@ public class EngineConfig implements Serializable { * 绑定的本地地址 */ private Address bindAddress; + /** + * 外部地址(需要 NAT 等情况存在) + */ + private Address externalAddress; /** * actor实例,交由使用侧自己实例化以便自行注入各种 bean */ diff --git a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/CSInitializerFactory.java b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/CSInitializerFactory.java index 8b02f3d4..c3afd46f 100644 --- a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/CSInitializerFactory.java +++ b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/CSInitializerFactory.java @@ -4,9 +4,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.reflections.Reflections; import tech.powerjob.common.OmsConstant; +import tech.powerjob.common.enums.Protocol; import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.remote.framework.cs.CSInitializer; +import java.util.Optional; import java.util.Set; /** @@ -18,8 +20,25 @@ import java.util.Set; @Slf4j class CSInitializerFactory { + private static final String OFFICIAL_HTTP_CS_INITIALIZER = "tech.powerjob.remote.http.HttpVertxCSInitializer"; + /** + * 未来底层框架摆脱 vertx 时可能会用这个 classname,or 开发者自己实现的 http 协议也可以用这个 classname,总之预留战未来 + */ + private static final String OFFICIAL_HTTP_CS_INITIALIZER2 = "tech.powerjob.remote.http.HttpCSInitializer"; + private static final String OFFICIAL_AKKA_CS_INITIALIZER = "tech.powerjob.remote.akka.AkkaCSInitializer"; + + private static final String EXTEND_CS_INITIALIZER_PATTERN = "tech.powerjob.remote.%s.CSInitializer"; + static CSInitializer build(String targetType) { + CSInitializer officialCSInitializer = tryLoadCSInitializerByClassName(targetType); + if (officialCSInitializer != null) { + return officialCSInitializer; + } + + log.info("[CSInitializerFactory] try load CSInitializerFactory by name failed, start to use Reflections!"); + + // JAVA SPI 机制太笨了,短期内继续保留 Reflections 官网下高版本兼容性 Reflections reflections = new Reflections(OmsConstant.PACKAGE); Set> cSInitializerClzSet = reflections.getSubTypesOf(CSInitializer.class); @@ -41,4 +60,52 @@ class CSInitializerFactory { throw new PowerJobException(String.format("can't load CSInitializer[%s], ensure your package name start with 'tech.powerjob' and import the dependencies!", targetType)); } + + /** + * 官方组件直接使用固定类名尝试加载,确保 reflections 不兼容情况下,至少能使用官方通讯协议 + * @param targetType 协议类型 + * @return CSInitializer + */ + private static CSInitializer tryLoadCSInitializerByClassName(String targetType) { + + if (Protocol.HTTP.name().equalsIgnoreCase(targetType)) { + Optional httpCsIOpt = tryLoadCSInitializerByClzName(OFFICIAL_HTTP_CS_INITIALIZER); + if (httpCsIOpt.isPresent()) { + return httpCsIOpt.get(); + } + Optional httpCsIOpt2 = tryLoadCSInitializerByClzName(OFFICIAL_HTTP_CS_INITIALIZER2); + if (httpCsIOpt2.isPresent()) { + return httpCsIOpt2.get(); + } + } + + if (Protocol.AKKA.name().equalsIgnoreCase(targetType)) { + Optional akkaCSIOpt = tryLoadCSInitializerByClzName(OFFICIAL_AKKA_CS_INITIALIZER); + if (akkaCSIOpt.isPresent()) { + return akkaCSIOpt.get(); + } + } + + // 尝试加载按规范命名的处理器,比如使用方自定义了 http2 协议,将其类名定为 tech.powerjob.remote.http2.CSInitializer 依然可确保在 Reflections 不可用的情况下完成加载 + String clz = String.format(EXTEND_CS_INITIALIZER_PATTERN, targetType); + Optional extOpt = tryLoadCSInitializerByClzName(clz); + return extOpt.orElse(null); + + } + + + private static Optional tryLoadCSInitializerByClzName(String clzName) { + try { + log.info("[CSInitializerFactory] try to load CSInitializer by classname: {}", clzName); + Class clz = Class.forName(clzName); + CSInitializer o = (CSInitializer) clz.getDeclaredConstructor().newInstance(); + log.info("[CSInitializerFactory] load CSInitializer[{}] successfully, obj: {}", clzName, o); + return Optional.of(o); + } catch (ClassNotFoundException ce) { + log.warn("[CSInitializerFactory] load CSInitializer by classname[{}] failed due to ClassNotFound: {}", clzName, ExceptionUtils.getMessage(ce)); + } catch (Exception e) { + log.warn("[CSInitializerFactory] load CSInitializer by classname[{}] failed.", clzName, e); + } + return Optional.empty(); + } } diff --git a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/PowerJobRemoteEngine.java b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/PowerJobRemoteEngine.java index 2f9cbc6b..0c0e5cbb 100644 --- a/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/PowerJobRemoteEngine.java +++ b/powerjob-remote/powerjob-remote-framework/src/main/java/tech/powerjob/remote/framework/engine/impl/PowerJobRemoteEngine.java @@ -41,6 +41,7 @@ public class PowerJobRemoteEngine implements RemoteEngine { csInitializer.init(new CSInitializerConfig() .setBindAddress(engineConfig.getBindAddress()) + .setExternalAddress(engineConfig.getExternalAddress()) .setServerType(engineConfig.getServerType()) ); diff --git a/powerjob-remote/powerjob-remote-impl-akka/pom.xml b/powerjob-remote/powerjob-remote-impl-akka/pom.xml index 4a9b4a23..1647f971 100644 --- a/powerjob-remote/powerjob-remote-impl-akka/pom.xml +++ b/powerjob-remote/powerjob-remote-impl-akka/pom.xml @@ -5,19 +5,19 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.1.0 4.0.0 powerjob-remote-impl-akka - 4.3.9 + 5.1.0 8 8 UTF-8 - 4.3.9 + 5.1.0 2.6.13 diff --git a/powerjob-remote/powerjob-remote-impl-akka/src/main/java/tech/powerjob/remote/akka/AkkaCSInitializer.java b/powerjob-remote/powerjob-remote-impl-akka/src/main/java/tech/powerjob/remote/akka/AkkaCSInitializer.java index cad05a8f..f00a0b29 100644 --- a/powerjob-remote/powerjob-remote-impl-akka/src/main/java/tech/powerjob/remote/akka/AkkaCSInitializer.java +++ b/powerjob-remote/powerjob-remote-impl-akka/src/main/java/tech/powerjob/remote/akka/AkkaCSInitializer.java @@ -9,6 +9,7 @@ import com.google.common.collect.Maps; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.remote.framework.actor.ActorInfo; import tech.powerjob.remote.framework.base.Address; @@ -47,8 +48,22 @@ public class AkkaCSInitializer implements CSInitializer { // 初始化 ActorSystem(macOS上 new ServerSocket 检测端口占用的方法并不生效,可能是AKKA是Scala写的缘故?没办法...只能靠异常重试了) Map overrideConfig = Maps.newHashMap(); - overrideConfig.put("akka.remote.artery.canonical.hostname", bindAddress.getHost()); - overrideConfig.put("akka.remote.artery.canonical.port", bindAddress.getPort()); + + Address externalAddress = config.getExternalAddress(); + + if (externalAddress == null || StringUtils.equalsIgnoreCase(externalAddress.toFullAddress(), bindAddress.toFullAddress())) { + overrideConfig.put("akka.remote.artery.canonical.hostname", bindAddress.getHost()); + overrideConfig.put("akka.remote.artery.canonical.port", bindAddress.getPort()); + log.info("[PowerJob-AKKA] not exist externalIp, overrideConfig: {}", overrideConfig); + } else { + overrideConfig.put("akka.remote.artery.canonical.hostname", externalAddress.getHost()); + overrideConfig.put("akka.remote.artery.canonical.port", externalAddress.getPort()); + + overrideConfig.put("akka.remote.artery.bind.hostname", "0.0.0.0"); + overrideConfig.put("akka.remote.artery.bind.port", bindAddress.getPort()); + + log.info("[PowerJob-AKKA] exist externalAddress[{}], final overrideConfig: {}", externalAddress, overrideConfig); + } Config akkaBasicConfig = ConfigFactory.load(AkkaConstant.AKKA_CONFIG); Config akkaFinalConfig = ConfigFactory.parseMap(overrideConfig).withFallback(akkaBasicConfig); diff --git a/powerjob-remote/powerjob-remote-impl-http/pom.xml b/powerjob-remote/powerjob-remote-impl-http/pom.xml index 19e3cce4..1c6d418f 100644 --- a/powerjob-remote/powerjob-remote-impl-http/pom.xml +++ b/powerjob-remote/powerjob-remote-impl-http/pom.xml @@ -5,12 +5,12 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.1.0 4.0.0 powerjob-remote-impl-http - 4.3.9 + 5.1.0 8 @@ -18,7 +18,7 @@ UTF-8 4.3.7 - 4.3.9 + 5.1.0 diff --git a/powerjob-remote/powerjob-remote-impl-http/src/main/java/tech/powerjob/remote/http/HttpVertxCSInitializer.java b/powerjob-remote/powerjob-remote-impl-http/src/main/java/tech/powerjob/remote/http/HttpVertxCSInitializer.java index 34117c44..481a4935 100644 --- a/powerjob-remote/powerjob-remote-impl-http/src/main/java/tech/powerjob/remote/http/HttpVertxCSInitializer.java +++ b/powerjob-remote/powerjob-remote-impl-http/src/main/java/tech/powerjob/remote/http/HttpVertxCSInitializer.java @@ -41,6 +41,8 @@ import java.util.concurrent.TimeUnit; * - vertx 唯一的缺点是其作为相对上层的框架,可能存在较为严重的包冲突问题,尤其是对于那些本身跑在 vertx-framework 上的用户 * - 不过该问题可以通过更换协议解决,预计后续提供一个基于 netty 和自定义协议的实现 * + * 20240316 note:注意类名被强依赖,后续若有改动需要同步更改 + * * @author tjq * @since 2022/12/31 */ diff --git a/powerjob-server/pom.xml b/powerjob-server/pom.xml index 70bf2ee4..f4533d83 100644 --- a/powerjob-server/pom.xml +++ b/powerjob-server/pom.xml @@ -5,12 +5,12 @@ powerjob tech.powerjob - 4.3.9 + 5.1.0 4.0.0 powerjob-server - 4.3.9 + 5.1.0 pom @@ -22,6 +22,7 @@ powerjob-server-migrate powerjob-server-core powerjob-server-monitor + powerjob-server-auth @@ -50,9 +51,9 @@ 3.0.10 9.2.1 - 4.3.9 - 4.3.9 - 4.3.9 + 5.1.0 + 5.1.0 + 5.1.0 1.6.14 3.17.1 1.12.665 @@ -96,12 +97,23 @@ powerjob-server-migrate ${project.version} + + tech.powerjob + powerjob-server-auth + ${project.version} + tech.powerjob powerjob-server-starter ${project.version} + + tech.powerjob + powerjob-client + ${project.version} + + org.mongodb @@ -120,7 +132,6 @@ aws-java-sdk-s3 ${aws-java-sdk-s3.version} - org.apache.commons diff --git a/powerjob-server/powerjob-server-auth/pom.xml b/powerjob-server/powerjob-server-auth/pom.xml new file mode 100644 index 00000000..7242fdf7 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/pom.xml @@ -0,0 +1,58 @@ + + + + + tech.powerjob + powerjob-server + 5.1.0 + + + 4.0.0 + + powerjob-server-auth + ${project.parent.version} + + + 8 + 8 + UTF-8 + 0.11.5 + 1.1.86 + + + + + tech.powerjob + powerjob-server-persistence + provided + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + com.aliyun + dingtalk + ${dingtalk.version} + + + + + + \ No newline at end of file diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/LoginUserHolder.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/LoginUserHolder.java new file mode 100644 index 00000000..7cf324a6 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/LoginUserHolder.java @@ -0,0 +1,48 @@ +package tech.powerjob.server.auth; + +/** + * LoginUserHolder + * + * @author tjq + * @since 2023/4/16 + */ +public class LoginUserHolder { + + private static final ThreadLocal TL = new ThreadLocal<>(); + + public static PowerJobUser get() { + return TL.get(); + } + + public static void set(PowerJobUser powerJobUser) { + TL.set(powerJobUser); + } + + public static void clean() { + TL.remove(); + } + + /** + * 获取用户名 + * @return 存在则返回常规用户名,否则返回 unknown + */ + public static String getUserName() { + PowerJobUser powerJobUser = get(); + if (powerJobUser != null) { + return powerJobUser.getUsername(); + } + return "UNKNOWN"; + } + + /** + * 获取用户ID + * @return 存在则返回,否则返回 null + */ + public static Long getUserId() { + PowerJobUser powerJobUser = get(); + if (powerJobUser != null) { + return powerJobUser.getId(); + } + return null; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java new file mode 100644 index 00000000..e6fabb4a --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java @@ -0,0 +1,40 @@ +package tech.powerjob.server.auth; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 权限 + * + * @author tjq + * @since 2023/3/20 + */ +@Getter +@AllArgsConstructor +public enum Permission { + + /** + * 不需要权限 + */ + NONE(1), + /** + * 读权限,查看控制台数据 + */ + READ(10), + /** + * 写权限,新增/修改任务等 + */ + WRITE(20), + /** + * 运维权限,比如任务的执行 + */ + OPS(30), + /** + * 超级权限 + */ + SU(100) + ; + + + private int v; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/PowerJobUser.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/PowerJobUser.java new file mode 100644 index 00000000..c0a750c1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/PowerJobUser.java @@ -0,0 +1,44 @@ +package tech.powerjob.server.auth; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** + * PowerJob 的 登陆用户 + * + * @author tjq + * @since 2023/3/20 + */ +@Getter +@Setter +@ToString +public class PowerJobUser implements Serializable { + + private Long id; + + private String username; + + /** + * 手机号 + */ + private String phone; + /** + * 邮箱地址 + */ + private String email; + /** + * webHook + */ + private String webHook; + /** + * 扩展字段 + */ + private String extra; + + /* ************** 以上为数据库字段 ************** */ + + private String jwtToken; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java new file mode 100644 index 00000000..9b0280ab --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java @@ -0,0 +1,53 @@ +package tech.powerjob.server.auth; + +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Set; + +import static tech.powerjob.server.auth.Permission.*; + +/** + * 角色 + * PowerJob 采用 RBAC 实现权限,出于实际需求的考虑,不决定采用动态权限模型。因此 RBAC 中的角色和权限均在此处定义。 + * 如果有自定义诉求,可以修改 Role 的定义 + * + * @author tjq + * @since 2023/3/20 + */ +@Getter +@AllArgsConstructor +public enum Role { + + /** + * 观察者,默认只读权限 + */ + OBSERVER(10, Sets.newHashSet(READ)), + /** + * 技术质量,读 + 操作权限 + */ + QA(20, Sets.newHashSet(READ, OPS)), + /** + * 开发者,读 + 编辑 + 操作权限 + */ + DEVELOPER(30, Sets.newHashSet(READ, WRITE, OPS)), + /** + * 管理员 + */ + ADMIN(40, Sets.newHashSet(READ, WRITE, OPS, SU)) + ; + + private final int v; + + private final Set permissions; + + public static Role of(int vv) { + for (Role role : values()) { + if (vv == role.v) { + return role; + } + } + throw new IllegalArgumentException("unknown role: " + vv); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java new file mode 100644 index 00000000..cec6f559 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java @@ -0,0 +1,40 @@ +package tech.powerjob.server.auth; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 权限范围 + * + * @author tjq + * @since 2023/9/3 + */ +@Getter +@AllArgsConstructor +public enum RoleScope { + + /** + * NAMESPACE 权限 + */ + NAMESPACE(1), + /** + * APP 级别权限 + */ + APP(10), + /** + * 全局权限 + */ + GLOBAL(666) + ; + + private final int v; + + public static RoleScope of(int vv) { + for (RoleScope rs : values()) { + if (vv == rs.v) { + return rs; + } + } + throw new IllegalArgumentException("unknown RoleScope: " + vv); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java new file mode 100644 index 00000000..7aa05528 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java @@ -0,0 +1,53 @@ +package tech.powerjob.server.auth.common; + +/** + * 常量 + * + * @author tjq + * @since 2024/2/11 + */ +public class AuthConstants { + + /* ********** 账号体系唯一标识,推荐开发者接入第三方登录体系时也使用4位编码,便于前端统一做样式 ********** */ + /** + * PowerJob自建账号体系 + */ + public static final String ACCOUNT_TYPE_POWER_JOB = "PWJB"; + /** + * 钉钉 + */ + public static final String ACCOUNT_TYPE_DING = "DING"; + /** + * 企业微信(预留,蹲一个 contributor) + */ + public static final String ACCOUNT_TYPE_WX = "QYWX"; + /** + * 飞书(预留,蹲一个 contributor +1) + */ + public static final String ACCOUNT_LARK = "LARK"; + + public static final String PARAM_KEY_USERNAME = "username"; + public static final String PARAM_KEY_PASSWORD = "password"; + /** + * 前端参数-密码加密类型,官方版本出于成本未进行前后端传输的对称加密,接入方有需求可自行实现,此处定义加密协议字段 + */ + public static final String PARAM_KEY_ENCRYPTION = "encryption"; + + /* ********** 账号体系 ********** */ + + /** + * JWT key + * 前端 header 默认首字母大写,保持一致方便处理 + */ + public static final String OLD_JWT_NAME = "Power_jwt"; + public static final String JWT_NAME = "PowerJwt"; + + /** + * 前端跳转到指定页面指令 + */ + public static final String FE_REDIRECT_KEY = "FE-REDIRECT:"; + + public static final String TIPS_NO_PERMISSION_TO_SEE = "NO_PERMISSION_TO_SEE"; + + public static final Long GLOBAL_ADMIN_TARGET_ID = 1L; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/PowerJobAuthException.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/PowerJobAuthException.java new file mode 100644 index 00000000..2b2862e0 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/PowerJobAuthException.java @@ -0,0 +1,23 @@ +package tech.powerjob.server.auth.common; + +import lombok.Getter; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.common.exception.PowerJobException; + +/** + * 鉴权相关错误 + * + * @author tjq + * @since 2024/2/10 + */ +@Getter +public class PowerJobAuthException extends PowerJobException { + + public PowerJobAuthException(ErrorCodes errorCode) { + this(errorCode, null); + } + + public PowerJobAuthException(ErrorCodes errorCode, String extraMsg) { + super(errorCode, extraMsg); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/utils/HttpServletUtils.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/utils/HttpServletUtils.java new file mode 100644 index 00000000..7c232ca1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/utils/HttpServletUtils.java @@ -0,0 +1,27 @@ +package tech.powerjob.server.auth.common.utils; + +import tech.powerjob.common.OmsConstant; + +import javax.servlet.http.HttpServletRequest; + +/** + * HttpServletUtils + * + * @author tjq + * @since 2024/2/12 + */ +public class HttpServletUtils { + + public static String fetchFromHeader(String key, HttpServletRequest httpServletRequest) { + // header、cookie 都能获取 + String v = httpServletRequest.getHeader(key); + + // 解决 window.localStorage.getItem 为 null 的问题 + if (OmsConstant.NULL.equalsIgnoreCase(v) || "undefined".equalsIgnoreCase(v)) { + return null; + } + + return v; + } + +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java new file mode 100644 index 00000000..98bfce2e --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java @@ -0,0 +1,45 @@ +package tech.powerjob.server.auth.interceptor; + +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * API 权限 + * + * @author tjq + * @since 2023/3/20 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiPermission { + /** + * API 名称 + * @return 空使用服务.方法名代替 + */ + String name() default ""; + + RoleScope roleScope() default RoleScope.APP; + + /** + * 需要的权限 + * @return 权限 + */ + Permission requiredPermission() default Permission.SU; + + /** + * 固定权限不支持的场景,需要使用动态权限 + * @return 动态权限 + */ + Class dynamicPermissionPlugin() default EmptyPlugin.class; + + /** + * 新增场景,需要授权插件执行授权 + * @return 授权插件 + */ + Class grandPermissionPlugin() default EmptyPlugin.class; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java new file mode 100644 index 00000000..e0179fe2 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java @@ -0,0 +1,65 @@ +package tech.powerjob.server.auth.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * ApiPermission 切面 + * 主要用于执行授权插件,完成创建后授权 + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +@Aspect +@Component +public class ApiPermissionAspect { + + @Pointcut("@annotation(ApiPermission)") + public void apiPermissionPointcut() { + // 定义切入点 + } + + /** + * 后置返回 + * 如果第一个参数为JoinPoint,则第二个参数为返回值的信息 + * 如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数 + * returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行, + * 参数为Object类型将匹配任何目标返回值 + * After注解标注的方法会在目标方法执行后运行,无论目标方法是正常完成还是抛出异常。它相当于finally块,因为它总是执行,所以适用于释放资源等清理活动。@After注解不能访问目标方法的返回值。 + * AfterReturning注解标注的方法仅在目标方法成功执行后(即正常返回)运行。它可以访问目标方法的返回值。使用@AfterReturning可以在方法正常返回后执行一些逻辑,比如对返回值进行处理或验证。 + */ + @AfterReturning(value = "apiPermissionPointcut()", returning = "result") + public void doAfterReturningAdvice1(JoinPoint joinPoint, Object result) { + + // 入参 + Object[] args = joinPoint.getArgs(); + + // 获取目标方法 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + ApiPermission annotationAnno = AnnotationUtils.getAnnotation(method, ApiPermission.class); + + assert annotationAnno != null; + + Class grandPermissionPluginClz = annotationAnno.grandPermissionPlugin(); + + try { + GrantPermissionPlugin grandPermissionPlugin = grandPermissionPluginClz.getDeclaredConstructor().newInstance(); + grandPermissionPlugin.grant(args, result, method, joinPoint.getTarget()); + } catch (Exception e) { + log.error("[ApiPermissionAspect] process ApiPermission grant failed", e); + ExceptionUtils.rethrow(e); + } + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/DynamicPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/DynamicPermissionPlugin.java new file mode 100644 index 00000000..504deb6e --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/DynamicPermissionPlugin.java @@ -0,0 +1,15 @@ +package tech.powerjob.server.auth.interceptor; + +import tech.powerjob.server.auth.Permission; + +import javax.servlet.http.HttpServletRequest; + +/** + * 动态权限 + * + * @author tjq + * @since 2023/9/3 + */ +public interface DynamicPermissionPlugin { + Permission calculate(HttpServletRequest request, Object handler); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/EmptyPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/EmptyPlugin.java new file mode 100644 index 00000000..95d00b8f --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/EmptyPlugin.java @@ -0,0 +1,24 @@ +package tech.powerjob.server.auth.interceptor; + +import tech.powerjob.server.auth.Permission; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; + +/** + * 空 + * + * @author tjq + * @since 2024/2/12 + */ +public class EmptyPlugin implements DynamicPermissionPlugin, GrantPermissionPlugin { + @Override + public Permission calculate(HttpServletRequest request, Object handler) { + return null; + } + + @Override + public void grant(Object[] args, Object result, Method method, Object originBean) { + + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/GrantPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/GrantPermissionPlugin.java new file mode 100644 index 00000000..34d085ad --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/GrantPermissionPlugin.java @@ -0,0 +1,21 @@ +package tech.powerjob.server.auth.interceptor; + +import java.lang.reflect.Method; + +/** + * 授予权限插件 + * + * @author tjq + * @since 2024/2/11 + */ +public interface GrantPermissionPlugin { + + /** + * 授权 + * @param args 入参 + * @param result 响应 + * @param method 被调用方法 + * @param originBean 原始对象 + */ + void grant(Object[] args, Object result, Method method, Object originBean); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java new file mode 100644 index 00000000..f88d99ee --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java @@ -0,0 +1,135 @@ +package tech.powerjob.server.auth.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import tech.powerjob.common.exception.ImpossibleException; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.common.utils.HttpServletUtils; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.common.Loggers; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * login auth and permission check + * + * @author tjq + * @since 2023/3/25 + */ +@Slf4j +@Component +public class PowerJobAuthInterceptor implements HandlerInterceptor { + @Resource + private PowerJobLoginService powerJobLoginService; + @Resource + private PowerJobPermissionService powerJobPermissionService; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler) throws Exception { + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; + final Method method = handlerMethod.getMethod(); + final ApiPermission apiPermissionAnno = method.getAnnotation(ApiPermission.class); + + // 无注解代表不需要权限,无需登陆直接访问 + if (apiPermissionAnno == null) { + return true; + } + + // 尝试直接解析登陆 + final Optional loginUserOpt = powerJobLoginService.ifLogin(request); + + // 未登录直接报错,返回固定状态码,前端拦截后跳转到登录页 + if (!loginUserOpt.isPresent()) { + throw new PowerJobAuthException(ErrorCodes.USER_NOT_LOGIN); + } + + // 登陆用户进行权限校验 + final PowerJobUser powerJobUser = loginUserOpt.get(); + + // 写入上下文 + LoginUserHolder.set(powerJobUser); + + Permission requiredPermission = parsePermission(request, handler, apiPermissionAnno); + RoleScope roleScope = apiPermissionAnno.roleScope(); + Long targetId = null; + + if (RoleScope.NAMESPACE.equals(roleScope)) { + + final String namespaceIdStr = HttpServletUtils.fetchFromHeader("NamespaceId", request); + if (StringUtils.isNotEmpty(namespaceIdStr)) { + targetId = Long.valueOf(namespaceIdStr); + } + } + + if (RoleScope.APP.equals(roleScope)) { + final String appIdStr = HttpServletUtils.fetchFromHeader("AppId", request); + if (StringUtils.isNotEmpty(appIdStr)) { + targetId = Long.valueOf(appIdStr); + } + } + + + final boolean hasPermission = powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, targetId, requiredPermission); + if (hasPermission) { + return true; + } + + final String resourceName = parseResourceName(apiPermissionAnno, handlerMethod); + Loggers.WEB.info("[PowerJobAuthInterceptor] user[{}] has no permission to access: {}", powerJobUser.getUsername(), resourceName); + + throw new PowerJobException("Permission denied!"); + } + + @Override + public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception { + LoginUserHolder.clean(); + } + + private static String parseResourceName(ApiPermission apiPermission, HandlerMethod handlerMethod) { + final String name = apiPermission.name(); + if (StringUtils.isNotEmpty(name)) { + return name; + } + try { + final String clzName = handlerMethod.getBean().getClass().getSimpleName(); + final String methodName = handlerMethod.getMethod().getName(); + return String.format("%s_%s", clzName, methodName); + } catch (Exception ignore) { + } + return "UNKNOWN"; + } + + private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) { + Class dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin(); + if (EmptyPlugin.class.equals(dynamicPermissionPlugin)) { + return apiPermission.requiredPermission(); + } + try { + DynamicPermissionPlugin dynamicPermission = dynamicPermissionPlugin.getDeclaredConstructor().newInstance(); + return dynamicPermission.calculate(request, handler); + } catch (Throwable t) { + log.error("[PowerJobAuthService] process dynamicPermissionPlugin failed!", t); + ExceptionUtils.rethrow(t); + } + throw new ImpossibleException(); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/JwtService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/JwtService.java new file mode 100644 index 00000000..28564b08 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/JwtService.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.jwt; + +import java.util.Map; + +/** + * JWT 服务 + * + * @author tjq + * @since 2023/3/20 + */ +public interface JwtService { + + String build(Map body, String extraSk); + + ParseResult parse(String jwt, String extraSk); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/ParseResult.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/ParseResult.java new file mode 100644 index 00000000..04b39768 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/ParseResult.java @@ -0,0 +1,35 @@ +package tech.powerjob.server.auth.jwt; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Map; + +/** + * 解析结果 + * + * @author tjq + * @since 2024/8/11 + */ +@Data +@Accessors(chain = true) +public class ParseResult implements Serializable { + + /** + * 解析状态 + */ + private Status status; + /** + * 解析结果 + */ + private Map result; + + private String msg; + + public enum Status { + SUCCESS, + EXPIRED, + FAILED + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/SecretProvider.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/SecretProvider.java new file mode 100644 index 00000000..349f393e --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/SecretProvider.java @@ -0,0 +1,13 @@ +package tech.powerjob.server.auth.jwt; + +/** + * JWT 安全性的核心 + * 对安全性有要求的接入方,可以自行重新该方法,自定义自己的安全 token 生成策略 + * + * @author tjq + * @since 2023/3/20 + */ +public interface SecretProvider { + + String fetchSecretKey(); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java new file mode 100644 index 00000000..6444f309 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java @@ -0,0 +1,47 @@ +package tech.powerjob.server.auth.jwt.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import tech.powerjob.server.auth.jwt.SecretProvider; +import tech.powerjob.common.utils.DigestUtils; + +import javax.annotation.Resource; + +/** + * PowerJob 默认实现 + * + * @author tjq + * @since 2023/3/20 + */ +@Slf4j +@Component +public class DefaultSecretProvider implements SecretProvider { + + @Resource + private Environment environment; + + private static final String PROPERTY_KEY = "spring.datasource.core.jdbc-url"; + + @Override + public String fetchSecretKey() { + + // 考虑到大部分用户都是开箱即用,此处还是提供一个相对安全的默认实现。JDBC URL 部署时必会改,skey 不固定,更安全 + try { + String propertyValue = environment.getProperty(PROPERTY_KEY); + if (StringUtils.isNotEmpty(propertyValue)) { + String md5 = DigestUtils.md5(propertyValue); + + log.debug("[DefaultSecretProvider] propertyValue: {} ==> md5: {}", propertyValue, md5); + + if (StringUtils.isNotEmpty(md5)) { + return md5; + } + } + } catch (Exception ignore) { + } + + return "ZQQZJ"; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/JwtServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/JwtServiceImpl.java new file mode 100644 index 00000000..871ea236 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/JwtServiceImpl.java @@ -0,0 +1,104 @@ +package tech.powerjob.server.auth.jwt.impl; + +import com.google.common.collect.Maps; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.powerjob.server.auth.jwt.JwtService; +import tech.powerjob.server.auth.jwt.ParseResult; +import tech.powerjob.server.auth.jwt.SecretProvider; + +import javax.annotation.Resource; +import java.security.Key; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +/** + * JWT 默认实现 + * + * @author tjq + * @since 2023/3/20 + */ +@Slf4j +@Service +public class JwtServiceImpl implements JwtService { + + @Resource + private SecretProvider secretProvider; + + /** + * JWT 客户端过期时间 + */ + @Value("${oms.auth.security.jwt.expire-seconds:604800}") + private int jwtExpireTime; + + /** + * GoodSong + */ + private static final String BASE_SECURITY = + "CengMengXiangZhangJianZouTianYa" + + "KanYiKanShiJieDeFanHua" + + "NianShaoDeXinZongYouXieQingKuang" + + "RuJinWoSiHaiWeiJia" + ; + + @Override + public String build(Map body, String extraSk) { + + final String secret = fetchSk(extraSk); + return innerBuild(secret, jwtExpireTime, body); + } + + static String innerBuild(String secret, int expireSeconds, Map body) { + JwtBuilder jwtBuilder = Jwts.builder() + .setHeaderParam("typ", "JWT") + .addClaims(body) + .setSubject("PowerJob") + .setExpiration(new Date(System.currentTimeMillis() + 1000L * expireSeconds)) + .setId(UUID.randomUUID().toString()) + .signWith(genSecretKey(secret), SignatureAlgorithm.HS256); + return jwtBuilder.compact(); + } + + @Override + public ParseResult parse(String jwt, String extraSk) { + try { + Map parseResult = innerParse(fetchSk(extraSk), jwt); + return new ParseResult().setStatus(ParseResult.Status.SUCCESS).setResult(parseResult); + } catch (ExpiredJwtException expiredJwtException) { + return new ParseResult().setStatus(ParseResult.Status.EXPIRED).setMsg(expiredJwtException.getMessage()); + } catch (Exception e) { + log.warn("[JwtService] parse jwt[{}] with extraSk[{}] failed", jwt, extraSk, e); + return new ParseResult().setStatus(ParseResult.Status.FAILED).setMsg(ExceptionUtils.getMessage(e)); + } + } + + private String fetchSk(String extraSk) { + if (StringUtils.isEmpty(extraSk)) { + return secretProvider.fetchSecretKey(); + } + return secretProvider.fetchSecretKey().concat(extraSk); + } + + static Map innerParse(String secret, String jwtStr) { + final Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(genSecretKey(secret)) + .build() + .parseClaimsJws(jwtStr); + Map ret = Maps.newHashMap(); + ret.putAll(claimsJws.getBody()); + return ret; + } + + private static Key genSecretKey(String secret) { + byte[] keyBytes = Decoders.BASE64.decode(BASE_SECURITY.concat(secret)); + return Keys.hmacShaKeyFor(keyBytes); + } + +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java new file mode 100644 index 00000000..0ecf38a1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java @@ -0,0 +1,30 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 登录类型描述 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class LoginTypeInfo implements Serializable { + + /** + * 登录类型,唯一标识 + */ + private String type; + /** + * 描述名称,前端展示用 + */ + private String name; + /** + * 展示用的 ICON + */ + private String iconUrl; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginRequest.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginRequest.java new file mode 100644 index 00000000..52f74efc --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginRequest.java @@ -0,0 +1,24 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.servlet.http.HttpServletRequest; + +/** + * 第三方登录请求 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class ThirdPartyLoginRequest { + + /** + * 原始参数,给第三方登录方式一个服务端和前端交互的数据通道。PowerJob 本身不感知其中的内容 + */ + private String originParams; + + private transient HttpServletRequest httpServletRequest; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java new file mode 100644 index 00000000..16dd819b --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java @@ -0,0 +1,42 @@ +package tech.powerjob.server.auth.login; + +import javax.servlet.http.HttpServletRequest; + +/** + * 第三方登录服务 + * + * @author tjq + * @since 2024/2/10 + */ +public interface ThirdPartyLoginService { + + /** + * 登陆服务的类型 + * @return 登陆服务类型,比如 PowerJob / DingTalk + */ + LoginTypeInfo loginType(); + + /** + * 生成登陆的重定向 URL + * @param httpServletRequest http请求 + * @return 重定向地址 + */ + String generateLoginUrl(HttpServletRequest httpServletRequest); + + /** + * 执行第三方登录 + * @param loginRequest 上下文 + * @return 登录地址 + */ + ThirdPartyUser login(ThirdPartyLoginRequest loginRequest); + + /** + * JWT 登录的回调校验 + * @param username 用户名称 + * @param tokenLoginVerifyInfo 二次校验信息 + * @return 是否通过 + */ + default boolean tokenLoginVerify(String username, TokenLoginVerifyInfo tokenLoginVerifyInfo) { + return true; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java new file mode 100644 index 00000000..f34604a1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java @@ -0,0 +1,47 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 第三方用户 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class ThirdPartyUser { + + /** + * 用户的唯一标识,用于关联到 PowerJob 的 username + */ + private String username; + /** + * JWT 登录的二次校验配置 + * 可空,空则代表放弃二次校验(会出现第三方登录改了密码当 PowerJob JWT 登录依然可用的情况) + */ + private TokenLoginVerifyInfo tokenLoginVerifyInfo; + + /* ******** 以下全部选填即可,只是方便数据同步,后续都可以去 PowerJob 控制台更改 ******** */ + /** + * 用户昵称 + */ + private String nick; + /** + * 手机号 + */ + private String phone; + /** + * 邮箱地址 + */ + private String email; + /** + * web 回调地址 + */ + private String webHook; + /** + * 扩展字段 + */ + private String extra; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java new file mode 100644 index 00000000..e60f0077 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java @@ -0,0 +1,31 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; + +import java.io.Serializable; + +/** + * JWT 登录时的校验信息 + * + * @author tjq + * @since 2024/2/16 + */ +@Data +public class TokenLoginVerifyInfo implements Serializable { + + /** + * 加密 token 部分,比如密码的 md5,会直接写入 JWT 下发给前端 + * 如果需要使用 JWT 二次校验,则该参数必须存在 + */ + private String encryptedToken; + + /** + * 补充信息,用于二次校验 + */ + private String additionalInfo; + + /** + * 依然是预留字段,第三方实现自用即可 + */ + private String extra; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java new file mode 100644 index 00000000..c9a646c3 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java @@ -0,0 +1,142 @@ +package tech.powerjob.server.auth.login.impl; + +import com.aliyun.dingtalkcontact_1_0.models.GetUserHeaders; +import com.aliyun.dingtalkcontact_1_0.models.GetUserResponseBody; +import com.aliyun.dingtalkoauth2_1_0.models.GetUserTokenRequest; +import com.aliyun.dingtalkoauth2_1_0.models.GetUserTokenResponse; +import com.aliyun.teaopenapi.models.Config; +import com.aliyun.teautil.models.RuntimeOptions; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.login.*; +import tech.powerjob.server.common.Loggers; + +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 钉钉账号体系登录第三方网站 + * PowerJob 官方支持钉钉账号体系登录原因: + * 1. 钉钉作为当下用户体量最大的企业级办公软件,覆盖率足够高,提供钉钉支持能让更多开发者开箱即用 + * 2. 钉钉的 API 设计和 PowerJob 设想一致,算是个最佳实践,其他企业内部的账号体系可参考这套流程进行接入 + * - PowerJob 重定向到第三方账号体系登陆页 -> 第三方完成登陆 -> 跳转回调 PowerJob auth 接口 -> PowerJob 解析回调登陆信息,完整用户关联 + * + * @author tjq + * @since 2023/3/26 + */ +@Service +public class DingTalkLoginService implements ThirdPartyLoginService { + + /* + 配置示例 + oms.auth.dingtalk.appkey=dinggzqqzqqzqqzqq + oms.auth.dingtalk.appSecret=iY-FS8mzqqzqq_xEizqqzqqzqqzqqzqqzqqYEbkZOal + oms.auth.dingtalk.callbackUrl=http://localhost:7700 + */ + + /** + * 钉钉应用 AppKey + */ + @Value("${oms.auth.dingtalk.appkey:#{null}}") + private String dingTalkAppKey; + /** + * 钉钉应用 AppSecret + */ + @Value("${oms.auth.dingtalk.appSecret:#{null}}") + private String dingTalkAppSecret; + /** + * 回调地址,powerjob 前端控制台地址,即 powerjob-console 地址 + * 比如本地调试时为 LocalDemoCallbackUrl + * 部署后则为 demoCallBackUrl + */ + @Value("${oms.auth.dingtalk.callbackUrl:#{null}}") + private String dingTalkCallbackUrl; + + @Override + public LoginTypeInfo loginType() { + return new LoginTypeInfo() + .setType(AuthConstants.ACCOUNT_TYPE_DING) + .setName("DingTalk") + ; + } + + @Override + @SneakyThrows + public String generateLoginUrl(HttpServletRequest httpServletRequest) { + if (StringUtils.isAnyEmpty(dingTalkAppKey, dingTalkAppSecret, dingTalkCallbackUrl)) { + throw new IllegalArgumentException("please config 'oms.auth.dingtalk.appkey', 'oms.auth.dingtalk.appSecret' and 'oms.auth.dingtalk.callbackUrl' in properties!"); + } + + String urlString = URLEncoder.encode(dingTalkCallbackUrl, StandardCharsets.UTF_8.name()); + String url = "https://login.dingtalk.com/oauth2/auth?" + + "redirect_uri=" + urlString + + "&response_type=code" + + "&client_id=" + dingTalkAppKey + + "&scope=openid" + + "&state=" + AuthConstants.ACCOUNT_TYPE_DING + + "&prompt=consent"; + Loggers.WEB.info("[DingTalkBizLoginService] login url: {}", url); + return url; + } + + @Override + @SneakyThrows + public ThirdPartyUser login(ThirdPartyLoginRequest loginRequest) { + try { + com.aliyun.dingtalkoauth2_1_0.Client client = authClient(); + GetUserTokenRequest getUserTokenRequest = new GetUserTokenRequest() + //应用基础信息-应用信息的AppKey,请务必替换为开发的应用AppKey + .setClientId(dingTalkAppKey) + //应用基础信息-应用信息的AppSecret,,请务必替换为开发的应用AppSecret + .setClientSecret(dingTalkAppSecret) + .setCode(loginRequest.getHttpServletRequest().getParameter("authCode")) + .setGrantType("authorization_code"); + GetUserTokenResponse getUserTokenResponse = client.getUserToken(getUserTokenRequest); + //获取用户个人 token + String accessToken = getUserTokenResponse.getBody().getAccessToken(); + // 查询钉钉用户 + final GetUserResponseBody dingUser = getUserinfo(accessToken); + // 将钉钉用户的唯一ID 和 PowerJob 账户体系的唯一键 username 关联 + if (dingUser != null) { + ThirdPartyUser bizUser = new ThirdPartyUser(); + bizUser.setUsername(dingUser.getUnionId()); + bizUser.setNick(dingUser.getNick()); + bizUser.setPhone(dingUser.getMobile()); + bizUser.setEmail(dingUser.getEmail()); + return bizUser; + } + } catch (Exception e) { + Loggers.WEB.error("[DingTalkBizLoginService] login by dingTalk failed!", e); + throw e; + } + throw new PowerJobException("login from dingTalk failed!"); + } + + /* 以下代码均拷自钉钉官网示例 */ + + private static com.aliyun.dingtalkoauth2_1_0.Client authClient() throws Exception { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + return new com.aliyun.dingtalkoauth2_1_0.Client(config); + } + private static com.aliyun.dingtalkcontact_1_0.Client contactClient() throws Exception { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + return new com.aliyun.dingtalkcontact_1_0.Client(config); + } + + private GetUserResponseBody getUserinfo(String accessToken) throws Exception { + com.aliyun.dingtalkcontact_1_0.Client client = contactClient(); + GetUserHeaders getUserHeaders = new GetUserHeaders(); + getUserHeaders.xAcsDingtalkAccessToken = accessToken; + //获取用户个人信息,如需获取当前授权人的信息,unionId参数必须传me + return client.getUserWithOptions("me", getUserHeaders, new RuntimeOptions()).getBody(); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java new file mode 100644 index 00000000..7e46a844 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java @@ -0,0 +1,121 @@ +package tech.powerjob.server.auth.login.impl; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.login.*; +import tech.powerjob.server.common.Loggers; +import tech.powerjob.common.utils.DigestUtils; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Optional; + +/** + * PowerJob 自带的登陆服务 + * 和应用主框架无关,依然属于第三方登录体系 + * + * @author tjq + * @since 2023/3/20 + */ +@Service +public class PwjbAccountLoginService implements ThirdPartyLoginService { + + @Resource + private PwjbUserInfoRepository pwjbUserInfoRepository; + + + @Override + public LoginTypeInfo loginType() { + return new LoginTypeInfo() + .setType(AuthConstants.ACCOUNT_TYPE_POWER_JOB) + .setName("PowerJob Account") + ; + } + + @Override + public String generateLoginUrl(HttpServletRequest httpServletRequest) { + // 前端实现跳转,服务端返回特殊指令 + return AuthConstants.FE_REDIRECT_KEY.concat("powerjobLogin"); + } + + @Override + public ThirdPartyUser login(ThirdPartyLoginRequest loginRequest) { + final String loginInfo = loginRequest.getOriginParams(); + if (StringUtils.isEmpty(loginInfo)) { + throw new IllegalArgumentException("can't find login Info"); + } + + Map loginInfoMap = JsonUtils.parseMap(loginInfo); + + final String username = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_USERNAME); + final String password = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_PASSWORD); + final String encryption = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_ENCRYPTION); + + Loggers.WEB.debug("[PowerJobLoginService] username: {}, password: {}, encryption: {}", username, password, encryption); + + if (StringUtils.isAnyEmpty(username, password)) { + Loggers.WEB.debug("[PowerJobLoginService] username or password is empty, login failed!"); + throw new PowerJobAuthException(ErrorCodes.INVALID_REQUEST); + } + + final Optional userInfoOpt = pwjbUserInfoRepository.findByUsername(username); + if (!userInfoOpt.isPresent()) { + Loggers.WEB.debug("[PowerJobLoginService] can't find user by username: {}", username); + throw new PowerJobAuthException(ErrorCodes.USER_NOT_EXIST); + } + + final PwjbUserInfoDO dbUser = userInfoOpt.get(); + + if (DigestUtils.rePassword(password, username).equals(dbUser.getPassword())) { + ThirdPartyUser bizUser = new ThirdPartyUser(); + bizUser.setUsername(username); + + // 回填第一次创建的信息 + String extra = dbUser.getExtra(); + if (StringUtils.isNotEmpty(extra)) { + ThirdPartyUser material = JsonUtils.parseObjectIgnoreException(extra, ThirdPartyUser.class); + if (material != null) { + bizUser.setEmail(material.getEmail()); + bizUser.setNick(material.getNick()); + bizUser.setPhone(material.getPhone()); + bizUser.setWebHook(material.getWebHook()); + } + } + + // 下发加密的密码作为 JWT 的一部分,方便处理改密码后失效的场景 + TokenLoginVerifyInfo tokenLoginVerifyInfo = new TokenLoginVerifyInfo(); + tokenLoginVerifyInfo.setEncryptedToken(dbUser.getPassword()); + bizUser.setTokenLoginVerifyInfo(tokenLoginVerifyInfo); + + return bizUser; + } + + Loggers.WEB.debug("[PowerJobLoginService] user[{}]'s password is incorrect, login failed!", username); + throw new PowerJobException("password is incorrect"); + } + + @Override + public boolean tokenLoginVerify(String username, TokenLoginVerifyInfo tokenLoginVerifyInfo) { + + if (tokenLoginVerifyInfo == null) { + return false; + } + + final Optional userInfoOpt = pwjbUserInfoRepository.findByUsername(username); + if (userInfoOpt.isPresent()) { + String dbPassword = userInfoOpt.get().getPassword(); + return StringUtils.equals(dbPassword, tokenLoginVerifyInfo.getEncryptedToken()); + } + + return false; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java new file mode 100644 index 00000000..a72bd06c --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java @@ -0,0 +1,32 @@ +package tech.powerjob.server.auth.service.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.servlet.http.HttpServletRequest; + +/** + * 执行登录的请求 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class LoginRequest { + + /** + * 登录类型 + */ + private String loginType; + + /** + * 原始参数,给第三方登录方式一个服务端和前端交互的数据通道。PowerJob 本身不感知其中的内容 + */ + private String originParams; + + /** + * http原始请求,第三方回调参数传递类型无法枚举,直接传递 HttpServletRequest 满足扩展性要求 + */ + private transient HttpServletRequest httpServletRequest; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/PowerJobLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/PowerJobLoginService.java new file mode 100644 index 00000000..f932b5be --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/PowerJobLoginService.java @@ -0,0 +1,48 @@ +package tech.powerjob.server.auth.service.login; + +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.login.LoginTypeInfo; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Optional; + +/** + * PowerJob 登录服务 + * + * @author tjq + * @since 2024/2/10 + */ +public interface PowerJobLoginService { + + /** + * 获取全部可登录的类型 + * @return 全部可登录类型 + */ + List fetchSupportLoginTypes(); + + + /** + * 获取第三方登录链接 + * @param loginType 登录类型 + * @param httpServletRequest http请求 + * @return 重定向地址 + */ + String fetchThirdPartyLoginUrl(String loginType, HttpServletRequest httpServletRequest); + + /** + * 执行真正的登录请求,底层调用第三方登录服务完成登录 + * @param loginRequest 登录请求 + * @return 登录完成的 PowerJobUser + * @throws PowerJobAuthException 鉴权失败抛出异常 + */ + PowerJobUser doLogin(LoginRequest loginRequest) throws PowerJobAuthException; + + /** + * 从 JWT 信息中解析用户登录信息 + * @param httpServletRequest httpServletRequest + * @return PowerJob 用户 + */ + Optional ifLogin(HttpServletRequest httpServletRequest); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java new file mode 100644 index 00000000..80aa018e --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java @@ -0,0 +1,255 @@ +package tech.powerjob.server.auth.service.login.impl; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.common.utils.HttpServletUtils; +import tech.powerjob.server.auth.jwt.JwtService; +import tech.powerjob.server.auth.login.*; +import tech.powerjob.server.auth.service.login.LoginRequest; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.common.Loggers; +import tech.powerjob.common.enums.SwitchableStatus; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; + +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * PowerJob 登录服务 + * + * @author tjq + * @since 2024/2/10 + */ +@Slf4j +@Service +public class PowerJobLoginServiceImpl implements PowerJobLoginService { + + private final JwtService jwtService; + private final UserInfoRepository userInfoRepository; + private final Map code2ThirdPartyLoginService; + + @Autowired + public PowerJobLoginServiceImpl(JwtService jwtService, UserInfoRepository userInfoRepository, List thirdPartyLoginServices) { + + this.jwtService = jwtService; + this.userInfoRepository = userInfoRepository; + + code2ThirdPartyLoginService = Maps.newHashMap(); + thirdPartyLoginServices.forEach(s -> { + code2ThirdPartyLoginService.put(s.loginType().getType(), s); + log.info("[PowerJobLoginService] register ThirdPartyLoginService: {}", s.loginType()); + }); + } + + @Override + public List fetchSupportLoginTypes() { + return Lists.newArrayList(code2ThirdPartyLoginService.values()).stream().map(ThirdPartyLoginService::loginType).collect(Collectors.toList()); + } + + @Override + public String fetchThirdPartyLoginUrl(String type, HttpServletRequest httpServletRequest) { + final ThirdPartyLoginService thirdPartyLoginService = fetchBizLoginService(type); + return thirdPartyLoginService.generateLoginUrl(httpServletRequest); + } + + @Override + public PowerJobUser doLogin(LoginRequest loginRequest) throws PowerJobAuthException { + final String loginType = loginRequest.getLoginType(); + final ThirdPartyLoginService thirdPartyLoginService = fetchBizLoginService(loginType); + + ThirdPartyLoginRequest thirdPartyLoginRequest = new ThirdPartyLoginRequest() + .setOriginParams(loginRequest.getOriginParams()) + .setHttpServletRequest(loginRequest.getHttpServletRequest()); + + final ThirdPartyUser bizUser = thirdPartyLoginService.login(thirdPartyLoginRequest); + + String dbUserName = String.format("%s_%s", loginType, bizUser.getUsername()); + Optional powerJobUserOpt = userInfoRepository.findByUsername(dbUserName); + + // 如果不存在用户,先同步创建用户 + if (!powerJobUserOpt.isPresent()) { + UserInfoDO newUser = new UserInfoDO(); + newUser.setUsername(dbUserName); + // 写入账号体系类型 + newUser.setAccountType(loginType); + newUser.setOriginUsername(bizUser.getUsername()); + + newUser.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo())); + + // 同步素材 + newUser.setEmail(bizUser.getEmail()); + newUser.setPhone(bizUser.getPhone()); + newUser.setNick(bizUser.getNick()); + newUser.setWebHook(bizUser.getWebHook()); + newUser.setExtra(bizUser.getExtra()); + + Loggers.WEB.info("[PowerJobLoginService] sync user to PowerJobUserSystem: {}", dbUserName); + userInfoRepository.saveAndFlush(newUser); + + powerJobUserOpt = userInfoRepository.findByUsername(dbUserName); + } else { + + UserInfoDO dbUserInfoDO = powerJobUserOpt.get(); + + checkUserStatus(dbUserInfoDO); + + // 更新二次校验的 TOKEN 信息 + dbUserInfoDO.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo())); + dbUserInfoDO.setGmtModified(new Date()); + + userInfoRepository.saveAndFlush(dbUserInfoDO); + } + + PowerJobUser ret = new PowerJobUser(); + + // 理论上 100% 存在 + if (powerJobUserOpt.isPresent()) { + final UserInfoDO dbUser = powerJobUserOpt.get(); + BeanUtils.copyProperties(dbUser, ret); + ret.setUsername(dbUserName); + } + + fillJwt(ret, Optional.ofNullable(bizUser.getTokenLoginVerifyInfo()).map(TokenLoginVerifyInfo::getEncryptedToken).orElse(null)); + + return ret; + } + + @Override + public Optional ifLogin(HttpServletRequest httpServletRequest) { + final Optional jwtBodyOpt = parseJwt(httpServletRequest); + if (!jwtBodyOpt.isPresent()) { + return Optional.empty(); + } + + JwtBody jwtBody = jwtBodyOpt.get(); + + Optional dbUserInfoOpt = userInfoRepository.findByUsername(jwtBody.getUsername()); + if (!dbUserInfoOpt.isPresent()) { + throw new PowerJobAuthException(ErrorCodes.USER_NOT_EXIST); + } + + UserInfoDO dbUser = dbUserInfoOpt.get(); + + checkUserStatus(dbUser); + + PowerJobUser powerJobUser = new PowerJobUser(); + + String tokenLoginVerifyInfoStr = dbUser.getTokenLoginVerifyInfo(); + TokenLoginVerifyInfo tokenLoginVerifyInfo = Optional.ofNullable(tokenLoginVerifyInfoStr).map(x -> JsonUtils.parseObjectIgnoreException(x, TokenLoginVerifyInfo.class)).orElse(new TokenLoginVerifyInfo()); + + // DB 中的 encryptedToken 存在,代表需要二次校验 + if (StringUtils.isNotEmpty(tokenLoginVerifyInfo.getEncryptedToken())) { + if (!StringUtils.equals(jwtBody.getEncryptedToken(), tokenLoginVerifyInfo.getEncryptedToken())) { + throw new PowerJobAuthException(ErrorCodes.INVALID_TOKEN); + } + + ThirdPartyLoginService thirdPartyLoginService = code2ThirdPartyLoginService.get(dbUser.getAccountType()); + boolean tokenLoginVerifyOk = thirdPartyLoginService.tokenLoginVerify(dbUser.getOriginUsername(), tokenLoginVerifyInfo); + + if (!tokenLoginVerifyOk) { + throw new PowerJobAuthException(ErrorCodes.USER_AUTH_FAILED); + } + } + + BeanUtils.copyProperties(dbUser, powerJobUser); + + // 兼容某些直接通过 ifLogin 判断登录的场景 + LoginUserHolder.set(powerJobUser); + + return Optional.of(powerJobUser); + } + + /** + * 检查 user 状态 + * @param dbUser user + */ + private void checkUserStatus(UserInfoDO dbUser) { + int accountStatus = Optional.ofNullable(dbUser.getStatus()).orElse(SwitchableStatus.ENABLE.getV()); + if (accountStatus == SwitchableStatus.DISABLE.getV()) { + throw new PowerJobAuthException(ErrorCodes.USER_DISABLED); + } + } + + private ThirdPartyLoginService fetchBizLoginService(String loginType) { + final ThirdPartyLoginService loginService = code2ThirdPartyLoginService.get(loginType); + if (loginService == null) { + throw new PowerJobAuthException(ErrorCodes.INVALID_REQUEST, "can't find ThirdPartyLoginService by type: " + loginType); + } + return loginService; + } + + private void fillJwt(PowerJobUser powerJobUser, String encryptedToken) { + + // 不能下发 userId,容易被轮询爆破 + JwtBody jwtBody = new JwtBody(); + jwtBody.setUsername(powerJobUser.getUsername()); + if (StringUtils.isNotEmpty(encryptedToken)) { + jwtBody.setEncryptedToken(encryptedToken); + } + + Map jwtMap = JsonUtils.parseMap(JsonUtils.toJSONString(jwtBody)); + + powerJobUser.setJwtToken(jwtService.build(jwtMap, null)); + } + + @SneakyThrows + private Optional parseJwt(HttpServletRequest httpServletRequest) { + // header、cookie 都能获取 + String jwtStr = HttpServletUtils.fetchFromHeader(AuthConstants.JWT_NAME, httpServletRequest); + if (StringUtils.isEmpty(jwtStr)) { + jwtStr = HttpServletUtils.fetchFromHeader(AuthConstants.OLD_JWT_NAME, httpServletRequest); + } + + /* + + 开发阶段跨域无法简单传输 cookies,暂时采取 header 方案传输 JWT + + if (StringUtils.isEmpty(jwtStr)) { + for (Cookie cookie : Optional.ofNullable(httpServletRequest.getCookies()).orElse(new Cookie[]{})) { + if (cookie.getName().equals(AuthConstants.JWT_NAME)) { + jwtStr = cookie.getValue(); + } + } + } + */ + + if (StringUtils.isEmpty(jwtStr)) { + return Optional.empty(); + } + final Map jwtBodyMap = jwtService.parse(jwtStr, null).getResult(); + + if (MapUtils.isEmpty(jwtBodyMap)) { + return Optional.empty(); + } + + return Optional.ofNullable(JsonUtils.parseObject(JsonUtils.toJSONString(jwtBodyMap), JwtBody.class)); + } + + @Data + static class JwtBody implements Serializable { + + private String username; + + private String encryptedToken; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java new file mode 100644 index 00000000..64dd7c31 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java @@ -0,0 +1,64 @@ +package tech.powerjob.server.auth.service.permission; + +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * PowerJob 鉴权服务 + * + * @author tjq + * @since 2024/2/11 + */ +public interface PowerJobPermissionService { + + + /** + * 判断用户是否有访问权限 + * @param userId userId + * @param roleScope 权限范围 + * @param target 权限目标ID + * @param permission 要求的权限 + * @return 是否有权限 + */ + boolean hasPermission(Long userId, RoleScope roleScope, Long target, Permission permission); + + /** + * 授予用户角色 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param userId 用户ID + * @param role 角色 + * @param extra 其他 + */ + void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra); + + /** + * 回收用户角色 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param userId 用户ID + * @param role 角色 + */ + void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role); + + /** + * 获取有相关权限的用户 + * @param roleScope 角色范围 + * @param target 目标 + * @return 角色对应的用户列表,user 可能重复,需要用 SET 去重(save APP/namespace 等场景,创建人自动被授权成为 ADMIN,如果用户在面板将自己添加到管理员,就会存在2套授权机制2次授权出现重复) + */ + Map> fetchUserWithPermissions(RoleScope roleScope, Long target); + + /** + * 获取用户有权限的目标 + * @param roleScope 角色范围 + * @param userId 用户ID + * @return result + */ + Map> fetchUserHadPermissionTargets(RoleScope roleScope, Long userId); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java new file mode 100644 index 00000000..9b1d4970 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java @@ -0,0 +1,175 @@ +package tech.powerjob.server.auth.service.permission; + +import com.google.common.collect.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.UserRoleDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.persistence.remote.repository.UserRoleRepository; + +import javax.annotation.Resource; +import java.util.*; + +/** + * PowerJobPermissionService + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +@Service +public class PowerJobPermissionServiceImpl implements PowerJobPermissionService { + + @Resource + private AppInfoRepository appInfoRepository; + @Resource + private UserRoleRepository userRoleRepository; + + @Override + public boolean hasPermission(Long userId, RoleScope roleScope, Long target, Permission requiredPermission) { + final List userRoleList = Optional.ofNullable(userRoleRepository.findAllByUserId(userId)).orElse(Collections.emptyList()); + + Multimap appId2Role = ArrayListMultimap.create(); + Multimap namespaceId2Role = ArrayListMultimap.create(); + + List globalRoles = Lists.newArrayList(); + + for (UserRoleDO userRole : userRoleList) { + final Role role = Role.of(userRole.getRole()); + + // 处理全局权限 + if (RoleScope.GLOBAL.getV() == userRole.getScope()) { + if (Role.ADMIN.equals(role)) { + return true; + } + globalRoles.add(role); + } + + if (RoleScope.NAMESPACE.getV() == userRole.getScope()) { + namespaceId2Role.put(userRole.getTarget(), role); + } + if (RoleScope.APP.getV() == userRole.getScope()) { + appId2Role.put(userRole.getTarget(), role); + } + } + + // 前置判断需要的权限(新增场景还没有 appId or namespaceId) + if (requiredPermission == Permission.NONE) { + return true; + } + + // 检验全局穿透权限 + for (Role role : globalRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + // 无超级管理员权限,校验普通权限 + if (RoleScope.APP.equals(roleScope)) { + return checkAppPermission(target, requiredPermission, appId2Role, namespaceId2Role); + } + + if (RoleScope.NAMESPACE.equals(roleScope)) { + return checkNamespacePermission(target, requiredPermission, namespaceId2Role); + } + + return false; + } + + @Override + public void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra) { + + UserRoleDO userRoleDO = new UserRoleDO(); + userRoleDO.setGmtCreate(new Date()); + userRoleDO.setGmtModified(new Date()); + userRoleDO.setExtra(extra); + + userRoleDO.setScope(roleScope.getV()); + userRoleDO.setTarget(target); + userRoleDO.setUserId(userId); + userRoleDO.setRole(role.getV()); + + userRoleRepository.saveAndFlush(userRoleDO); + log.info("[PowerJobPermissionService] [grantPermission] saveAndFlush userRole successfully: {}", userRoleDO); + } + + @Override + public void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role) { + List originUserRole = userRoleRepository.findAllByScopeAndTargetAndRoleAndUserId(roleScope.getV(), target, role.getV(), userId); + log.info("[PowerJobPermissionService] [retrievePermission] origin rule: {}", originUserRole); + Optional.ofNullable(originUserRole).orElse(Collections.emptyList()).forEach(r -> { + userRoleRepository.deleteById(r.getId()); + log.info("[PowerJobPermissionService] [retrievePermission] delete UserRole: {}", r); + }); + } + + @Override + public Map> fetchUserWithPermissions(RoleScope roleScope, Long target) { + List permissionUserList = userRoleRepository.findAllByScopeAndTarget(roleScope.getV(), target); + Map> ret = Maps.newHashMap(); + Optional.ofNullable(permissionUserList).orElse(Collections.emptyList()).forEach(userRoleDO -> { + Role role = Role.of(userRoleDO.getRole()); + Set userIds = ret.computeIfAbsent(role, ignore -> Sets.newHashSet()); + userIds.add(userRoleDO.getUserId()); + }); + + return ret; + } + + @Override + public Map> fetchUserHadPermissionTargets(RoleScope roleScope, Long userId) { + + Map> ret = Maps.newHashMap(); + List userRoleDOList = userRoleRepository.findAllByUserIdAndScope(userId, roleScope.getV()); + + Optional.ofNullable(userRoleDOList).orElse(Collections.emptyList()).forEach(r -> { + Role role = Role.of(r.getRole()); + List targetIds = ret.computeIfAbsent(role, ignore -> Lists.newArrayList()); + targetIds.add(r.getTarget()); + }); + + return ret; + } + + private boolean checkAppPermission(Long targetId, Permission requiredPermission, Multimap appId2Role, Multimap namespaceId2Role) { + + final Collection appRoles = appId2Role.get(targetId); + for (Role role : appRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + // 校验 namespace 穿透权限 + Optional appInfoOpt = appInfoRepository.findById(targetId); + if (!appInfoOpt.isPresent()) { + throw new IllegalArgumentException("can't find appInfo by appId in permission check: " + targetId); + } + Long namespaceId = Optional.ofNullable(appInfoOpt.get().getNamespaceId()).orElse(-1L); + Collection namespaceRoles = namespaceId2Role.get(namespaceId); + for (Role role : namespaceRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + return false; + } + + private boolean checkNamespacePermission(Long targetId, Permission requiredPermission, Multimap namespaceId2Role) { + Collection namespaceRoles = namespaceId2Role.get(targetId); + for (Role role : namespaceRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + return false; + } + +} diff --git a/powerjob-server/powerjob-server-common/pom.xml b/powerjob-server/powerjob-server-common/pom.xml index 72d44c41..3b61ba0e 100644 --- a/powerjob-server/powerjob-server-common/pom.xml +++ b/powerjob-server/powerjob-server-common/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java new file mode 100644 index 00000000..20b0e58c --- /dev/null +++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java @@ -0,0 +1,18 @@ +package tech.powerjob.server.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 统一定义日志 + * + * @author tjq + * @since 2023/3/25 + */ +public class Loggers { + + /** + * Web 层统一日志 + */ + public static final Logger WEB = LoggerFactory.getLogger("P_SERVER_LOGGER_WEB"); +} diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java index 89a89730..eb2051a8 100644 --- a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java +++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java @@ -3,6 +3,8 @@ package tech.powerjob.server.common; import com.google.common.base.Joiner; import com.google.common.base.Splitter; +import java.util.Map; + /** * Splitter & Joiner * @@ -16,4 +18,9 @@ public class SJ { public static final Joiner MONITOR_JOINER = Joiner.on("|").useForNull("-"); + private static final Splitter.MapSplitter MAP_SPLITTER = Splitter.onPattern(";").withKeyValueSeparator(":"); + + public static Map splitKvString(String kvString) { + return MAP_SPLITTER.split(kvString); + } } diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/AESUtil.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/AESUtil.java new file mode 100644 index 00000000..fba942e0 --- /dev/null +++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/AESUtil.java @@ -0,0 +1,92 @@ +package tech.powerjob.server.common.utils; + +import lombok.SneakyThrows; +import tech.powerjob.common.utils.DigestUtils; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +public class AESUtil { + + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int KEY_SIZE = 256; // AES 256-bit + private static final int GCM_NONCE_LENGTH = 12; // GCM nonce length (12 bytes) + private static final int GCM_TAG_LENGTH = 16; // GCM authentication tag length (16 bytes) + + // SecureRandom 实例,用于生成 nonce + private static final SecureRandom secureRandom = new SecureRandom(); + + /** + * 生成密钥 + * + * @param key 传入的密钥字符串,必须是 32 字节(256 位)长度 + * @return SecretKeySpec 实例 + */ + private static SecretKeySpec getKey(String key) { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + // 不足 32 字节,则使用 MD5 转为 32 位 + if (keyBytes.length != KEY_SIZE / 8) { + keyBytes = DigestUtils.md5(key).getBytes(StandardCharsets.UTF_8); + } + return new SecretKeySpec(keyBytes, ALGORITHM); + } + + /** + * 加密 + * + * @param data 要加密的数据 + * @param key 加密密钥 + * @return 加密后的数据(Base64 编码),包含 nonce + */ + @SneakyThrows + public static String encrypt(String data, String key) { + byte[] nonce = new byte[GCM_NONCE_LENGTH]; + secureRandom.nextBytes(nonce); // 生成随机的 nonce + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); + cipher.init(Cipher.ENCRYPT_MODE, getKey(key), gcmParameterSpec); + + byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + + // 将 nonce 和密文连接在一起,然后进行 Base64 编码 + byte[] combinedData = new byte[nonce.length + encryptedData.length]; + System.arraycopy(nonce, 0, combinedData, 0, nonce.length); + System.arraycopy(encryptedData, 0, combinedData, nonce.length, encryptedData.length); + + return Base64.getEncoder().encodeToString(combinedData); + } + + /** + * 解密 + * + * @param encryptedData 要解密的数据(Base64 编码),包含 nonce + * @param key 解密密钥 + * @return 解密后的数据 + */ + @SneakyThrows + public static String decrypt(String encryptedData, String key) { + byte[] combinedData = Base64.getDecoder().decode(encryptedData); + + // 提取 nonce + byte[] nonce = new byte[GCM_NONCE_LENGTH]; + System.arraycopy(combinedData, 0, nonce, 0, nonce.length); + + // 提取实际的加密数据 + byte[] encryptedText = new byte[combinedData.length - nonce.length]; + System.arraycopy(combinedData, nonce.length, encryptedText, 0, encryptedText.length); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); + cipher.init(Cipher.DECRYPT_MODE, getKey(key), gcmParameterSpec); + + byte[] decryptedData = cipher.doFinal(encryptedText); + return new String(decryptedData, StandardCharsets.UTF_8); + } +} diff --git a/powerjob-server/powerjob-server-common/src/test/java/tech/powerjob/server/common/utils/AESUtilTest.java b/powerjob-server/powerjob-server-common/src/test/java/tech/powerjob/server/common/utils/AESUtilTest.java new file mode 100644 index 00000000..84c924cf --- /dev/null +++ b/powerjob-server/powerjob-server-common/src/test/java/tech/powerjob/server/common/utils/AESUtilTest.java @@ -0,0 +1,28 @@ +package tech.powerjob.server.common.utils; + +import org.junit.jupiter.api.Test; + +/** + * AESUtilTest + * + * @author tjq + * @since 2024/8/10 + */ +class AESUtilTest { + + @Test + void testAes() throws Exception { + + String sk = "ChinaNo.1_ChinaNo.1_ChinaNo.1"; + + String txt = "kyksjdfh"; + + String encrypt = AESUtil.encrypt(txt, sk); + System.out.println(encrypt); + String decrypt = AESUtil.decrypt(encrypt, sk); + System.out.println(decrypt); + + assert txt.equals(decrypt); + } + +} \ No newline at end of file diff --git a/powerjob-server/powerjob-server-core/pom.xml b/powerjob-server/powerjob-server-core/pom.xml index 426bd20b..11b81f55 100644 --- a/powerjob-server/powerjob-server-core/pom.xml +++ b/powerjob-server/powerjob-server-core/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/container/ContainerService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/container/ContainerService.java index 58a636d1..9468ea99 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/container/ContainerService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/container/ContainerService.java @@ -37,7 +37,7 @@ import tech.powerjob.common.utils.NetUtils; import tech.powerjob.common.utils.SegmentLock; import tech.powerjob.remote.framework.base.URL; import tech.powerjob.server.common.constants.ContainerSourceType; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.module.WorkerInfo; import tech.powerjob.server.common.utils.OmsFileUtils; import tech.powerjob.server.extension.LockService; @@ -336,7 +336,6 @@ public class ContainerService { sb.append("WARN: there exists multi version container now, please redeploy to fix this problem").append(System.lineSeparator()); } - sb.append("divisive version ==> ").append(System.lineSeparator()); version2DeployedContainerInfoList.asMap().forEach((version, deployedContainerInfos) -> { sb.append("[version] ").append(version).append(System.lineSeparator()); deployedContainerInfos.forEach(deployedContainerInfo -> sb.append(String.format("Address: %s, DeployedTime: %s", deployedContainerInfo.getWorkerAddress(), CommonUtils.formatTime(deployedContainerInfo.getDeployedTime()))).append(System.lineSeparator())); diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/handler/AbWorkerRequestHandler.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/handler/AbWorkerRequestHandler.java index 13065d1e..595f135d 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/handler/AbWorkerRequestHandler.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/handler/AbWorkerRequestHandler.java @@ -12,7 +12,7 @@ import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.common.utils.NetUtils; import tech.powerjob.remote.framework.actor.Handler; import tech.powerjob.remote.framework.actor.ProcessType; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.module.WorkerInfo; import tech.powerjob.server.common.utils.SpringUtils; import tech.powerjob.server.monitor.MonitorService; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/InstanceStatusCheckService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/InstanceStatusCheckService.java index a6674c6d..951d37ed 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/InstanceStatusCheckService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/InstanceStatusCheckService.java @@ -12,7 +12,7 @@ import tech.powerjob.common.enums.InstanceStatus; import tech.powerjob.common.enums.TimeExpressionType; import tech.powerjob.common.enums.WorkflowInstanceStatus; import tech.powerjob.server.common.Holder; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.core.DispatchService; import tech.powerjob.server.core.instance.InstanceManager; import tech.powerjob.server.core.workflow.WorkflowInstanceManager; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/PowerScheduleService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/PowerScheduleService.java index 1b4fd5e8..7ab429d6 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/PowerScheduleService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/scheduler/PowerScheduleService.java @@ -11,7 +11,7 @@ import org.springframework.util.CollectionUtils; import tech.powerjob.common.enums.InstanceStatus; import tech.powerjob.common.enums.TimeExpressionType; import tech.powerjob.common.model.LifeCycle; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.timewheel.holder.InstanceTimeWheelService; import tech.powerjob.server.core.DispatchService; import tech.powerjob.server.core.instance.InstanceService; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java index 4e81b048..09821667 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/AppInfoService.java @@ -1,5 +1,9 @@ package tech.powerjob.server.core.service; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; + +import java.util.Optional; + /** * AppInfoService * @@ -7,5 +11,36 @@ package tech.powerjob.server.core.service; * @since 2023/3/4 */ public interface AppInfoService { - Long assertApp(String appName, String password); + + Optional findByAppName(String appName); + + /** + * 获取 AppInfo(带缓存) + * @param appId appId + * @param useCache cache + * @return App 信息 + */ + Optional findById(Long appId, boolean useCache); + + void deleteById(Long appId); + + /** + * 保存 App + * @param appInfo app 信息 + * @return 保存后结果 + */ + AppInfoDO save(AppInfoDO appInfo); + + /** + * + * @param appName 验证 APP 账号密码 + * @param password 密码 + * @param encryptType 密码类型 + * @return appId + */ + Long assertApp(String appName, String password, String encryptType); + + Long assertApp(AppInfoDO appInfo, String password, String encryptType); + + String fetchOriginAppPassword(AppInfoDO appInfo); } diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java index 6acea5bd..4087438c 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java @@ -1,14 +1,13 @@ package tech.powerjob.server.core.service; -import tech.powerjob.server.persistence.remote.model.UserInfoDO; -import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import com.google.common.base.Splitter; import com.google.common.collect.Lists; -import org.springframework.stereotype.Service; import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import javax.annotation.Resource; -import java.util.Date; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -25,16 +24,6 @@ public class UserService { @Resource private UserInfoRepository userInfoRepository; - /** - * 保存/修改 用户 - * @param userInfoDO user - */ - public void save(UserInfoDO userInfoDO) { - userInfoDO.setGmtCreate(new Date()); - userInfoDO.setGmtModified(userInfoDO.getGmtCreate()); - userInfoRepository.saveAndFlush(userInfoDO); - } - /** * 根据用户ID字符串获取用户信息详细列表 * @param userIds 逗号分割的用户ID信息 diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java index b5e96ed0..fc7dacfe 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/AppInfoServiceImpl.java @@ -1,13 +1,22 @@ package tech.powerjob.server.core.service.impl; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import tech.powerjob.common.enums.EncryptType; +import tech.powerjob.common.enums.ErrorCodes; import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.utils.DigestUtils; +import tech.powerjob.server.common.utils.AESUtil; import tech.powerjob.server.core.service.AppInfoService; import tech.powerjob.server.persistence.remote.model.AppInfoDO; import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; -import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * AppInfoServiceImpl @@ -15,25 +24,105 @@ import java.util.Objects; * @author tjq * @since 2023/3/4 */ +@Slf4j @Service @RequiredArgsConstructor public class AppInfoServiceImpl implements AppInfoService { + private final Cache appId2AppInfoDO = CacheBuilder.newBuilder() + .softValues() + .expireAfterWrite(3, TimeUnit.MINUTES) + .maximumSize(1024) + .build(); + private final AppInfoRepository appInfoRepository; - /** - * 验证应用访问权限 - * @param appName 应用名称 - * @param password 密码 - * @return 应用ID - */ - @Override - public Long assertApp(String appName, String password) { + private static final String ENCRYPT_KEY = "ChinaNo.1_ChinaNo.1_ChinaNo.1AAA"; - AppInfoDO appInfo = appInfoRepository.findByAppName(appName).orElseThrow(() -> new PowerJobException("can't find appInfo by appName: " + appName)); - if (Objects.equals(appInfo.getPassword(), password)) { - return appInfo.getId(); - } - throw new PowerJobException("password error!"); + private static final String ENCRYPT_PWD_PREFIX = "sys_encrypt_aes:"; + + @Override + public Optional findByAppName(String appName) { + return appInfoRepository.findByAppName(appName); } + + @Override + public Optional findById(Long appId, boolean useCache) { + if (!useCache) { + Optional appInfoOpt = appInfoRepository.findById(appId); + appInfoOpt.ifPresent(appInfo -> appId2AppInfoDO.put(appId, appInfo)); + return appInfoOpt; + } + try { + AppInfoDO appInfoDO = appId2AppInfoDO.get(appId, () -> { + Optional appInfoOpt = appInfoRepository.findById(appId); + if (appInfoOpt.isPresent()) { + return appInfoOpt.get(); + } + throw new IllegalArgumentException("can't find appInfo by appId:" + appId); + }); + return Optional.of(appInfoDO); + } catch (Exception e) { + log.warn("[AppInfoService] findByIdWithCache failed,appId={}", appId, e); + } + return Optional.empty(); + } + + @Override + public void deleteById(Long appId) { + appInfoRepository.deleteById(appId); + } + + @Override + public AppInfoDO save(AppInfoDO appInfo) { + + String originPassword = appInfo.getPassword(); + String encryptPassword = AESUtil.encrypt(originPassword, ENCRYPT_KEY); + String finalPassword = ENCRYPT_PWD_PREFIX.concat(encryptPassword); + appInfo.setPassword(finalPassword); + + return appInfoRepository.saveAndFlush(appInfo); + } + + @Override + public Long assertApp(String appName, String password, String encryptType) { + AppInfoDO appInfo = appInfoRepository.findByAppName(appName).orElseThrow(() -> new PowerJobException(ErrorCodes.INVALID_APP, appName)); + return assertApp(appInfo, password, encryptType); + } + + @Override + public Long assertApp(AppInfoDO appInfo, String password, String encryptType) { + boolean checkPass = checkPassword(appInfo, password, encryptType); + if (!checkPass) { + throw new PowerJobException(ErrorCodes.INCORRECT_PASSWORD, null); + } + return appInfo.getId(); + } + + private boolean checkPassword(AppInfoDO appInfo, String password, String encryptType) { + String originPwd = fetchOriginAppPassword(appInfo); + if (StringUtils.isEmpty(encryptType) || EncryptType.NONE.getCode().equalsIgnoreCase(encryptType)) { + return password.equals(originPwd); + } + if (EncryptType.MD5.getCode().equalsIgnoreCase(encryptType)) { + return password.equalsIgnoreCase(DigestUtils.md5(originPwd)); + } + throw new PowerJobException(ErrorCodes.INVALID_REQUEST, "unknown_encryptType:" + encryptType); + } + + @Override + public String fetchOriginAppPassword(AppInfoDO appInfo) { + String dbPwd = appInfo.getPassword(); + if (StringUtils.isEmpty(dbPwd)) { + return dbPwd; + } + + if (dbPwd.startsWith(ENCRYPT_PWD_PREFIX)) { + String encryptPassword = dbPwd.replaceFirst(ENCRYPT_PWD_PREFIX, StringUtils.EMPTY); + return AESUtil.decrypt(encryptPassword, ENCRYPT_KEY); + } + + return dbPwd; + } + } diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/job/JobServiceImpl.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/job/JobServiceImpl.java index 6ed9e9ce..550c7f68 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/job/JobServiceImpl.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/impl/job/JobServiceImpl.java @@ -18,7 +18,7 @@ import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.response.JobInfoDTO; import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.server.common.SJ; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.timewheel.holder.InstanceTimeWheelService; import tech.powerjob.server.core.DispatchService; import tech.powerjob.server.core.instance.InstanceService; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java index d583f69b..75568e76 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java @@ -1,7 +1,6 @@ package tech.powerjob.server.core.uid; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import tech.powerjob.server.remote.server.self.ServerInfoService; @@ -22,7 +21,7 @@ public class IdGenerateService { private static final int DATA_CENTER_ID = 0; public IdGenerateService(ServerInfoService serverInfoService) { - long id = serverInfoService.fetchServiceInfo().getId(); + long id = serverInfoService.fetchCurrentServerInfo().getId(); snowFlakeIdGenerator = new SnowFlakeIdGenerator(DATA_CENTER_ID, id); log.info("[IdGenerateService] initialize IdGenerateService successfully, ID:{}", id); } diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/JobNodeValidator.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/JobNodeValidator.java index 7765aec8..aedf5c33 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/JobNodeValidator.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/JobNodeValidator.java @@ -5,14 +5,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import tech.powerjob.common.enums.WorkflowNodeType; import tech.powerjob.common.exception.PowerJobException; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.core.workflow.algorithm.WorkflowDAG; import tech.powerjob.server.persistence.remote.model.JobInfoDO; import tech.powerjob.server.persistence.remote.model.WorkflowNodeInfoDO; import tech.powerjob.server.persistence.remote.repository.JobInfoRepository; -import javax.annotation.Resource; - /** * @author Echo009 * @since 2021/12/14 diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/NestedWorkflowNodeValidator.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/NestedWorkflowNodeValidator.java index 0c22fa1b..b257c80c 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/NestedWorkflowNodeValidator.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/validator/NestedWorkflowNodeValidator.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Component; import tech.powerjob.common.enums.WorkflowNodeType; import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.model.PEWorkflowDAG; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.core.workflow.algorithm.WorkflowDAG; import tech.powerjob.server.persistence.remote.model.WorkflowInfoDO; import tech.powerjob.server.persistence.remote.model.WorkflowNodeInfoDO; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceManager.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceManager.java index 2c6cdd78..1dde853f 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceManager.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceManager.java @@ -18,7 +18,7 @@ import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.model.PEWorkflowDAG; import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.common.utils.CommonUtils; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.utils.SpringUtils; import tech.powerjob.server.core.alarm.AlarmUtils; import tech.powerjob.server.core.helper.StatusMappingHelper; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceService.java index 047a706d..93c1d7c0 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowInstanceService.java @@ -12,7 +12,7 @@ import tech.powerjob.common.enums.WorkflowNodeType; import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.model.PEWorkflowDAG; import tech.powerjob.common.response.WorkflowInstanceInfoDTO; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.utils.SpringUtils; import tech.powerjob.server.core.instance.InstanceService; import tech.powerjob.server.core.lock.UseCacheLock; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowService.java index 3a86cfb5..f6e2c701 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/WorkflowService.java @@ -15,7 +15,7 @@ import tech.powerjob.common.model.PEWorkflowDAG; import tech.powerjob.common.request.http.SaveWorkflowNodeRequest; import tech.powerjob.common.request.http.SaveWorkflowRequest; import tech.powerjob.server.common.SJ; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.timewheel.holder.InstanceTimeWheelService; import tech.powerjob.server.core.scheduler.TimingStrategyService; import tech.powerjob.server.core.service.NodeValidateService; diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/hanlder/impl/NestedWorkflowNodeHandler.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/hanlder/impl/NestedWorkflowNodeHandler.java index b08413a6..12b1e5d4 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/hanlder/impl/NestedWorkflowNodeHandler.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/workflow/hanlder/impl/NestedWorkflowNodeHandler.java @@ -11,7 +11,7 @@ import tech.powerjob.common.enums.WorkflowNodeType; import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.model.PEWorkflowDAG; import tech.powerjob.common.utils.CommonUtils; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.utils.SpringUtils; import tech.powerjob.server.core.workflow.WorkflowInstanceManager; import tech.powerjob.server.core.workflow.algorithm.WorkflowDAGUtils; diff --git a/powerjob-server/powerjob-server-extension/pom.xml b/powerjob-server/powerjob-server-extension/pom.xml index ad4d772f..6b3686ae 100644 --- a/powerjob-server/powerjob-server-extension/pom.xml +++ b/powerjob-server/powerjob-server-extension/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-extension/src/main/java/tech/powerjob/server/extension/dfs/DFsService.java b/powerjob-server/powerjob-server-extension/src/main/java/tech/powerjob/server/extension/dfs/DFsService.java index fdb77b6b..7fe61f7c 100644 --- a/powerjob-server/powerjob-server-extension/src/main/java/tech/powerjob/server/extension/dfs/DFsService.java +++ b/powerjob-server/powerjob-server-extension/src/main/java/tech/powerjob/server/extension/dfs/DFsService.java @@ -16,14 +16,14 @@ public interface DFsService { * @param storeRequest 存储请求 * @throws IOException 异常 */ - void store(StoreRequest storeRequest) throws IOException; + void store(StoreRequest storeRequest) throws Exception; /** * 下载文件 * @param downloadRequest 文件下载请求 * @throws IOException 异常 */ - void download(DownloadRequest downloadRequest) throws IOException; + void download(DownloadRequest downloadRequest) throws Exception; /** * 获取文件元信息 @@ -31,7 +31,7 @@ public interface DFsService { * @return 存在则返回文件元信息 * @throws IOException 异常 */ - Optional fetchFileMeta(FileLocation fileLocation) throws IOException; + Optional fetchFileMeta(FileLocation fileLocation) throws Exception; /** * 清理 powerjob 认为“过期”的文件 diff --git a/powerjob-server/powerjob-server-migrate/pom.xml b/powerjob-server/powerjob-server-migrate/pom.xml index a8bf4916..724d851f 100644 --- a/powerjob-server/powerjob-server-migrate/pom.xml +++ b/powerjob-server/powerjob-server-migrate/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-monitor/pom.xml b/powerjob-server/powerjob-server-monitor/pom.xml index 012e8121..198abcd8 100644 --- a/powerjob-server/powerjob-server-monitor/pom.xml +++ b/powerjob-server/powerjob-server-monitor/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-persistence/pom.xml b/powerjob-server/powerjob-server-persistence/pom.xml index 5956e0ee..d3f78c11 100644 --- a/powerjob-server/powerjob-server-persistence/pom.xml +++ b/powerjob-server/powerjob-server-persistence/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java index 31d0bbbf..3f4fe41d 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java @@ -86,7 +86,7 @@ public class QueryConvertUtils { }; } - private static String convertLikeParams(Object o) { + public static String convertLikeParams(Object o) { String s = (String) o; if (!s.startsWith("%")) { s = "%" + s; diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/config/dialect/AdpPostgreSQLDialect.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/config/dialect/AdpPostgreSQLDialect.java new file mode 100644 index 00000000..8552c674 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/config/dialect/AdpPostgreSQLDialect.java @@ -0,0 +1,37 @@ +package tech.powerjob.server.persistence.config.dialect; + +import org.hibernate.dialect.PostgreSQL10Dialect; +import org.hibernate.type.descriptor.sql.LongVarbinaryTypeDescriptor; +import org.hibernate.type.descriptor.sql.LongVarcharTypeDescriptor; +import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; + +import java.sql.Types; + +/** + * PG数据库方言 + * 使用方自行通过配置文件激活:spring.datasource.remote.hibernate.properties.hibernate.dialect=tech.powerjob.server.persistence.config.dialect.AdpPostgreSQLDialect + * + * @author litong0531 + * @since 2024/8/11 + */ +public class AdpPostgreSQLDialect extends PostgreSQL10Dialect { + + public AdpPostgreSQLDialect() { + super(); + registerColumnType(Types.BLOB, "bytea"); + registerColumnType(Types.CLOB, "text"); + } + + @Override + public SqlTypeDescriptor remapSqlTypeDescriptor(SqlTypeDescriptor sqlTypeDescriptor) { + switch (sqlTypeDescriptor.getSqlType()) { + case Types.CLOB: + return LongVarcharTypeDescriptor.INSTANCE; + case Types.BLOB: + return LongVarbinaryTypeDescriptor.INSTANCE; + case Types.NCLOB: + return LongVarbinaryTypeDescriptor.INSTANCE; + } + return super.remapSqlTypeDescriptor(sqlTypeDescriptor); + } +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java index 7395af38..fe073946 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java @@ -22,7 +22,14 @@ public class AppInfoDO { @GenericGenerator(name = "native", strategy = "native") private Long id; + private String appName; + + /** + * 描述 + */ + private String title; + /** * 应用分组密码 */ @@ -35,7 +42,24 @@ public class AppInfoDO { */ private String currentServer; + /** + * 命名空间ID,外键关联 + */ + private Long namespaceId; + /** + * 管理标签 + */ + private String tags; + /** + * 扩展字段 + */ + private String extra; + private Date gmtCreate; private Date gmtModified; + + private Long creator; + + private Long modifier; } diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/NamespaceDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/NamespaceDO.java new file mode 100644 index 00000000..35d01bf8 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/NamespaceDO.java @@ -0,0 +1,65 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * 命名空间,用于组织管理 App + * + * @author tjq + * @since 2023/9/3 + */ +@Data +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(name = "uidx01_namespace", columnNames = {"code"})}) +public class NamespaceDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + /** + * 空间唯一标识 + */ + private String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + private String name; + + /** + * 鉴权 token,后续 OpenAPI 调用需要 + */ + private String token; + + private Integer status; + + /** + * 部门,组织架构相关属性。 + * 预留数据库字段方便基于组织架构二次开发 + */ + private String dept; + + /** + * 标签,扩展性之王,多值逗号分割 + */ + private String tags; + + /** + * 扩展字段 + */ + private String extra; + + private Date gmtCreate; + + private Date gmtModified; + + private Long creator; + + private Long modifier; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/PwjbUserInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/PwjbUserInfoDO.java new file mode 100644 index 00000000..81418271 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/PwjbUserInfoDO.java @@ -0,0 +1,36 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * PowerJob 自建登录体系的用户表,只存储使用 PowerJob 自带登录方式登录的用户信息 + * + * @author tjq + * @since 2024/2/13 + */ +@Data +@Entity +@Table(uniqueConstraints = { + @UniqueConstraint(name = "uidx01_username", columnNames = {"username"}) +}) +public class PwjbUserInfoDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + private String username; + + private String password; + + private String extra; + + private Date gmtCreate; + + private Date gmtModified; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java new file mode 100644 index 00000000..b72cd64f --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java @@ -0,0 +1,49 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * 杂项 + * KKV 表存一些配置数据 + * + * @author tjq + * @since 2024/2/15 + */ +@Data +@Entity +@NoArgsConstructor +@Table(uniqueConstraints = {@UniqueConstraint(name = "uidx01_sundry", columnNames = {"pkey", "skey"})}) +public class SundryDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + /** + * PKEY + */ + private String pkey; + /** + * SKEY + */ + private String skey; + /** + * 内容 + */ + private String content; + + /** + * 其他参数 + */ + private String extra; + + private Date gmtCreate; + + private Date gmtModified; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java index e19bef8c..2c3ef4d6 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java @@ -8,16 +8,20 @@ import java.util.Date; /** * 用户信息表 + * PowerJob 自身维护的全部用户体系数据 + * 5.0.0 可能不兼容改动:为了支持第三方登录,需要通过 username 与第三方登录系统做匹配,该列需要声明为唯一索引,确保全局唯一 * * @author tjq * @since 2020/4/12 */ @Data @Entity -@Table(indexes = { - @Index(name = "uidx01_user_info", columnList = "username"), - @Index(name = "uidx02_user_info", columnList = "email") -}) +@Table(uniqueConstraints = { + @UniqueConstraint(name = "uidx01_user_name", columnNames = {"username"}) +}, + indexes = { + @Index(name = "uidx02_user_info", columnList = "email") + }) public class UserInfoDO { @Id @@ -25,7 +29,17 @@ public class UserInfoDO { @GenericGenerator(name = "native", strategy = "native") private Long id; + /** + * 账号类型 + */ + private String accountType; + private String username; + /** + * since 5.0.0 + * 昵称(第三方登陆的 username 很难识别,方便后续展示引入 nick) + */ + private String nick; private String password; /** @@ -40,11 +54,28 @@ public class UserInfoDO { * webHook */ private String webHook; + /** - * 扩展字段 + * JWT 登录的二次校验信息 + */ + private String tokenLoginVerifyInfo; + + /** + * 扩展字段 for 第三方 + * PowerJob 内部不允许使用该字段 */ private String extra; + /** + * 原始账号 username + */ + private String originUsername; + + /** + * 账号当前状态 + */ + private Integer status; + private Date gmtCreate; private Date gmtModified; diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java new file mode 100644 index 00000000..22d7b629 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java @@ -0,0 +1,53 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * 用户角色表 + * + * @author tjq + * @since 2023/3/20 + */ +@Data +@Entity +@Table(indexes = { + @Index(name = "uidx01_user_id", columnList = "userId") +}) +public class UserRoleDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + /** + * 授予角色的用户ID + */ + private Long userId; + + /** + * 权限范围,namespace 还是 app + */ + private Integer scope; + /** + * 和 scope 一起组成授权目标,比如某个 app 或 某个 namespace + */ + private Long target; + + /** + * 角色,比如 Observer + */ + private Integer role; + /** + * 扩展字段 + */ + private String extra; + + private Date gmtCreate; + + private Date gmtModified; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java index a7e6d09d..ec98e8e9 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java @@ -1,12 +1,14 @@ package tech.powerjob.server.persistence.remote.repository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import tech.powerjob.server.persistence.remote.model.AppInfoDO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -16,7 +18,7 @@ import java.util.Optional; * @author tjq * @since 2020/4/1 */ -public interface AppInfoRepository extends JpaRepository { +public interface AppInfoRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByAppName(String appName); @@ -31,4 +33,8 @@ public interface AppInfoRepository extends JpaRepository { @Query(value = "select id from AppInfoDO where currentServer = :currentServer") List listAppIdByCurrentServer(@Param("currentServer")String currentServer); + List findAllByNamespaceId(Long namespaceId); + + + List findAllByIdIn(Collection ids); } diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/NamespaceRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/NamespaceRepository.java new file mode 100644 index 00000000..ff776903 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/NamespaceRepository.java @@ -0,0 +1,22 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * 命名空间 + * + * @author tjq + * @since 2023/9/3 + */ +public interface NamespaceRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByCode(String code); + + List findAllByIdIn(Collection ids); +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/PwjbUserInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/PwjbUserInfoRepository.java new file mode 100644 index 00000000..68c02bb0 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/PwjbUserInfoRepository.java @@ -0,0 +1,17 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; + +import java.util.Optional; + +/** + * PwjbUserInfoRepository + * + * @author tjq + * @since 2024/2/13 + */ +public interface PwjbUserInfoRepository extends JpaRepository { + + Optional findByUsername(String username); +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java new file mode 100644 index 00000000..62e47a5a --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java @@ -0,0 +1,20 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import tech.powerjob.server.persistence.remote.model.SundryDO; + +import java.util.List; +import java.util.Optional; + +/** + * SundryRepository + * + * @author tjq + * @since 2024/2/15 + */ +public interface SundryRepository extends JpaRepository { + + List findAllByPkey(String pkey); + + Optional findByPkeyAndSkey(String pkey, String skey); +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java index 2a11d207..60a2ae1b 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java @@ -1,9 +1,11 @@ package tech.powerjob.server.persistence.remote.repository; -import tech.powerjob.server.persistence.remote.model.UserInfoDO; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; import java.util.List; +import java.util.Optional; /** * 用户信息表数据库访问层 @@ -11,7 +13,9 @@ import java.util.List; * @author tjq * @since 2020/4/12 */ -public interface UserInfoRepository extends JpaRepository { +public interface UserInfoRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByUsername(String username); List findByUsernameLike(String username); diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java new file mode 100644 index 00000000..fb4c69cf --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java @@ -0,0 +1,23 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import tech.powerjob.server.persistence.remote.model.UserRoleDO; + +import java.util.List; + +/** + * UserRoleRepository + * + * @author tjq + * @since 2023/3/20 + */ +public interface UserRoleRepository extends JpaRepository { + + List findAllByUserId(Long userId); + + List findAllByScopeAndTarget(Integer scope, Long target); + + List findAllByScopeAndTargetAndRoleAndUserId(Integer scope, Long target, Integer role, Long userId); + + List findAllByUserIdAndScope(Long userId, Integer scope); +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/StorageConfiguration.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/StorageConfiguration.java index 890fbeba..245cdb73 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/StorageConfiguration.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/StorageConfiguration.java @@ -27,6 +27,12 @@ public class StorageConfiguration { return new MySqlSeriesDfsService(); } + @Bean + @Conditional(PostgresqlSeriesDfsService.PostgresqlSeriesCondition.class) + public DFsService initPGDbFs() { + return new PostgresqlSeriesDfsService(); + } + @Bean @Conditional(AliOssService.AliOssCondition.class) public DFsService initAliOssFs() { diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/impl/MySqlSeriesDfsService.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/impl/MySqlSeriesDfsService.java index fb79b3e7..fe138ebb 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/impl/MySqlSeriesDfsService.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/impl/MySqlSeriesDfsService.java @@ -17,8 +17,7 @@ import org.springframework.context.annotation.Conditional; import org.springframework.core.env.Environment; import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.common.utils.CommonUtils; -import tech.powerjob.common.utils.NetUtils; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.spring.condition.PropertyAndOneBeanCondition; import tech.powerjob.server.extension.dfs.*; import tech.powerjob.server.persistence.storage.AbstractDFsService; diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/impl/PostgresqlSeriesDfsService.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/impl/PostgresqlSeriesDfsService.java new file mode 100644 index 00000000..95644ff9 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/storage/impl/PostgresqlSeriesDfsService.java @@ -0,0 +1,407 @@ +package tech.powerjob.server.persistence.storage.impl; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.env.Environment; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.common.enums.SwitchableStatus; +import tech.powerjob.server.common.spring.condition.PropertyAndOneBeanCondition; +import tech.powerjob.server.extension.dfs.DFsService; +import tech.powerjob.server.extension.dfs.DownloadRequest; +import tech.powerjob.server.extension.dfs.FileLocation; +import tech.powerjob.server.extension.dfs.FileMeta; +import tech.powerjob.server.extension.dfs.StoreRequest; +import tech.powerjob.server.persistence.storage.AbstractDFsService; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Blob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Priority; +import javax.sql.DataSource; + +/** + * postgresql 数据库存储,使用的版本是14 + * ********************* 配置项 ********************* + * oms.storage.dfs.postgresql_series.driver + * oms.storage.dfs.postgresql_series.url + * oms.storage.dfs.postgresql_series.username + * oms.storage.dfs.postgresql_series.password + * oms.storage.dfs.postgresql_series.auto_create_table + * oms.storage.dfs.postgresql_series.table_name + * + * @author jetol + * @since 2024-1-8 + */ +@Slf4j +@Priority(value = Integer.MAX_VALUE - 4) +@Conditional(PostgresqlSeriesDfsService.PostgresqlSeriesCondition.class) +public class PostgresqlSeriesDfsService extends AbstractDFsService { + + private DataSource dataSource; + + private static final String TYPE_POSTGRESQL = "postgresql_series"; + + /** + * 数据库驱动,Postgresql 为 org.postgresql.Driver + */ + private static final String KEY_DRIVER_NAME = "driver"; + /** + * 数据库地址,比如 jdbc:postgresql://localhost:3306/powerjob-daily + */ + private static final String KEY_URL = "url"; + /** + * 数据库账号,比如 root + */ + private static final String KEY_USERNAME = "username"; + /** + * 数据库密码 + */ + private static final String KEY_PASSWORD = "password"; + /** + * 是否自动建表 + */ + private static final String KEY_AUTO_CREATE_TABLE = "auto_create_table"; + /** + * 表名 + */ + private static final String KEY_TABLE_NAME = "table_name"; + + /* ********************* SQL region ********************* */ + + private static final String DEFAULT_TABLE_NAME = "powerjob_files"; + + private static final String POWERJOB_FILES_ID_SEQ = "CREATE SEQUENCE powerjob_files_id_seq\n" + + " START WITH 1\n" + + " INCREMENT BY 1\n" + + " NO MINVALUE\n" + + " NO MAXVALUE\n" + + " CACHE 1;" ; + private static final String CREATE_TABLE_SQL = "CREATE TABLE powerjob_files (\n" + + " id bigint NOT NULL DEFAULT nextval('powerjob_files_id_seq') PRIMARY KEY,\n" + + " bucket varchar(255) NOT NULL,\n" + + " name varchar(255) NOT NULL,\n" + + " version varchar(255) NOT NULL,\n" + + " meta varchar(255) NULL DEFAULT NULL,\n" + + " length bigint NOT NULL,\n" + + " status int NOT NULL,\n" + + " data bytea NOT NULL,\n" + + " extra varchar(255) NULL DEFAULT NULL,\n" + + " gmt_create timestamp without time zone NOT NULL,\n" + + " gmt_modified timestamp without time zone NULL DEFAULT NULL\n" + + ");"; + + private static final String INSERT_SQL = "insert into %s(bucket, name, version, meta, length, status, data, extra, gmt_create, gmt_modified) values (?,?,?,?,?,?,?,?,?,?);"; + + private static final String DELETE_SQL = "DELETE FROM %s "; + + private static final String QUERY_FULL_SQL = "select * from %s"; + + private static final String QUERY_META_SQL = "select bucket, name, version, meta, length, status, extra, gmt_create, gmt_modified from %s"; + + + private void deleteByLocation(FileLocation fileLocation) { + String dSQLPrefix = fullSQL(DELETE_SQL); + String dSQL = dSQLPrefix.concat(whereSQL(fileLocation)); + executeDelete(dSQL); + } + + private void executeDelete(String sql) { + try (Connection con = dataSource.getConnection()) { + con.createStatement().executeUpdate(sql); + } catch (Exception e) { + log.error("[PostgresqlSeriesDfsService] executeDelete failed, sql: {}", sql); + } + } + + @Override + public void store(StoreRequest storeRequest) throws IOException, SQLException { + + Stopwatch sw = Stopwatch.createStarted(); + String insertSQL = fullSQL(INSERT_SQL); + + FileLocation fileLocation = storeRequest.getFileLocation(); + + // 覆盖写,写之前先删除 + deleteByLocation(fileLocation); + + Map meta = Maps.newHashMap(); + meta.put("_server_", serverInfo.getIp()); + meta.put("_local_file_path_", storeRequest.getLocalFile().getAbsolutePath()); + BufferedInputStream bufferedInputStream = new BufferedInputStream(Files.newInputStream(storeRequest.getLocalFile().toPath())); + + Date date = new Date(System.currentTimeMillis()); + + Connection con =null; + PreparedStatement pst =null; + try { + con = dataSource.getConnection(); + //pg库提示报错:org.postgresql.util.PSQLException: Large Objects may not be used in auto-commit mode. + con.setAutoCommit(false); + log.info("[PostgresqlSeriesDfsService] set autocommit false."); + + pst = con.prepareStatement(insertSQL); + + pst.setString(1, fileLocation.getBucket()); + pst.setString(2, fileLocation.getName()); + pst.setString(3, "mu"); + pst.setString(4, JsonUtils.toJSONString(meta)); + pst.setLong(5, storeRequest.getLocalFile().length()); + pst.setInt(6, SwitchableStatus.ENABLE.getV()); + //PreparedStatement类并没有提供setBlob方法来直接设置BYTEA类型字段,因为PostgreSQL不支持JDBC中的java.sql.Blob接口 +// pst.setBlob(7, bufferedInputStream);org.postgresql.util.PSQLException: ERROR: column "data" is of type bytea but expression is of type bigint + pst.setBytes(7, bufferedInputStreamToByteArray(bufferedInputStream)); + pst.setString(8, null); + pst.setDate(9, date); + pst.setDate(10, date); + + pst.execute(); + con.commit(); + log.info("[PostgresqlSeriesDfsService] store [{}] successfully, cost: {}", fileLocation, sw); + + } catch (Exception e) { + if(con != null){ + con.rollback(); + } + log.error("[PostgresqlSeriesDfsService] store [{}] failed!", fileLocation); + ExceptionUtils.rethrow(e); + }finally { + if(con != null){ + //设置回来,恢复自动提交模式 + con.setAutoCommit(true); + log.info("[PostgresqlSeriesDfsService] set autocommit true."); + con.close(); + } + if(null != pst){ + pst.close(); + } + bufferedInputStream.close(); + } + } + + /** + * 上面已经有异常处理,这里直接往上抛 + * @param bis + * @return + * @throws IOException + */ + public static byte[] bufferedInputStreamToByteArray(BufferedInputStream bis) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if(null == bis ){ + return null; + } + // 创建缓冲区 + byte[] buffer = new byte[1024]; + int read; + // 读取流中的数据并写入到ByteArrayOutputStream + while ((read = bis.read(buffer)) != -1) { + baos.write(buffer, 0, read); + } + // 关闭输入流 + bis.close(); + // 转换为字节数组并返回 + return baos.toByteArray(); + } + + @Override + public void download(DownloadRequest downloadRequest) throws IOException { + + Stopwatch sw = Stopwatch.createStarted(); + String querySQL = fullSQL(QUERY_FULL_SQL); + + FileLocation fileLocation = downloadRequest.getFileLocation(); + + FileUtils.forceMkdirParent(downloadRequest.getTarget()); + + try (Connection con = dataSource.getConnection()) { + + ResultSet resultSet = con.createStatement().executeQuery(querySQL.concat(whereSQL(fileLocation))); + + boolean exist = resultSet.next(); + + if (!exist) { + log.warn("[PostgresqlSeriesDfsService] download file[{}] failed due to not exits!", fileLocation); + return; + } + + Blob dataBlob = resultSet.getBlob("data"); + FileUtils.copyInputStreamToFile(new BufferedInputStream(dataBlob.getBinaryStream()), downloadRequest.getTarget()); + + log.info("[PostgresqlSeriesDfsService] download [{}] successfully, cost: {}", fileLocation, sw); + + } catch (Exception e) { + log.error("[PostgresqlSeriesDfsService] download file [{}] failed!", fileLocation, e); + ExceptionUtils.rethrow(e); + } + + } + + @Override + public Optional fetchFileMeta(FileLocation fileLocation) { + + String querySQL = fullSQL(QUERY_META_SQL); + + try (Connection con = dataSource.getConnection()) { + + ResultSet resultSet = con.createStatement().executeQuery(querySQL.concat(whereSQL(fileLocation))); + + boolean exist = resultSet.next(); + + if (!exist) { + return Optional.empty(); + } + + FileMeta fileMeta = new FileMeta() + .setLength(resultSet.getLong("length")) + .setLastModifiedTime(resultSet.getDate("gmt_modified")) + .setMetaInfo(JsonUtils.parseMap(resultSet.getString("meta"))); + return Optional.of(fileMeta); + + } catch (Exception e) { + log.error("[PostgresqlSeriesDfsService] fetchFileMeta [{}] failed!", fileLocation); + ExceptionUtils.rethrow(e); + } + + return Optional.empty(); + } + + @Override + public void cleanExpiredFiles(String bucket, int days) { + + // 虽然官方提供了服务端删除的能力,依然强烈建议用户直接在数据库层面配置清理事件!!! + + String dSQLPrefix = fullSQL(DELETE_SQL); + final long targetTs = DateUtils.addDays(new Date(System.currentTimeMillis()), -days).getTime(); + final String targetDeleteTime = CommonUtils.formatTime(targetTs); + log.info("[PostgresqlSeriesDfsService] start to cleanExpiredFiles, targetDeleteTime: {}", targetDeleteTime); + String fSQL = dSQLPrefix.concat(String.format(" where gmt_modified < '%s'", targetDeleteTime)); + log.info("[PostgresqlSeriesDfsService] cleanExpiredFiles SQL: {}", fSQL); + executeDelete(fSQL); + } + + @Override + protected void init(ApplicationContext applicationContext) { + + Environment env = applicationContext.getEnvironment(); + + PostgresqlProperty postgresqlProperty = new PostgresqlProperty() + .setDriver(fetchProperty(env, TYPE_POSTGRESQL, KEY_DRIVER_NAME)) + .setUrl(fetchProperty(env, TYPE_POSTGRESQL, KEY_URL)) + .setUsername(fetchProperty(env, TYPE_POSTGRESQL, KEY_USERNAME)) + .setPassword(fetchProperty(env, TYPE_POSTGRESQL, KEY_PASSWORD)) + .setAutoCreateTable(Boolean.TRUE.toString().equalsIgnoreCase(fetchProperty(env, TYPE_POSTGRESQL, KEY_AUTO_CREATE_TABLE))) + ; + + try { + initDatabase(postgresqlProperty); + initTable(postgresqlProperty); + } catch (Exception e) { + log.error("[PostgresqlSeriesDfsService] init datasource failed!", e); + ExceptionUtils.rethrow(e); + } + + log.info("[PostgresqlSeriesDfsService] initialize successfully, THIS_WILL_BE_THE_STORAGE_LAYER."); + } + + void initDatabase(PostgresqlProperty property) { + + log.info("[PostgresqlSeriesDfsService] init datasource by config: {}", property); + + HikariConfig config = new HikariConfig(); + + config.setDriverClassName(property.driver); + config.setJdbcUrl(property.url); + config.setUsername(property.username); + config.setPassword(property.password); + + config.setAutoCommit(true); + // 池中最小空闲连接数量 + config.setMinimumIdle(2); + // 池中最大连接数量 + config.setMaximumPoolSize(32); + + dataSource = new HikariDataSource(config); + } + + void initTable(PostgresqlProperty property) throws Exception { + + if (property.autoCreateTable) { + + String powerjobFilesIdSeq = fullSQL(POWERJOB_FILES_ID_SEQ); + String createTableSQL = fullSQL(CREATE_TABLE_SQL); + + log.info("[PostgresqlSeriesDfsService] use create table SQL: {}", createTableSQL); + try (Connection connection = dataSource.getConnection()) { + connection.createStatement().execute(powerjobFilesIdSeq); + connection.createStatement().execute(createTableSQL); + log.info("[PostgresqlSeriesDfsService] auto create table successfully!"); + } + } + } + + private String fullSQL(String sql) { + return String.format(sql, parseTableName()); + } + + private String parseTableName() { + // 误删,兼容本地 unit test + if (applicationContext == null) { + return DEFAULT_TABLE_NAME; + } + String tableName = fetchProperty(applicationContext.getEnvironment(), TYPE_POSTGRESQL, KEY_TABLE_NAME); + return StringUtils.isEmpty(tableName) ? DEFAULT_TABLE_NAME : tableName; + } + + private static String whereSQL(FileLocation fileLocation) { + return String.format(" where bucket='%s' AND name='%s' ", fileLocation.getBucket(), fileLocation.getName()); + } + + @Override + public void destroy() throws Exception { + } + + @Data + @Accessors(chain = true) + static class PostgresqlProperty { + private String driver; + private String url; + private String username; + private String password; + + private boolean autoCreateTable; + } + + public static class PostgresqlSeriesCondition extends PropertyAndOneBeanCondition { + @Override + protected List anyConfigKey() { + return Lists.newArrayList("oms.storage.dfs.postgresql_series.url"); + } + + @Override + protected Class beanType() { + return DFsService.class; + } + } +} diff --git a/powerjob-server/powerjob-server-remote/pom.xml b/powerjob-server/powerjob-server-remote/pom.xml index 5254a96e..74c10daf 100644 --- a/powerjob-server/powerjob-server-remote/pom.xml +++ b/powerjob-server/powerjob-server-remote/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/election/ServerElectionService.java b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/election/ServerElectionService.java index e6febdfe..d7ece94d 100644 --- a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/election/ServerElectionService.java +++ b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/election/ServerElectionService.java @@ -6,7 +6,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import tech.powerjob.common.enums.Protocol; import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.request.ServerDiscoveryRequest; import tech.powerjob.common.response.AskResponse; @@ -16,8 +15,8 @@ import tech.powerjob.server.extension.LockService; import tech.powerjob.server.persistence.remote.model.AppInfoDO; import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; import tech.powerjob.server.remote.transporter.ProtocolInfo; -import tech.powerjob.server.remote.transporter.impl.ServerURLFactory; import tech.powerjob.server.remote.transporter.TransportService; +import tech.powerjob.server.remote.transporter.impl.ServerURLFactory; import java.util.Date; import java.util.Optional; @@ -150,7 +149,7 @@ public class ServerElectionService { URL targetUrl = ServerURLFactory.ping2Friend(serverAddress); try { - AskResponse response = transportService.ask(Protocol.HTTP.name(), targetUrl, ping, AskResponse.class) + AskResponse response = transportService.ask(transportService.defaultProtocol().getProtocol(), targetUrl, ping, AskResponse.class) .toCompletableFuture() .get(PING_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (response.isSuccess()) { diff --git a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/redirector/DesignateServerAspect.java b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/redirector/DesignateServerAspect.java index ad906487..b2563519 100644 --- a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/redirector/DesignateServerAspect.java +++ b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/redirector/DesignateServerAspect.java @@ -14,14 +14,13 @@ import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import tech.powerjob.common.RemoteConstant; -import tech.powerjob.common.enums.Protocol; import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.response.AskResponse; import tech.powerjob.remote.framework.base.URL; import tech.powerjob.server.persistence.remote.model.AppInfoDO; import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; -import tech.powerjob.server.remote.transporter.impl.ServerURLFactory; import tech.powerjob.server.remote.transporter.TransportService; +import tech.powerjob.server.remote.transporter.impl.ServerURLFactory; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -100,7 +99,7 @@ public class DesignateServerAspect { final URL friendUrl = ServerURLFactory.process2Friend(targetServer); - CompletionStage askCS = transportService.ask(Protocol.HTTP.name(), friendUrl, remoteProcessReq, AskResponse.class); + CompletionStage askCS = transportService.ask(transportService.defaultProtocol().getProtocol(), friendUrl, remoteProcessReq, AskResponse.class); AskResponse askResponse = askCS.toCompletableFuture().get(RemoteConstant.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (!askResponse.isSuccess()) { diff --git a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java index 5c799a7a..1e119ec6 100644 --- a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java +++ b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java @@ -14,6 +14,13 @@ public interface ServerInfoService { * fetch current server info * @return ServerInfo */ - ServerInfo fetchServiceInfo(); + ServerInfo fetchCurrentServerInfo(); + + /** + * fetch schedule server info + * @param appId appId + * @return ServerInfo + */ + ServerInfo fetchAppServerInfo(Long appId); } diff --git a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java index 8a205e49..21af89c8 100644 --- a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java +++ b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java @@ -15,6 +15,7 @@ import tech.powerjob.server.common.module.ServerInfo; import tech.powerjob.server.extension.LockService; import tech.powerjob.server.persistence.remote.model.ServerInfoDO; import tech.powerjob.server.persistence.remote.repository.ServerInfoRepository; +import tech.powerjob.server.remote.server.redirector.DesignateServer; import java.util.Date; import java.util.List; @@ -138,7 +139,13 @@ public class ServerInfoServiceImpl implements ServerInfoService { } @Override - public ServerInfo fetchServiceInfo() { + public ServerInfo fetchCurrentServerInfo() { + return serverInfo; + } + + @Override + @DesignateServer + public ServerInfo fetchAppServerInfo(Long appId) { return serverInfo; } } diff --git a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/transporter/impl/PowerTransportService.java b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/transporter/impl/PowerTransportService.java index 331a83ad..53748862 100644 --- a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/transporter/impl/PowerTransportService.java +++ b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/transporter/impl/PowerTransportService.java @@ -104,23 +104,35 @@ public class PowerTransportService implements TransportService, InitializingBean // 从构造器注入改为从 applicationContext 获取来避免循环依赖 final Map beansWithAnnotation = applicationContext.getBeansWithAnnotation(Actor.class); - log.info("[PowerTransportService] find Actor num={},names={}", beansWithAnnotation.size(), beansWithAnnotation.keySet()); + log.info("[PowerTransportService] [{}] find Actor num={},names={}", protocol, beansWithAnnotation.size(), beansWithAnnotation.keySet()); Address address = new Address() .setHost(NetUtils.getLocalHost()) .setPort(port); + + ProtocolInfo protocolInfo = new ProtocolInfo(protocol, address.getHost(), address.getPort(), null); + EngineConfig engineConfig = new EngineConfig() .setServerType(ServerType.SERVER) .setType(protocol.toUpperCase()) .setBindAddress(address) .setActorList(Lists.newArrayList(beansWithAnnotation.values())); - log.info("[PowerTransportService] start to initialize RemoteEngine[type={},address={}]", protocol, address); + + if (!StringUtils.equalsIgnoreCase(protocolInfo.getExternalAddress(), protocolInfo.getAddress())) { + Address externalAddress = Address.fromIpv4(protocolInfo.getExternalAddress()); + engineConfig.setExternalAddress(externalAddress); + log.info("[PowerTransportService] [{}] exist externalAddress: {}", protocol, externalAddress); + } + + log.info("[PowerTransportService] [{}] start to initialize RemoteEngine[address={}]", protocol, address); RemoteEngine re = new PowerJobRemoteEngine(); final EngineOutput engineOutput = re.start(engineConfig); - log.info("[PowerTransportService] start RemoteEngine[type={},address={}] successfully", protocol, address); + log.info("[PowerTransportService] [{}] start RemoteEngine[address={}] successfully", protocol, address); this.engines.add(re); - this.protocolName2Info.put(protocol, new ProtocolInfo(protocol, address.getHost(), address.getPort(), engineOutput.getTransporter())); + + protocolInfo.setTransporter(engineOutput.getTransporter()); + this.protocolName2Info.put(protocol, protocolInfo); } @Override diff --git a/powerjob-server/powerjob-server-starter/pom.xml b/powerjob-server/powerjob-server-starter/pom.xml index b2d32bd1..8a9dd15a 100644 --- a/powerjob-server/powerjob-server-starter/pom.xml +++ b/powerjob-server/powerjob-server-starter/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.1.0 ../pom.xml 4.0.0 @@ -43,10 +43,19 @@ tech.powerjob powerjob-server-core + + tech.powerjob + powerjob-server-auth + tech.powerjob powerjob-server-migrate + + + tech.powerjob + powerjob-client + diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/ModifyOrCreateDynamicPermission.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/ModifyOrCreateDynamicPermission.java new file mode 100644 index 00000000..44962eaa --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/ModifyOrCreateDynamicPermission.java @@ -0,0 +1,46 @@ +package tech.powerjob.server.auth.plugin; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StreamUtils; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.interceptor.DynamicPermissionPlugin; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 针对 namespace 和 app 两大鉴权纬度,创建不需要任何权限,但任何修改操作都需要 WRITE 权限 + * 创建不需要权限,修改需要校验权限 + * + * @author tjq + * @since 2023/9/3 + */ +@Slf4j +public class ModifyOrCreateDynamicPermission implements DynamicPermissionPlugin { + @Override + public Permission calculate(HttpServletRequest request, Object handler) { + + try { + //获取请求body + byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream()); + String body = new String(bodyBytes, request.getCharacterEncoding()); + + Map inputParams = JsonUtils.parseMap(body); + + Object id = inputParams.get("id"); + + // 创建,不需要权限 + if (id == null) { + return Permission.NONE; + } + + return Permission.WRITE; + } catch (Exception e) { + log.error("[ModifyOrCreateDynamicPermission] check permission failed, please fix the bug!!!", e); + } + + // 异常情况先放行,不影响功能使用,后续修复 BUG + return Permission.NONE; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveAppGrantPermissionPlugin.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveAppGrantPermissionPlugin.java new file mode 100644 index 00000000..4bffe6fd --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveAppGrantPermissionPlugin.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.plugin; + +import tech.powerjob.server.auth.RoleScope; + +/** + * desc + * + * @author tjq + * @since 2024/2/11 + */ +public class SaveAppGrantPermissionPlugin extends SaveGrantPermissionPlugin { + @Override + protected RoleScope fetchRuleScope() { + return RoleScope.APP; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java new file mode 100644 index 00000000..766d6854 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java @@ -0,0 +1,76 @@ +package tech.powerjob.server.auth.plugin; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.GrantPermissionPlugin; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.common.utils.SpringUtils; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * WEB 类保存&修改一体型请求-授权插件 + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +public abstract class SaveGrantPermissionPlugin implements GrantPermissionPlugin { + + private static final String KEY_ID = "id"; + + @Override + public void grant(Object[] args, Object result, Method method, Object originBean) { + + if (args == null || args.length != 1) { + throw new IllegalArgumentException("[GrantPermission] args not match, maybe there has some bug"); + } + + // 理论上不可能,前置已完成判断 + PowerJobUser powerJobUser = LoginUserHolder.get(); + if (powerJobUser == null) { + throw new IllegalArgumentException("[GrantPermission] user not login, can't grant permission"); + } + + // 解析ID,非空代表更新,不授权 + Map saveRequest = JsonUtils.parseMap(JsonUtils.toJSONString(args[0])); + Long id = MapUtils.getLong(saveRequest, KEY_ID); + if (id != null) { + return; + } + + if (!(result instanceof ResultDTO)) { + throw new IllegalArgumentException("[GrantPermission] result not instanceof ResultDTO, maybe there has some bug"); + } + + ResultDTO resultDTO = (ResultDTO) result; + + if (!resultDTO.isSuccess()) { + log.warn("[GrantPermission] result not success, skip grant permission!"); + return; + } + + Map saveResult = JsonUtils.parseMap(JsonUtils.toJSONString(resultDTO.getData())); + Long savedId = MapUtils.getLong(saveResult, KEY_ID); + if (savedId == null) { + throw new IllegalArgumentException("[GrantPermission] result success but id not exits, maybe there has some bug, please fix it!!!"); + } + + PowerJobPermissionService powerJobPermissionService = SpringUtils.getBean(PowerJobPermissionService.class); + + Map extra = Maps.newHashMap(); + extra.put("source", "SaveGrantPermissionPlugin"); + + powerJobPermissionService.grantRole(fetchRuleScope(), savedId, powerJobUser.getId(), Role.ADMIN, JsonUtils.toJSONString(extra)); + } + + protected abstract RoleScope fetchRuleScope(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveNamespaceGrantPermissionPlugin.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveNamespaceGrantPermissionPlugin.java new file mode 100644 index 00000000..955ded5a --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveNamespaceGrantPermissionPlugin.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.plugin; + +import tech.powerjob.server.auth.RoleScope; + +/** + * namespace 授权插件 + * + * @author tjq + * @since 2024/2/11 + */ +public class SaveNamespaceGrantPermissionPlugin extends SaveGrantPermissionPlugin { + @Override + protected RoleScope fetchRuleScope() { + return RoleScope.NAMESPACE; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/WebAuthService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/WebAuthService.java new file mode 100644 index 00000000..c0a1bb08 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/WebAuthService.java @@ -0,0 +1,62 @@ +package tech.powerjob.server.auth.service; + +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import java.util.List; +import java.util.Map; + +/** + * Web Auth 服务 + * 写在 starter 包下,抽取 controller 的公共逻辑 + * (powerjob 的 service/core 包核心处理调度核心逻辑,admin 部分代码收口在 stater 包) + * + * @author tjq + * @since 2024/2/12 + */ +public interface WebAuthService { + + /** + * 对当前登录用户授予角色 + * @param roleScope 角色范围 + * @param target 目标 + * @param role 角色 + * @param extra 其他信息 + */ + void grantRole2LoginUser(RoleScope roleScope, Long target, Role role, String extra); + + /** + * 处理授权 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param componentUserRoleInfo 人员角色信息 + */ + void processPermissionOnSave(RoleScope roleScope, Long target, ComponentUserRoleInfo componentUserRoleInfo); + + /** + * 获取目标相关权限人员列表 + * @param roleScope 权限范围 + * @param target 权限目标 + * @return ComponentUserRoleInfo + */ + ComponentUserRoleInfo fetchComponentUserRoleInfo(RoleScope roleScope, Long target); + + /** + * 判断当前用户是否有权限 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param permission 要求的权限 + * @return 是否有权限 + */ + boolean hasPermission(RoleScope roleScope, Long target, Permission permission); + + /** + * 是否为全局管理员 + * @return true or false + */ + boolean isGlobalAdmin(); + + Map> fetchMyPermissionTargets(RoleScope roleScope); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java new file mode 100644 index 00000000..42f6def9 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java @@ -0,0 +1,121 @@ +package tech.powerjob.server.auth.service.impl; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.*; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import javax.annotation.Resource; +import java.util.*; + +/** + * WebAuthService + * + * @author tjq + * @since 2024/2/12 + */ +@Slf4j +@Service +public class WebAuthServiceImpl implements WebAuthService { + + @Resource + private PowerJobPermissionService powerJobPermissionService; + + + @Override + public void grantRole2LoginUser(RoleScope roleScope, Long target, Role role, String extra) { + Long userId = LoginUserHolder.getUserId(); + if (userId == null) { + throw new PowerJobAuthException(ErrorCodes.USER_NOT_LOGIN); + } + powerJobPermissionService.grantRole(roleScope, target, userId, role, extra); + } + + @Override + public void processPermissionOnSave(RoleScope roleScope, Long target, ComponentUserRoleInfo o) { + ComponentUserRoleInfo componentUserRoleInfo = Optional.ofNullable(o).orElse(new ComponentUserRoleInfo()); + + Map> role2Uids = powerJobPermissionService.fetchUserWithPermissions(roleScope, target); + diffGrant(roleScope, target, Role.OBSERVER, componentUserRoleInfo.getObserver(), role2Uids); + diffGrant(roleScope, target, Role.QA, componentUserRoleInfo.getQa(), role2Uids); + diffGrant(roleScope, target, Role.DEVELOPER, componentUserRoleInfo.getDeveloper(), role2Uids); + diffGrant(roleScope, target, Role.ADMIN, componentUserRoleInfo.getAdmin(), role2Uids); + } + + @Override + public ComponentUserRoleInfo fetchComponentUserRoleInfo(RoleScope roleScope, Long target) { + Map> role2Uids = powerJobPermissionService.fetchUserWithPermissions(roleScope, target); + return new ComponentUserRoleInfo() + .setObserver(Lists.newArrayList(role2Uids.getOrDefault(Role.OBSERVER, Collections.emptySet()))) + .setQa(Lists.newArrayList(role2Uids.getOrDefault(Role.QA, Collections.emptySet()))) + .setDeveloper(Lists.newArrayList(role2Uids.getOrDefault(Role.DEVELOPER, Collections.emptySet()))) + .setAdmin(Lists.newArrayList(role2Uids.getOrDefault(Role.ADMIN, Collections.emptySet()))); + } + + @Override + public boolean hasPermission(RoleScope roleScope, Long target, Permission permission) { + + PowerJobUser powerJobUser = LoginUserHolder.get(); + if (powerJobUser == null) { + return false; + } + + return powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, target, permission); + } + + @Override + public boolean isGlobalAdmin() { + return hasPermission(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, Permission.SU); + } + + @Override + public Map> fetchMyPermissionTargets(RoleScope roleScope) { + + PowerJobUser powerJobUser = LoginUserHolder.get(); + if (powerJobUser == null) { + throw new PowerJobAuthException(ErrorCodes.USER_NOT_LOGIN); + } + + // 展示不考虑穿透权限的问题(即拥有 namespace 权限也可以看到全部的 apps) + return powerJobPermissionService.fetchUserHadPermissionTargets(roleScope, powerJobUser.getId()); + } + + private void diffGrant(RoleScope roleScope, Long target, Role role, List uids, Map> originRole2Uids) { + + Set originUids = Sets.newHashSet(Optional.ofNullable(originRole2Uids.get(role)).orElse(Collections.emptySet())); + Set currentUids = Sets.newHashSet(Optional.ofNullable(uids).orElse(Collections.emptyList())); + + Map extraInfo = Maps.newHashMap(); + extraInfo.put("grantor", LoginUserHolder.getUserName()); + extraInfo.put("source", "diffGrant"); + String extra = JsonUtils.toJSONString(extraInfo); + + Set allIds = Sets.newHashSet(originUids); + allIds.addAll(currentUids); + + Set allIds2 = Sets.newHashSet(allIds); + + // 在 originUids 不在 currentUids,需要取消授权 + allIds.removeAll(currentUids); + allIds.forEach(cancelPermissionUid -> { + powerJobPermissionService.retrieveRole(roleScope, target, cancelPermissionUid, role); + log.info("[WebAuthService] [diffGrant] cancelPermission: roleScope={},target={},uid={},role={}", roleScope, target, cancelPermissionUid, role); + }); + + // 在 currentUids 当不在 orignUids,需要增加授权 + allIds2.removeAll(originUids); + allIds2.forEach(addPermissionUid -> { + powerJobPermissionService.grantRole(roleScope, target, addPermissionUid, role, extra); + log.info("[WebAuthService] [diffGrant] grantPermission: roleScope={},target={},uid={},role={},extra={}", roleScope, target, addPermissionUid, role, extra); + }); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java new file mode 100644 index 00000000..ec471f7a --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java @@ -0,0 +1,119 @@ +package tech.powerjob.server.config; + +import com.google.common.collect.Sets; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; +import java.util.Set; + +/** + * 解决 HttpServletRequest 只能被读取一次的问题,方便全局日志 & 鉴权,切面提前读取数据 + * 在请求进入Servlet容器之前,先经过Filter的过滤器链。在请求进入Controller之前,先经过 HandlerInterceptor 的拦截器链。Filter 一定先于 HandlerInterceptor 执行 + * 解决HttpServletRequest 流数据不可重复读 + * + * @author tjq + * @since 2024/2/11 + */ +@Component +public class CachingRequestBodyFilter implements Filter { + + + /** + * 忽略部分不需要处理的类型: + * GET 请求的数据一般是 Query String,直接在 url 的后面,不需要特殊处理 + * multipart/form-data:doDispatch() 阶段就会进行处理,此处已经空值了,强行处理会导致结果空 + * application/x-www-form-urlencoded:估计也类似,有特殊逻辑,导致 OpenAPI 部分请求参数无法传递,同样忽略 + */ + private static final Set IGNORE_CONTENT_TYPES = Sets.newHashSet("application/x-www-form-urlencoded", "multipart/form-data"); + + private static final Set IGNORE_URIS = Sets.newHashSet("/container/jarUpload"); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + String uri = ((HttpServletRequest) request).getRequestURI(); + // 忽略 jar 上传等处理路径 + if (IGNORE_URIS.contains(uri)) { + chain.doFilter(request, response); + return; + } + String contentType = request.getContentType(); + if (contentType != null && !IGNORE_CONTENT_TYPES.contains(contentType)) { + CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(wrappedRequest, response); + return; + } + } + chain.doFilter(request, response); + } + + // Implement other required methods like init() and destroy() if necessary + + + public static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final String body; + + public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try { + InputStream inputStream = request.getInputStream(); + if (inputStream != null) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + } + body = stringBuilder.toString(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); + + return new ServletInputStream() { + public int read() throws IOException { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return byteArrayInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException("Not implemented"); + } + }; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + public String getBody() { + return this.body; + } + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java index 5f4467e1..44c11726 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java @@ -41,7 +41,7 @@ public class SwaggerConfig { return new OpenAPI() .info(new Info().title("PowerJob") .description("Distributed scheduling and computing framework.") - .version(serverInfoService.fetchServiceInfo().getVersion()) + .version(serverInfoService.fetchCurrentServerInfo().getVersion()) .contact(contact) .license(new License().name("Apache License 2.0").url("https://github.com/PowerJob/PowerJob/blob/master/LICENSE"))); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java index c1e819ad..b16f2017 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java @@ -3,9 +3,15 @@ package tech.powerjob.server.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; +import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.server.auth.interceptor.PowerJobAuthInterceptor; +import tech.powerjob.server.openapi.OpenApiInterceptor; + +import javax.annotation.Resource; /** * CORS @@ -16,6 +22,12 @@ import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration @EnableWebSocket public class WebConfig implements WebMvcConfigurer { + + @Resource + private OpenApiInterceptor openApiInterceptor; + @Resource + private PowerJobAuthInterceptor powerJobAuthInterceptor; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -26,4 +38,23 @@ public class WebConfig implements WebMvcConfigurer { public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + /* + 可以添加多个拦截器 + addPathPatterns("/**") 表示对所有请求都拦截 + .excludePathPatterns("/base/index") 表示排除对/base/index请求的拦截 + 多个拦截器可以设置order顺序,值越小,preHandle越先执行,postHandle和afterCompletion越后执行 + order默认的值是0,如果只添加一个拦截器,可以不显示设置order的值 + */ + registry.addInterceptor(powerJobAuthInterceptor) + .addPathPatterns("/**") + .excludePathPatterns("/css/**", "/js/**", "/images/**", "/img/**", "/fonts/**", "/favicon.ico") + .order(0); + + registry.addInterceptor(openApiInterceptor) + .addPathPatterns(OpenAPIConstant.WEB_PATH.concat("/**")) + .order(1); + } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java new file mode 100644 index 00000000..130b18fd --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java @@ -0,0 +1,102 @@ +package tech.powerjob.server.initializer; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.extension.LockService; +import tech.powerjob.server.persistence.remote.model.SundryDO; +import tech.powerjob.server.persistence.remote.repository.SundryRepository; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * 新系统初始化器 + * + * @author tjq + * @since 2023/9/5 + */ +@Slf4j +@Component +public class NewSystemInitializer implements CommandLineRunner { + + + private static final String LOCK_PREFIX = "sys_init_lock_"; + + private static final int MAX_LOCK_TIME = 5000; + + + @Resource + private LockService lockService; + @Resource + private SundryRepository sundryRepository; + @Resource + private SystemInitializeService systemInitializeService; + + private static final String SUNDRY_PKEY = "sys_initialize"; + + @Override + public void run(String... args) throws Exception { + initSystemAdmin(); + initDefaultNamespace(); + } + + private void initSystemAdmin() { + clusterInit(SystemInitializeService.GOAL_INIT_ADMIN, Void -> systemInitializeService.initAdmin()); + } + + private void initDefaultNamespace() { + clusterInit(SystemInitializeService.GOAL_INIT_NAMESPACE, Void -> systemInitializeService.initNamespace()); + } + + private void clusterInit(String name, Consumer initFunc) { + + Optional sundryOpt = sundryRepository.findByPkeyAndSkey(SUNDRY_PKEY, name); + if (sundryOpt.isPresent()) { + log.info("[NewSystemInitializer] already initialized, skip: {}", name); + return; + } + + String lockName = LOCK_PREFIX.concat(name); + + while (true) { + try { + + boolean lockStatus = lockService.tryLock(lockName, MAX_LOCK_TIME); + + // 无论是否拿到锁,都重现检测一次,如果已完成初始化,则直接 return + Optional sundryOpt2 = sundryRepository.findByPkeyAndSkey(SUNDRY_PKEY, name); + if (sundryOpt2.isPresent()) { + log.info("[NewSystemInitializer] other server finished initialize, skip process: {}", name); + break; + } + + if (!lockStatus) { + CommonUtils.easySleep(277); + continue; + } + + log.info("[NewSystemInitializer] try to initialize: {}", name); + initFunc.accept(null); + log.info("[NewSystemInitializer] initialize [{}] successfully!", name); + + // 写入初始化成功标记 + SundryDO sundryDO = new SundryDO(); + sundryDO.setPkey(SUNDRY_PKEY); + sundryDO.setSkey(name); + sundryDO.setContent("A"); + sundryDO.setGmtCreate(new Date()); + sundryRepository.saveAndFlush(sundryDO); + log.info("[NewSystemInitializer] write initialized tag successfully: {}", sundryDO); + + break; + } finally { + lockService.unlock(lockName); + } + } + + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java new file mode 100644 index 00000000..e5ec2ceb --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java @@ -0,0 +1,24 @@ +package tech.powerjob.server.initializer; + +/** + * 系统初始化服务 + * + * @author tjq + * @since 2024/2/15 + */ +public interface SystemInitializeService { + + String GOAL_INIT_ADMIN = "goal_init_admin"; + String GOAL_INIT_NAMESPACE = "goal_init_namespace"; + + + /** + * 初始化超级管理员 + */ + void initAdmin(); + + /** + * 初始化 namespace + */ + void initNamespace(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java new file mode 100644 index 00000000..00a717c7 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java @@ -0,0 +1,105 @@ +package tech.powerjob.server.initializer; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.service.login.LoginRequest; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.NamespaceWebService; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; +import javax.transaction.Transactional; +import java.util.Map; + +/** + * 初始化 PowerJob 首次部署相关的内容 + * 为了可维护性足够高,统一使用 WEB 请求进行初始化,不直接操作底层,防止后续内部逻辑变更后出现问题 + * + * @author tjq + * @since 2024/2/15 + */ +@Slf4j +@Service +public class SystemInitializeServiceImpl implements SystemInitializeService { + + @Value("${oms.auth.initiliaze.admin.password:#{null}}") + private String defaultAdminPassword; + @Resource + private PwjbUserWebService pwjbUserWebService; + @Resource + private NamespaceWebService namespaceWebService; + @Resource + private PowerJobLoginService powerJobLoginService; + @Resource + private PowerJobPermissionService powerJobPermissionService; + + private static final String SYSTEM_ADMIN_NAME = "ADMIN"; + + + private static final String SYSTEM_DEFAULT_NAMESPACE = "default_namespace"; + + @Override + @Transactional(rollbackOn = Exception.class) + public void initAdmin() { + + String username = SYSTEM_ADMIN_NAME; + String password = StringUtils.isEmpty(defaultAdminPassword) ? RandomStringUtils.randomAlphabetic(8) : defaultAdminPassword; + + // STEP1: 创建 PWJB 用户 + ModifyUserInfoRequest createUser = new ModifyUserInfoRequest(); + createUser.setUsername(username); + createUser.setNick(username); + createUser.setPassword(password); + + log.info("[SystemInitializeService] [S1] create default PWJB user by request: {}", createUser); + PwjbUserInfoDO savedPwjbUser = pwjbUserWebService.save(createUser); + log.info("[SystemInitializeService] [S1] create default PWJB user successfully: {}", savedPwjbUser); + + Map params = Maps.newHashMap(); + params.put(AuthConstants.PARAM_KEY_USERNAME, username); + params.put(AuthConstants.PARAM_KEY_PASSWORD, password); + + // STEP2: 创建 USER 对象 + LoginRequest loginRequest = new LoginRequest() + .setLoginType(AuthConstants.ACCOUNT_TYPE_POWER_JOB) + .setOriginParams(JsonUtils.toJSONString(params)); + log.info("[SystemInitializeService] [S2] createPowerJobUser user by request: {}", loginRequest); + PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginRequest); + log.info("[SystemInitializeService] [S2] createPowerJobUser successfully: {}", powerJobUser); + + // STEP3: 授予全局管理员权限 + powerJobPermissionService.grantRole(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, powerJobUser.getId(), Role.ADMIN, null); + log.info("[SystemInitializeService] [S3] GRANT ADMIN successfully!"); + + // 循环10遍,强提醒用户,第一次使用必须更改 admin 密码 + for (int i = 0; i < 10; i++) { + log.warn("[SystemInitializeService] The system has automatically created a super administrator account[username={},password={}], please log in and change the password immediately!", username, password); + } + } + + @Override + @Transactional(rollbackOn = Exception.class) + public void initNamespace() { + ModifyNamespaceRequest saveNamespaceReq = new ModifyNamespaceRequest(); + saveNamespaceReq.setName(SYSTEM_DEFAULT_NAMESPACE); + saveNamespaceReq.setCode(SYSTEM_DEFAULT_NAMESPACE); + + log.info("[SystemInitializeService] create default namespace by request: {}", saveNamespaceReq); + NamespaceDO savedNamespaceDO = namespaceWebService.save(saveNamespaceReq); + log.info("[SystemInitializeService] create default namespace successfully: {}", savedNamespaceDO); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/OpenAPIController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenAPIController.java similarity index 87% rename from powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/OpenAPIController.java rename to powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenAPIController.java index 33e76f7c..40a9abf3 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/OpenAPIController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenAPIController.java @@ -1,24 +1,28 @@ -package tech.powerjob.server.web.controller; +package tech.powerjob.server.openapi; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.web.bind.annotation.*; +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; import tech.powerjob.common.OpenAPIConstant; import tech.powerjob.common.PowerQuery; +import tech.powerjob.common.enums.ErrorCodes; import tech.powerjob.common.enums.InstanceStatus; +import tech.powerjob.common.exception.PowerJobException; import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.request.http.SaveWorkflowNodeRequest; import tech.powerjob.common.request.http.SaveWorkflowRequest; import tech.powerjob.common.request.query.JobInfoQuery; -import tech.powerjob.common.response.InstanceInfoDTO; -import tech.powerjob.common.response.JobInfoDTO; -import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.common.response.WorkflowInstanceInfoDTO; +import tech.powerjob.common.response.*; import tech.powerjob.server.core.instance.InstanceService; import tech.powerjob.server.core.service.AppInfoService; import tech.powerjob.server.core.service.CacheService; import tech.powerjob.server.core.service.JobService; import tech.powerjob.server.core.workflow.WorkflowInstanceService; import tech.powerjob.server.core.workflow.WorkflowService; +import tech.powerjob.server.openapi.security.OpenApiSecurityService; import tech.powerjob.server.persistence.remote.model.WorkflowInfoDO; import tech.powerjob.server.persistence.remote.model.WorkflowNodeInfoDO; import tech.powerjob.server.web.response.WorkflowInfoVO; @@ -31,6 +35,7 @@ import java.util.List; * @author tjq * @since 2020/4/15 */ +@Slf4j @RestController @RequestMapping(OpenAPIConstant.WEB_PATH) @RequiredArgsConstructor @@ -46,12 +51,37 @@ public class OpenAPIController { private final WorkflowInstanceService workflowInstanceService; + private final OpenApiSecurityService openApiSecurityService; + private final CacheService cacheService; @PostMapping(OpenAPIConstant.ASSERT) public ResultDTO assertAppName(String appName, @RequestParam(required = false) String password) { - return ResultDTO.success(appInfoService.assertApp(appName, password)); + return ResultDTO.success(appInfoService.assertApp(appName, password, null)); + } + + /** + * APP 鉴权 + * @param appAuthRequest 鉴权请求 + * @return 鉴权响应 + */ + @PostMapping(OpenAPIConstant.AUTH_APP) + public PowerResultDTO auth(@RequestBody AppAuthRequest appAuthRequest) { + try { + return PowerResultDTO.s(openApiSecurityService.authAppByParam(appAuthRequest)); + } catch (PowerJobException pje) { + PowerResultDTO f = PowerResultDTO.f(pje.getMessage()); + f.setCode(pje.getCode()); + return f; + } catch (Throwable t) { + + log.error("[OpenAPIController] auth failed for request: {}", appAuthRequest, t); + + PowerResultDTO f = PowerResultDTO.f(ExceptionUtils.getMessage(t)); + f.setCode(ErrorCodes.SYSTEM_UNKNOWN_ERROR.getCode()); + return f; + } } /* ************* Job 区 ************* */ diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenApiInterceptor.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenApiInterceptor.java new file mode 100644 index 00000000..ecd632eb --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/OpenApiInterceptor.java @@ -0,0 +1,91 @@ +package tech.powerjob.server.openapi; + +import com.google.common.collect.Sets; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import tech.powerjob.common.OmsConstant; +import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.response.PowerResultDTO; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.openapi.security.OpenApiSecurityService; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.util.Set; + +/** + * OpenAPI 拦截器 + * + * @author 程序帕鲁 + * @since 2024/2/19 + */ +@Slf4j +@Component +public class OpenApiInterceptor implements HandlerInterceptor { + + @Resource + private OpenApiSecurityService openApiSecurityService; + + /** + * 4.x 及前序版本的 OpenAPI 均为携带 auth 的必要参数,直接开启鉴权功能会导致之前的服务全部报错 + * 因此提供功能开关给到使用者,若无安全影响,可展示关闭鉴权功能,等 client 升级完毕后再打开鉴权 + */ + @Value("${oms.auth.openapi.enable:false}") + private boolean enableOpenApiAuth; + + private static final Set IGNORE_OPEN_API_PATH = Sets.newHashSet(OpenAPIConstant.ASSERT, OpenAPIConstant.AUTH_APP); + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { + + if (!enableOpenApiAuth) { + return true; + } + + // 鉴权类请求跳过拦截 + String requestURI = request.getRequestURI(); + for (String endPath : IGNORE_OPEN_API_PATH) { + if (requestURI.endsWith(endPath)) { + return true; + } + } + + try { + openApiSecurityService.authAppByToken(request); + response.addHeader(OpenAPIConstant.RESPONSE_HEADER_AUTH_STATUS, Boolean.TRUE.toString()); + } catch (PowerJobException pje) { + response.addHeader(OpenAPIConstant.RESPONSE_HEADER_AUTH_STATUS, Boolean.FALSE.toString()); + writeResponse(PowerResultDTO.f(pje), response); + return false; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + writeResponse(PowerResultDTO.f(e), response); + + log.error("[OpenApiInterceptor] unknown exception when auth app by token", e); + + return false; + } + + return true; + } + + @SneakyThrows + private void writeResponse( PowerResultDTO powerResult, HttpServletResponse response) { + + // 设置响应的 Content-Type + response.setContentType(OmsConstant.JSON_MEDIA_TYPE); + + // 将 JSON 写入响应 + PrintWriter writer = response.getWriter(); + writer.write(JsonUtils.toJSONString(powerResult)); + writer.flush(); + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityService.java new file mode 100644 index 00000000..ab9ee65f --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityService.java @@ -0,0 +1,28 @@ +package tech.powerjob.server.openapi.security; + +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; + +import javax.servlet.http.HttpServletRequest; + +/** + * OPENAPI 安全服务 + * + * @author tjq + * @since 2024/2/19 + */ +public interface OpenApiSecurityService { + + /** + * APP 纬度请求的鉴权 & 验证 + * @param appAuthRequest 请求参数 + * @return token + */ + AppAuthResult authAppByParam(AppAuthRequest appAuthRequest); + + /** + * APP 纬度请求的鉴权 & 验证 + * @param httpServletRequest http 原始请求 + */ + void authAppByToken(HttpServletRequest httpServletRequest); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityServiceImpl.java new file mode 100644 index 00000000..00c4a762 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/openapi/security/OpenApiSecurityServiceImpl.java @@ -0,0 +1,106 @@ +package tech.powerjob.server.openapi.security; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.client.module.AppAuthRequest; +import tech.powerjob.client.module.AppAuthResult; +import tech.powerjob.common.OpenAPIConstant; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.common.utils.HttpServletUtils; +import tech.powerjob.server.auth.jwt.JwtService; +import tech.powerjob.server.auth.jwt.ParseResult; +import tech.powerjob.server.core.service.AppInfoService; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Optional; + +/** + * OpenApiSecurityService + * + * @author tjq + * @since 2024/2/19 + */ +@Slf4j +@Service +public class OpenApiSecurityServiceImpl implements OpenApiSecurityService { + + @Resource + private JwtService jwtService; + @Resource + private AppInfoService appInfoService; + + private static final String JWT_KEY_APP_ID = "appId"; + private static final String JWT_KEY_APP_PASSWORD = "password"; + private static final String JWT_KEY_ENCRYPT_TYPE = "encryptType"; + + @Override + public void authAppByToken(HttpServletRequest httpServletRequest) { + + String token = HttpServletUtils.fetchFromHeader(OpenAPIConstant.REQUEST_HEADER_ACCESS_TOKEN, httpServletRequest); + String appIdFromHeader = HttpServletUtils.fetchFromHeader(OpenAPIConstant.REQUEST_HEADER_APP_ID, httpServletRequest); + + if (StringUtils.isEmpty(appIdFromHeader)) { + throw new PowerJobException(ErrorCodes.INVALID_REQUEST, "lack_of_appId_in_header"); + } + + if (StringUtils.isEmpty(token)) { + throw new PowerJobException(ErrorCodes.OPEN_API_AUTH_FAILED, "token_is_empty"); + } + + ParseResult parseResult = jwtService.parse(token, null); + switch (parseResult.getStatus()) { + case EXPIRED: + throw new PowerJobException(ErrorCodes.TOKEN_EXPIRED, parseResult.getMsg()); + case FAILED: + throw new PowerJobException(ErrorCodes.INVALID_TOKEN, parseResult.getMsg()); + } + + Map jwtResult = parseResult.getResult(); + + Long appIdFromJwt = MapUtils.getLong(jwtResult, JWT_KEY_APP_ID); + String passwordFromJwt = MapUtils.getString(jwtResult, JWT_KEY_APP_PASSWORD); + String encryptType = MapUtils.getString(jwtResult, JWT_KEY_ENCRYPT_TYPE); + + // 校验 appId 一致性 + if (!StringUtils.equals(appIdFromHeader, String.valueOf(appIdFromJwt))) { + throw new PowerJobException(ErrorCodes.INVALID_REQUEST, "Inconsistent_appId_from_token_and_header"); + } + + // 此处不考虑改密码后的缓存时间,毕竟只要改了密码,一定会报错。换言之 OpenAPI 模式下,密码不可更改 + Optional appInfoOpt = appInfoService.findById(appIdFromJwt, true); + if (!appInfoOpt.isPresent()) { + throw new PowerJobException(ErrorCodes.INVALID_APP, "can_not_find_app"); + } + + appInfoService.assertApp(appInfoOpt.get(), passwordFromJwt, encryptType); + } + + + @Override + public AppAuthResult authAppByParam(AppAuthRequest appAuthRequest) { + + String appName = appAuthRequest.getAppName(); + String encryptedPassword = appAuthRequest.getEncryptedPassword(); + + Long appId = appInfoService.assertApp(appName, encryptedPassword, appAuthRequest.getEncryptType()); + + Map jwtBody = Maps.newHashMap(); + jwtBody.put(JWT_KEY_APP_ID, appId); + jwtBody.put(JWT_KEY_APP_PASSWORD, encryptedPassword); + jwtBody.put(JWT_KEY_ENCRYPT_TYPE, appAuthRequest.getEncryptType()); + + AppAuthResult appAuthResult = new AppAuthResult(); + + appAuthResult.setAppId(appId); + appAuthResult.setToken(jwtService.build(jwtBody, null)); + + return appAuthResult; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java index 6a4aa54a..cd335752 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java @@ -19,7 +19,7 @@ import java.util.List; public class ServerInfoAwareProcessor { public ServerInfoAwareProcessor(ServerInfoService serverInfoService, List awareList) { - final ServerInfo serverInfo = serverInfoService.fetchServiceInfo(); + final ServerInfo serverInfo = serverInfoService.fetchCurrentServerInfo(); log.info("[ServerInfoAwareProcessor] current server info: {}", serverInfo); awareList.forEach(aware -> { aware.setServerInfo(serverInfo); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java index 0730312f..45fc77bc 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java @@ -1,7 +1,5 @@ package tech.powerjob.server.web; -import tech.powerjob.common.exception.PowerJobException; -import tech.powerjob.common.response.ResultDTO; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -10,6 +8,8 @@ import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.response.PowerResultDTO; /** * 统一处理 web 层异常信息 @@ -23,11 +23,16 @@ public class ControllerExceptionHandler { @ResponseBody @ExceptionHandler(Exception.class) - public ResultDTO exceptionHandler(Exception e) { + public PowerResultDTO exceptionHandler(Exception e) { + + PowerResultDTO ret = PowerResultDTO.f(ExceptionUtils.getMessage(e)); // 不是所有异常都需要打印完整堆栈,后续可以定义内部的Exception,便于判断 - if (e instanceof IllegalArgumentException || e instanceof PowerJobException) { - log.warn("[ControllerException] http request failed, message is {}.", e.getMessage()); + if (e instanceof PowerJobException) { + ret.setCode(((PowerJobException) e).getCode()); + log.warn("[ControllerException] PowerJobException, message is {}.", e.getMessage()); + } else if (e instanceof IllegalArgumentException) { + log.warn("[ControllerException] http request failed due to IllegalArgument, message is {}.", e.getMessage()); } else if (e instanceof HttpMessageNotReadableException || e instanceof MethodArgumentTypeMismatchException) { log.warn("[ControllerException] invalid http request params, exception is {}.", e.getMessage()); } else if (e instanceof HttpRequestMethodNotSupportedException) { @@ -35,6 +40,7 @@ public class ControllerExceptionHandler { } else { log.error("[ControllerException] http request failed.", e); } - return ResultDTO.failed(ExceptionUtils.getMessage(e)); + + return ret; } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java index f50e9a90..36d048ef 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java @@ -1,26 +1,55 @@ package tech.powerjob.server.web.controller; -import lombok.RequiredArgsConstructor; -import tech.powerjob.common.exception.PowerJobException; -import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.persistence.remote.model.AppInfoDO; -import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; -import tech.powerjob.server.core.service.AppInfoService; -import tech.powerjob.server.web.request.AppAssertRequest; -import tech.powerjob.server.web.request.ModifyAppInfoRequest; import com.google.common.collect.Lists; -import lombok.Data; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.util.CollectionUtils; -import org.springframework.web.bind.annotation.*; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.plugin.ModifyOrCreateDynamicPermission; +import tech.powerjob.server.auth.plugin.SaveAppGrantPermissionPlugin; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.common.module.WorkerInfo; +import tech.powerjob.server.core.service.AppInfoService; +import tech.powerjob.server.persistence.PageResult; +import tech.powerjob.server.persistence.QueryConvertUtils; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.remote.worker.WorkerClusterQueryService; +import tech.powerjob.server.web.converter.NamespaceConverter; +import tech.powerjob.server.web.request.AppAssertRequest; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; +import tech.powerjob.server.web.request.ModifyAppInfoRequest; +import tech.powerjob.server.web.request.QueryAppInfoRequest; +import tech.powerjob.server.web.response.AppInfoVO; +import tech.powerjob.server.web.response.NamespaceBaseVO; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.service.NamespaceWebService; +import tech.powerjob.server.web.service.UserWebService; -import javax.annotation.Resource; -import java.util.Date; -import java.util.List; -import java.util.Objects; +import javax.persistence.criteria.Predicate; +import java.util.*; import java.util.stream.Collectors; /** @@ -30,19 +59,26 @@ import java.util.stream.Collectors; * @author tjq * @since 2020/4/1 */ +@Slf4j @RestController @RequestMapping("/appInfo") @RequiredArgsConstructor public class AppInfoController { - private final AppInfoService appInfoService; + private final WebAuthService webAuthService; + private final UserWebService userWebService; + + private final AppInfoService appInfoService; private final AppInfoRepository appInfoRepository; - private static final int MAX_APP_NUM = 200; + private final NamespaceWebService namespaceWebService; + + private final WorkerClusterQueryService workerClusterQueryService; @PostMapping("/save") - public ResultDTO saveAppInfo(@RequestBody ModifyAppInfoRequest req) { + @ApiPermission(name = "App-Save", roleScope = RoleScope.APP, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveAppGrantPermissionPlugin.class) + public ResultDTO saveAppInfo(@RequestBody ModifyAppInfoRequest req) { req.valid(); AppInfoDO appInfoDO; @@ -51,59 +87,165 @@ public class AppInfoController { if (id == null) { appInfoDO = new AppInfoDO(); appInfoDO.setGmtCreate(new Date()); - }else { - appInfoDO = appInfoRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("can't find appInfo by id:" + id)); + appInfoDO.setCreator(LoginUserHolder.getUserId()); + } else { + appInfoDO = appInfoService.findById(id, false).orElseThrow(() -> new IllegalArgumentException("can't find appInfo by id:" + id)); - // 对比密码 - if (!Objects.equals(req.getOldPassword(), appInfoDO.getPassword())) { - throw new PowerJobException("The password is incorrect."); + // 不允许修改 appName + if (!appInfoDO.getAppName().equalsIgnoreCase(req.getAppName())) { + throw new IllegalArgumentException("NOT_ALLOW_CHANGE_THE_APP_NAME"); } } - BeanUtils.copyProperties(req, appInfoDO); + + appInfoDO.setAppName(req.getAppName()); + appInfoDO.setTitle(req.getTitle()); + appInfoDO.setPassword(req.getPassword()); + appInfoDO.setNamespaceId(req.getNamespaceId()); + appInfoDO.setTags(req.getTags()); + appInfoDO.setExtra(req.getExtra()); + appInfoDO.setGmtModified(new Date()); + appInfoDO.setModifier(LoginUserHolder.getUserId()); - appInfoRepository.saveAndFlush(appInfoDO); - return ResultDTO.success(null); + AppInfoDO savedAppInfo = appInfoService.save(appInfoDO); + + // 重现授权 + webAuthService.processPermissionOnSave(RoleScope.APP, savedAppInfo.getId(), req.getComponentUserRoleInfo()); + + return ResultDTO.success(convert(Lists.newArrayList(savedAppInfo), false).get(0)); } - @PostMapping("/assert") - public ResultDTO assertApp(@RequestBody AppAssertRequest request) { - return ResultDTO.success(appInfoService.assertApp(request.getAppName(), request.getPassword())); - } + @PostMapping("/delete") + @ApiPermission(name = "App-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.SU) + public ResultDTO deleteApp(Long appId) { - @GetMapping("/delete") - public ResultDTO deleteAppInfo(Long appId) { - appInfoRepository.deleteById(appId); - return ResultDTO.success(null); - } + log.warn("[AppInfoController] try to delete app: {}", appId); - @GetMapping("/list") - public ResultDTO> listAppInfo(@RequestParam(required = false) String condition) { - List result; - Pageable limit = PageRequest.of(0, MAX_APP_NUM); - if (StringUtils.isEmpty(condition)) { - result = appInfoRepository.findAll(limit).getContent(); - }else { - result = appInfoRepository.findByAppNameLike("%" + condition + "%", limit).getContent(); + List allAliveWorkers = workerClusterQueryService.getAllAliveWorkers(appId); + if (CollectionUtils.isNotEmpty(allAliveWorkers)) { + return ResultDTO.failed("Unable to delete apps with live workers, Please remove the worker dependency first!"); } - return ResultDTO.success(convert(result)); + + appInfoService.deleteById(appId); + log.warn("[AppInfoController] delete app[id={}] successfully!", appId); + return ResultDTO.success(null); } - private static List convert(List data) { + @PostMapping("/list") + @ApiPermission(name = "App-List", roleScope = RoleScope.APP, requiredPermission = Permission.NONE) + public ResultDTO> listAppInfoByQuery(@RequestBody QueryAppInfoRequest queryAppInfoRequest) { + + Pageable pageable = PageRequest.of(queryAppInfoRequest.getIndex(), queryAppInfoRequest.getPageSize()); + + // 相关权限(先查处关联 ids) + Set queryAppIds; + Boolean showMyRelated = queryAppInfoRequest.getShowMyRelated(); + if (BooleanUtils.isTrue(showMyRelated)) { + Set targetIds = Sets.newHashSet(); + webAuthService.fetchMyPermissionTargets(RoleScope.APP).values().forEach(targetIds::addAll); + queryAppIds = targetIds; + + if (CollectionUtils.isEmpty(queryAppIds)) { + return ResultDTO.success(new PageResult<>()); + } + + } else { + queryAppIds = Collections.emptySet(); + } + + Specification specification = (root, query, criteriaBuilder) -> { + List predicates = Lists.newArrayList(); + + Long appId = queryAppInfoRequest.getAppId(); + Long namespaceId = queryAppInfoRequest.getNamespaceId(); + + if (appId != null) { + predicates.add(criteriaBuilder.equal(root.get("id"), appId)); + } + + if (namespaceId != null) { + predicates.add(criteriaBuilder.equal(root.get("namespaceId"), namespaceId)); + } + + if (StringUtils.isNotEmpty(queryAppInfoRequest.getAppNameLike())) { + predicates.add(criteriaBuilder.like(root.get("appName"), QueryConvertUtils.convertLikeParams(queryAppInfoRequest.getAppNameLike()))); + } + + if (StringUtils.isNotEmpty(queryAppInfoRequest.getTagLike())) { + predicates.add(criteriaBuilder.like(root.get("tags"), QueryConvertUtils.convertLikeParams(queryAppInfoRequest.getTagLike()))); + } + + if (!queryAppIds.isEmpty()) { + predicates.add(criteriaBuilder.in(root.get("id")).value(queryAppIds)); + } + + return query.where(predicates.toArray(new Predicate[0])).getRestriction(); + }; + + Page pageAppInfoResult = appInfoRepository.findAll(specification, pageable); + + PageResult pageRet = new PageResult<>(pageAppInfoResult); + + List appInfoDos = pageAppInfoResult.get().collect(Collectors.toList()); + pageRet.setData(convert(appInfoDos, true)); + + return ResultDTO.success(pageRet); + } + + @PostMapping("/becomeAdmin") + @ApiPermission(name = "App-BecomeAdmin", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.NONE) + public ResultDTO becomeAdminByAppNameAndPassword(@RequestBody AppAssertRequest appAssertRequest) { + String appName = appAssertRequest.getAppName(); + + Long appId = appInfoService.assertApp(appName, appAssertRequest.getPassword(), appAssertRequest.getEncryptType()); + + Map extra = Maps.newHashMap(); + extra.put("source", "becomeAdminByAppNameAndPassword"); + + webAuthService.grantRole2LoginUser(RoleScope.APP, appId, Role.ADMIN, JsonUtils.toJSONString(extra)); + + return ResultDTO.success(null); + } + + private List convert(List data, boolean fillDetail) { if (CollectionUtils.isEmpty(data)) { return Lists.newLinkedList(); } + + // app 界面使用频率不高,数据库操作 rt 也不会太长,展示不考虑性能问题,简单期间串行补全 return data.stream().map(appInfoDO -> { AppInfoVO appInfoVO = new AppInfoVO(); BeanUtils.copyProperties(appInfoDO, appInfoVO); + + appInfoVO.setGmtCreateStr(CommonUtils.formatTime(appInfoDO.getGmtCreate())); + appInfoVO.setGmtModifiedStr(CommonUtils.formatTime(appInfoDO.getGmtModified())); + + if (fillDetail) { + // 人员面板 + ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.APP, appInfoDO.getId()); + appInfoVO.setComponentUserRoleInfo(componentUserRoleInfo); + + // 密码 + boolean hasPermission = webAuthService.hasPermission(RoleScope.APP, appInfoDO.getId(), Permission.READ); + String originPassword = appInfoService.fetchOriginAppPassword(appInfoDO); + appInfoVO.setPassword(hasPermission ? originPassword : AuthConstants.TIPS_NO_PERMISSION_TO_SEE); + + // namespace + Optional namespaceOpt = namespaceWebService.findById(appInfoDO.getNamespaceId()); + if (namespaceOpt.isPresent()) { + NamespaceBaseVO baseNamespace = NamespaceConverter.do2BaseVo(namespaceOpt.get()); + appInfoVO.setNamespace(baseNamespace); + appInfoVO.setNamespaceName(baseNamespace.getName()); + } + + // user 信息 + appInfoVO.setCreatorShowName(userWebService.fetchBaseUserInfo(appInfoDO.getCreator()).map(UserBaseVO::getShowName).orElse(null)); + appInfoVO.setModifierShowName(userWebService.fetchBaseUserInfo(appInfoDO.getModifier()).map(UserBaseVO::getShowName).orElse(null)); + + } + return appInfoVO; }).collect(Collectors.toList()); } - @Data - private static class AppInfoVO { - private Long id; - private String appName; - } - } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java new file mode 100644 index 00000000..b9198764 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java @@ -0,0 +1,123 @@ +package tech.powerjob.server.web.controller; + +import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.utils.CollectionUtils; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.login.LoginTypeInfo; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.auth.service.login.LoginRequest; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import javax.annotation.Resource; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Optional; + +/** + * 登录 & 权限相关 + * + * @author tjq + * @since 2023/4/16 + */ +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Resource + private WebAuthService webAuthService; + @Resource + private PowerJobLoginService powerJobLoginService; + + @GetMapping("/supportLoginTypes") + public ResultDTO> listSupportLoginTypes() { + return ResultDTO.success(powerJobLoginService.fetchSupportLoginTypes()); + } + + @GetMapping("/thirdPartyLoginUrl") + public ResultDTO getThirdPartyLoginUrl(String type, HttpServletRequest request) { + String url = powerJobLoginService.fetchThirdPartyLoginUrl(type, request); + return ResultDTO.success(url); + } + + /** + * 第三方账号体系回调登录接口,eg, 接受钉钉登录回调 + * @param httpServletRequest 请求 + * @param httpServletResponse 响应 + * @return 登录结果 + */ + @RequestMapping(value = "/thirdPartyLoginCallback", method = {RequestMethod.GET, RequestMethod.POST}) + public ResultDTO loginCallback(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { + + LoginRequest loginContext = new LoginRequest().setHttpServletRequest(httpServletRequest); + + // 常见登录组件的标准规范(钉钉、企业微信、飞书),第三方原样透传。开发者在对接第三方登录体系时,可能需要修改此处,将 type 回填 + final String state = httpServletRequest.getParameter("state"); + loginContext.setLoginType(state); + + final PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginContext); + fillJwt4LoginUser(powerJobUser, httpServletResponse); + + return ResultDTO.success(powerJobUser); + } + + /** + * 第三方账号体系直接登录接口,eg, 接受 PowerJob 自带账号密码体系的登录请求 + * @param loginRequest 登录请求 + * @param httpServletResponse 响应 + * @return 登录结果 + */ + @PostMapping("/thirdPartyLoginDirect") + public ResultDTO selfLogin(@RequestBody LoginRequest loginRequest, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { + loginRequest.setHttpServletRequest(httpServletRequest); + try { + final PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginRequest); + if (powerJobUser == null) { + return ResultDTO.failed("USER_NOT_FOUND"); + } + fillJwt4LoginUser(powerJobUser, httpServletResponse); + return ResultDTO.success(powerJobUser); + } catch (Exception e) { + return ResultDTO.failed(e.getMessage()); + } + } + + @GetMapping(value = "/ifLogin") + public ResultDTO ifLogin(HttpServletRequest httpServletRequest) { + final Optional powerJobUser = powerJobLoginService.ifLogin(httpServletRequest); + return powerJobUser.map(ResultDTO::success).orElseGet(() -> ResultDTO.success(null)); + } + + /* ****************** 授权相关 ****************** */ + + @GetMapping("/listGlobalAdmin") + public ResultDTO> listGlobalAdmin() { + // 全局只设置超级管理员权限 + ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID); + return ResultDTO.success(componentUserRoleInfo.getAdmin()); + } + + @PostMapping("/saveGlobalAdmin") + @ApiPermission(name = "Auth-SaveGlobalAdmin", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.SU) + public ResultDTO saveGlobalAdmin(@RequestBody ComponentUserRoleInfo componentUserRoleInfo) { + + if (CollectionUtils.isEmpty(componentUserRoleInfo.getAdmin())) { + throw new IllegalArgumentException("At least one super administrator is required!"); + } + + webAuthService.processPermissionOnSave(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, componentUserRoleInfo); + + return ResultDTO.success(null); + } + + private void fillJwt4LoginUser(PowerJobUser powerJobUser, HttpServletResponse httpServletResponse) { + httpServletResponse.addCookie(new Cookie(AuthConstants.JWT_NAME, powerJobUser.getJwtToken())); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java index 02acd5d0..98f1ef6f 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java @@ -4,13 +4,15 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import tech.powerjob.common.OmsConstant; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.common.constants.ContainerSourceType; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.utils.OmsFileUtils; import tech.powerjob.server.core.container.ContainerService; import tech.powerjob.server.core.container.ContainerTemplateGenerator; @@ -39,22 +41,25 @@ import java.util.stream.Collectors; @RequestMapping("/container") public class ContainerController { - - private final int port; - private final ContainerService containerService; private final AppInfoRepository appInfoRepository; private final ContainerInfoRepository containerInfoRepository; - public ContainerController(@Value("${server.port}") int port, ContainerService containerService, AppInfoRepository appInfoRepository, ContainerInfoRepository containerInfoRepository) { - this.port = port; + public ContainerController(ContainerService containerService, AppInfoRepository appInfoRepository, ContainerInfoRepository containerInfoRepository) { this.containerService = containerService; this.appInfoRepository = appInfoRepository; this.containerInfoRepository = containerInfoRepository; } + /** + * 暴露给 worker 的下载端口,制品本身 version 不可枚举,不单独鉴权 + * 如果对此有安全性需求,可自行实现加密鉴权逻辑,或者干脆走自己的下载通道下载制品 + * @param version 容器版本 + * @param response 响应 + * @throws IOException 异常 + */ @GetMapping("/downloadJar") public void downloadJar(String version, HttpServletResponse response) throws IOException { File file = containerService.fetchContainerJarFile(version); @@ -66,12 +71,14 @@ public class ContainerController { } @PostMapping("/downloadContainerTemplate") + @ApiPermission(name = "Container-DownloadContainerTemplate", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public void downloadContainerTemplate(@RequestBody GenerateContainerTemplateRequest req, HttpServletResponse response) throws IOException { File zipFile = ContainerTemplateGenerator.generate(req.getGroup(), req.getArtifact(), req.getName(), req.getPackageName(), req.getJavaVersion()); OmsFileUtils.file2HttpResponse(zipFile, response); } @PostMapping("/jarUpload") + @ApiPermission(name = "Container-JarUpload", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO fileUpload(@RequestParam("file") MultipartFile file) throws Exception { if (file == null || file.isEmpty()) { return ResultDTO.failed("empty file"); @@ -80,6 +87,7 @@ public class ContainerController { } @PostMapping("/save") + @ApiPermission(name = "Container-Save", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO saveContainer(@RequestBody SaveContainerInfoRequest request) { request.valid(); @@ -93,12 +101,14 @@ public class ContainerController { } @GetMapping("/delete") + @ApiPermission(name = "Container-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO deleteContainer(Long appId, Long containerId) { containerService.delete(appId, containerId); return ResultDTO.success(null); } @GetMapping("/list") + @ApiPermission(name = "Container-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> listContainers(Long appId) { List res = containerInfoRepository.findByAppIdAndStatusNot(appId, SwitchableStatus.DELETED.getV()) .stream().map(ContainerController::convert).collect(Collectors.toList()); @@ -106,6 +116,7 @@ public class ContainerController { } @GetMapping("/listDeployedWorker") + @ApiPermission(name = "Container-ListDeployedWorker", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO listDeployedWorker(Long appId, Long containerId, HttpServletResponse response) { AppInfoDO appInfoDO = appInfoRepository.findById(appId).orElseThrow(() -> new IllegalArgumentException("can't find app by id:" + appId)); String targetServer = appInfoDO.getCurrentServer(); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java index 56ff5ea0..fb1cfa00 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java @@ -3,6 +3,9 @@ package tech.powerjob.server.web.controller; import tech.powerjob.common.OmsConstant; import tech.powerjob.common.enums.InstanceStatus; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.common.utils.OmsFileUtils; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.StringPage; @@ -46,8 +49,6 @@ import java.util.stream.Collectors; @RequestMapping("/instance") public class InstanceController { - - @Resource private InstanceService instanceService; @Resource @@ -59,18 +60,21 @@ public class InstanceController { private InstanceInfoRepository instanceInfoRepository; @GetMapping("/stop") + @ApiPermission(name = "Instance-Stop", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO stopInstance(Long appId,Long instanceId) { instanceService.stopInstance(appId,instanceId); return ResultDTO.success(null); } @GetMapping("/retry") + @ApiPermission(name = "Instance-Retry", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO retryInstance(String appId, Long instanceId) { instanceService.retryInstance(Long.valueOf(appId), instanceId); return ResultDTO.success(null); } @GetMapping("/detail") + @ApiPermission(name = "Instance-Detail", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO getInstanceDetail(Long appId, Long instanceId) { QueryInstanceDetailRequest queryInstanceDetailRequest = new QueryInstanceDetailRequest(); queryInstanceDetailRequest.setAppId(appId); @@ -97,11 +101,13 @@ public class InstanceController { } @GetMapping("/log") + @ApiPermission(name = "Instance-Log", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO getInstanceLog(Long appId, Long instanceId, Long index) { return ResultDTO.success(instanceLogService.fetchInstanceLog(appId, instanceId, index)); } @GetMapping("/downloadLogUrl") + @ApiPermission(name = "Instance-FetchDownloadLogUrl", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO getDownloadUrl(Long appId, Long instanceId) { return ResultDTO.success(instanceLogService.fetchDownloadUrl(appId, instanceId)); } @@ -133,6 +139,7 @@ public class InstanceController { } @PostMapping("/list") + @ApiPermission(name = "Instance-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> list(@RequestBody QueryInstanceRequest request) { Sort sort = Sort.by(Sort.Direction.DESC, "gmtModified"); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java index aaa3a9f1..74431895 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java @@ -3,7 +3,10 @@ package tech.powerjob.server.web.controller; import org.apache.commons.lang3.StringUtils; import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.remote.model.JobInfoDO; import tech.powerjob.server.persistence.remote.repository.JobInfoRepository; @@ -39,39 +42,46 @@ public class JobController { private JobInfoRepository jobInfoRepository; @PostMapping("/save") + @ApiPermission(name = "Job-Save", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO saveJobInfo(@RequestBody SaveJobInfoRequest request) { jobService.saveJob(request); return ResultDTO.success(null); } @PostMapping("/copy") + @ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO copyJob(String jobId) { return ResultDTO.success(JobInfoVO.from(jobService.copyJob(Long.valueOf(jobId)))); } @GetMapping("/export") + @ApiPermission(name = "Job-Export", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO exportJob(String jobId) { return ResultDTO.success(jobService.exportJob(Long.valueOf(jobId))); } @GetMapping("/disable") + @ApiPermission(name = "Job-Disable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO disableJob(String jobId) { jobService.disableJob(Long.valueOf(jobId)); return ResultDTO.success(null); } @GetMapping("/delete") + @ApiPermission(name = "Job-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO deleteJob(String jobId) { jobService.deleteJob(Long.valueOf(jobId)); return ResultDTO.success(null); } @GetMapping("/run") + @ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO runImmediately(String appId, String jobId, @RequestParam(required = false) String instanceParams) { return ResultDTO.success(jobService.runJob(Long.valueOf(appId), Long.valueOf(jobId), instanceParams, 0L)); } @PostMapping("/list") + @ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> listJobs(@RequestBody QueryJobInfoRequest request) { Sort sort = Sort.by(Sort.Direction.ASC, "id"); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java new file mode 100644 index 00000000..11cbcd9e --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java @@ -0,0 +1,117 @@ +package tech.powerjob.server.web.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.plugin.ModifyOrCreateDynamicPermission; +import tech.powerjob.server.auth.plugin.SaveNamespaceGrantPermissionPlugin; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.persistence.PageResult; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.converter.NamespaceConverter; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; +import tech.powerjob.server.web.response.NamespaceBaseVO; +import tech.powerjob.server.web.response.NamespaceVO; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.service.NamespaceWebService; +import tech.powerjob.server.web.service.UserWebService; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 命名空间 Controller + * + * @author tjq + * @since 2023/9/3 + */ +@Slf4j +@RestController +@RequestMapping("/namespace") +public class NamespaceController { + + @Resource + private WebAuthService webAuthService; + @Resource + private UserWebService userWebService; + @Resource + private NamespaceWebService namespaceWebService; + + @ResponseBody + @PostMapping("/save") + @ApiPermission(name = "Namespace-Save", roleScope = RoleScope.NAMESPACE, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveNamespaceGrantPermissionPlugin.class) + public ResultDTO save(@RequestBody ModifyNamespaceRequest req) { + + NamespaceDO savedNamespace = namespaceWebService.save(req); + return ResultDTO.success(NamespaceConverter.do2BaseVo(savedNamespace)); + } + + @DeleteMapping("/delete") + @ApiPermission(name = "Namespace-Delete", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.SU) + public ResultDTO deleteNamespace(Long id) { + namespaceWebService.delete(id); + return ResultDTO.success(null); + } + + @PostMapping("/list") + @ApiPermission(name = "Namespace-List", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE) + public ResultDTO> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) { + + Page namespacePageResult = namespaceWebService.list(queryNamespaceRequest); + + PageResult ret = new PageResult<>(namespacePageResult); + ret.setData(namespacePageResult.get().map(x -> { + NamespaceVO detailVo = new NamespaceVO(); + NamespaceBaseVO baseVO = NamespaceConverter.do2BaseVo(x); + BeanUtils.copyProperties(baseVO, detailVo); + + fillDetail(x, detailVo); + return detailVo; + }).collect(Collectors.toList())); + + return ResultDTO.success(ret); + } + + @PostMapping("/listAll") + @ApiPermission(name = "Namespace-ListAll", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE) + public ResultDTO> listAll() { + // 数量应该不是很多,先简单处理,不查询精简对象 + List namespaceRepositoryAll = namespaceWebService.listAll(); + List namespaceBaseVOList = namespaceRepositoryAll.stream().map(nd -> { + NamespaceBaseVO nv = new NamespaceBaseVO(); + nv.setId(nd.getId()); + nv.setCode(nd.getCode()); + nv.setName(nd.getName()); + nv.genShowName(); + return nv; + }).collect(Collectors.toList()); + return ResultDTO.success(namespaceBaseVOList); + } + + private void fillDetail(NamespaceDO namespaceDO, NamespaceVO namespaceVO) { + + Long namespaceId = namespaceVO.getId(); + + // 权限用户关系 + ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.NAMESPACE, namespaceId); + namespaceVO.setComponentUserRoleInfo(componentUserRoleInfo); + + // 有权限用户填充 token + boolean hasPermission = webAuthService.hasPermission(RoleScope.NAMESPACE, namespaceId, Permission.READ); + namespaceVO.setToken(hasPermission ? namespaceDO.getToken() : AuthConstants.TIPS_NO_PERMISSION_TO_SEE); + + // 用户信息 + namespaceVO.setCreatorShowName(userWebService.fetchBaseUserInfo(namespaceDO.getCreator()).map(UserBaseVO::getShowName).orElse(null)); + namespaceVO.setModifierShowName(userWebService.fetchBaseUserInfo(namespaceDO.getModifier()).map(UserBaseVO::getShowName).orElse(null)); + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java new file mode 100644 index 00000000..af397b9e --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java @@ -0,0 +1,45 @@ +package tech.powerjob.server.web.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; + +/** + * PowerJob 自带的登录体系 + * (同样视为第三方服务,与主框架没有任何关系) + * + * @author tjq + * @since 2024/2/13 + */ +@RestController +@RequestMapping("/pwjbUser") +public class PwjbUserInfoController { + + @Resource + private PwjbUserWebService pwjbUserWebService; + + /** + * 创建第三方登录体系(PowerJob) 的账户,不允许修改 + * @param request 请求(此处复用了主框架请求,便于用户一次性把所有参数都填入) + * @return 创建结果 + */ + @PostMapping("/create") + public ResultDTO save(@RequestBody ModifyUserInfoRequest request) { + pwjbUserWebService.save(request); + return ResultDTO.success(null); + } + + @PostMapping("/changePassword") + public ResultDTO changePassword(@RequestBody ChangePasswordRequest changePasswordRequest) { + + pwjbUserWebService.changePassword(changePasswordRequest); + return ResultDTO.success(null); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java index 5864fecc..fc180187 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java @@ -10,8 +10,13 @@ import org.springframework.web.bind.annotation.RestController; import tech.powerjob.common.OmsConstant; import tech.powerjob.common.enums.InstanceStatus; import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.common.module.WorkerInfo; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; import tech.powerjob.server.persistence.remote.repository.InstanceInfoRepository; import tech.powerjob.server.persistence.remote.repository.JobInfoRepository; import tech.powerjob.server.remote.server.self.ServerInfoService; @@ -21,6 +26,7 @@ import tech.powerjob.server.web.response.WorkerStatusVO; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.TimeZone; import java.util.stream.Collectors; @@ -36,6 +42,8 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class SystemInfoController { + private final AppInfoRepository appInfoRepository; + private final JobInfoRepository jobInfoRepository; private final InstanceInfoRepository instanceInfoRepository; @@ -45,6 +53,7 @@ public class SystemInfoController { private final WorkerClusterQueryService workerClusterQueryService; @GetMapping("/listWorker") + @ApiPermission(name = "System-ListWorker", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> listWorker(Long appId) { List workerInfos = workerClusterQueryService.getAllWorkers(appId); @@ -52,10 +61,19 @@ public class SystemInfoController { } @GetMapping("/overview") + @ApiPermission(name = "System-Overview", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO getSystemOverview(Long appId) { SystemOverviewVO overview = new SystemOverviewVO(); + Optional appInfoOpt = appInfoRepository.findById(appId); + if (appInfoOpt.isPresent()) { + AppInfoDO appInfo = appInfoOpt.get(); + + overview.setAppId(appId); + overview.setAppName(appInfo.getAppName()); + } + // 总任务数量 overview.setJobCount(jobInfoRepository.countByAppIdAndStatusNot(appId, SwitchableStatus.DELETED.getV())); // 运行任务数 @@ -69,7 +87,8 @@ public class SystemInfoController { // 服务器时间 overview.setServerTime(DateFormatUtils.format(new Date(), OmsConstant.TIME_PATTERN)); - overview.setServerInfo(serverInfoService.fetchServiceInfo()); + overview.setWebServerInfo(serverInfoService.fetchCurrentServerInfo()); + overview.setScheduleServerInfo(serverInfoService.fetchAppServerInfo(appId)); return ResultDTO.success(overview); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java index 029c653f..8dcb6687 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java @@ -1,21 +1,45 @@ package tech.powerjob.server.web.controller; -import tech.powerjob.common.response.ResultDTO; -import org.springframework.beans.BeanUtils; -import tech.powerjob.server.persistence.remote.model.UserInfoDO; -import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; -import tech.powerjob.server.core.service.UserService; -import tech.powerjob.server.web.request.ModifyUserInfoRequest; import com.google.common.collect.Lists; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.util.CollectionUtils; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.common.enums.SwitchableStatus; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; +import tech.powerjob.server.web.converter.NamespaceConverter; +import tech.powerjob.server.web.converter.UserConverter; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.request.QueryUserRequest; +import tech.powerjob.server.web.response.AppBaseVO; +import tech.powerjob.server.web.response.NamespaceBaseVO; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.response.UserDetailVO; +import tech.powerjob.server.web.service.UserWebService; import javax.annotation.Resource; -import java.util.List; +import javax.servlet.http.HttpServletRequest; +import java.util.*; import java.util.stream.Collectors; /** @@ -24,25 +48,64 @@ import java.util.stream.Collectors; * @author tjq * @since 2020/4/12 */ + +@Slf4j @RestController @RequestMapping("/user") public class UserInfoController { @Resource - private UserService userService; + private UserWebService userWebService; @Resource private UserInfoRepository userInfoRepository; + @Resource + private PowerJobLoginService powerJobLoginService; + @Resource + private WebAuthService webAuthService; + @Resource + private NamespaceRepository namespaceRepository; + @Resource + private AppInfoRepository appInfoRepository; + + @SneakyThrows + @PostMapping("/modify") + public ResultDTO modifyUser(@RequestBody ModifyUserInfoRequest modifyUserInfoRequest, HttpServletRequest httpServletRequest) { + + Long userId = modifyUserInfoRequest.getId(); + checkModifyUserPermission(userId, httpServletRequest); + + Optional userOpt = userInfoRepository.findById(userId); + if (!userOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by userId:" + userId); + } + + UserInfoDO dbUser = userOpt.get(); + + // 拷入允许修改的内容 + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getNick())) { + dbUser.setNick(modifyUserInfoRequest.getNick()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getPhone())) { + dbUser.setPhone(modifyUserInfoRequest.getPhone()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getEmail())) { + dbUser.setEmail(modifyUserInfoRequest.getEmail()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getWebHook())) { + dbUser.setWebHook(modifyUserInfoRequest.getWebHook()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getExtra())) { + dbUser.setExtra(modifyUserInfoRequest.getExtra()); + } + + dbUser.setGmtModified(new Date()); + userInfoRepository.saveAndFlush(dbUser); - @PostMapping("save") - public ResultDTO save(@RequestBody ModifyUserInfoRequest request) { - UserInfoDO userInfoDO = new UserInfoDO(); - BeanUtils.copyProperties(request, userInfoDO); - userService.save(userInfoDO); return ResultDTO.success(null); } - @GetMapping("list") - public ResultDTO> list(@RequestParam(required = false) String name) { + @GetMapping("/list") + public ResultDTO> list(@RequestParam(required = false) String name) { List result; if (StringUtils.isEmpty(name)) { @@ -53,18 +116,140 @@ public class UserInfoController { return ResultDTO.success(convert(result)); } - private static List convert(List data) { + /** + * 查询用户信息(用于管理员操作,会返回敏感信息) + * @param queryUserRequest 查询请求 + * @return 响应 + */ + @PostMapping("/query") + @ApiPermission(name = "User-Query", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.SU) + public ResultDTO> query(@RequestBody QueryUserRequest queryUserRequest) { + List userInfoDos = userWebService.list(queryUserRequest); + List userBaseVOS = userInfoDos.stream().map(x -> UserConverter.do2BaseVo(x, true)).collect(Collectors.toList()); + return ResultDTO.success(userBaseVOS); + } + + @GetMapping("/detail") + public ResultDTO getUserDetail(HttpServletRequest httpServletRequest) { + Optional powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest); + if (!powerJobUserOpt.isPresent()) { + throw new PowerJobAuthException(ErrorCodes.USER_NOT_LOGIN); + } + Optional userinfoDoOpt = userInfoRepository.findById(powerJobUserOpt.get().getId()); + if (!userinfoDoOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by id: " + powerJobUserOpt.get().getId()); + } + UserDetailVO userDetailVO = new UserDetailVO(); + BeanUtils.copyProperties(userinfoDoOpt.get(), userDetailVO); + userDetailVO.genShowName(); + + // 权限信息 + Map> globalPermissions = webAuthService.fetchMyPermissionTargets(RoleScope.GLOBAL); + userDetailVO.setGlobalRoles(globalPermissions.keySet().stream().map(Enum::name).collect(Collectors.toList())); + + Map> namespacePermissions = webAuthService.fetchMyPermissionTargets(RoleScope.NAMESPACE); + List nsList = namespaceRepository.findAllByIdIn(mergeIds(namespacePermissions)); + Map id2NamespaceDo = Maps.newHashMap(); + nsList.forEach(x -> id2NamespaceDo.put(x.getId(), x)); + Map> role2NamespaceBaseVo = Maps.newHashMap(); + namespacePermissions.forEach((k, v) -> { + List namespaceBaseVOS = Lists.newArrayList(); + role2NamespaceBaseVo.put(k.name(), namespaceBaseVOS); + v.forEach(nId -> { + NamespaceDO namespaceDO = id2NamespaceDo.get(nId); + if (namespaceDO == null) { + return; + } + NamespaceBaseVO namespaceBaseVO = NamespaceConverter.do2BaseVo(namespaceDO); + namespaceBaseVOS.add(namespaceBaseVO); + }); + }); + userDetailVO.setRole2NamespaceList(role2NamespaceBaseVo); + + Map> appPermissions = webAuthService.fetchMyPermissionTargets(RoleScope.APP); + List appList = appInfoRepository.findAllByIdIn(mergeIds(appPermissions)); + Map id2AppInfo = Maps.newHashMap(); + appList.forEach(x -> id2AppInfo.put(x.getId(), x)); + Map> role2AppBaseVo = Maps.newHashMap(); + appPermissions.forEach((k, v) -> { + List appBaseVOS = Lists.newArrayList(); + role2AppBaseVo.put(k.name(), appBaseVOS); + v.forEach(nId -> { + AppInfoDO appInfoDO = id2AppInfo.get(nId); + if (appInfoDO == null) { + return; + } + AppBaseVO appBaseVO = new AppBaseVO(); + BeanUtils.copyProperties(appInfoDO, appBaseVO); + appBaseVOS.add(appBaseVO); + }); + }); + userDetailVO.setRole2AppList(role2AppBaseVo); + + return ResultDTO.success(userDetailVO); + } + + @PostMapping("/disable") + public ResultDTO disableUser(Long uid, HttpServletRequest httpServletRequest) { + changeAccountStatus(uid, SwitchableStatus.DISABLE, httpServletRequest); + return ResultDTO.success(null); + } + + @PostMapping("/enable") + public ResultDTO enableUser(Long uid, HttpServletRequest httpServletRequest) { + changeAccountStatus(uid, SwitchableStatus.ENABLE, httpServletRequest); + return ResultDTO.success(null); + } + + private void changeAccountStatus(Long uid, SwitchableStatus targetStatus, HttpServletRequest httpServletRequest) { + checkModifyUserPermission(uid, httpServletRequest); + + Optional userOpt = userInfoRepository.findById(uid); + if (!userOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by userId:" + uid); + } + + UserInfoDO dbUser = userOpt.get(); + + dbUser.setStatus(targetStatus.getV()); + dbUser.setGmtModified(new Date()); + + userInfoRepository.saveAndFlush(dbUser); + log.info("[UserInfoController] changeAccountStatus, userId={},targetStatus={}", uid, targetStatus); + } + + /** + * 检查针对 user 处理的权限 + * @param uid 目标 userId + * @param httpServletRequest http 上下文请求 + */ + private void checkModifyUserPermission(Long uid, HttpServletRequest httpServletRequest) { + Optional powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest); + if (!powerJobUserOpt.isPresent()) { + throw new PowerJobAuthException(ErrorCodes.USER_NOT_LOGIN); + } + PowerJobUser currentLoginUser = powerJobUserOpt.get(); + + boolean myself = uid.equals(currentLoginUser.getId()); + boolean globalAdmin = webAuthService.isGlobalAdmin(); + + if (myself || globalAdmin) { + return; + } + + throw new PowerJobException("Only the administrator and account owner can modify the account"); + } + + private static List convert(List data) { if (CollectionUtils.isEmpty(data)) { return Lists.newLinkedList(); } - return data.stream().map(x -> new UserItemVO(x.getId(), x.getUsername())).collect(Collectors.toList()); + return data.stream().map(x -> UserConverter.do2BaseVo(x, false)).collect(Collectors.toList()); } - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static final class UserItemVO { - private Long id; - private String username; + private static Set mergeIds(Map> map) { + Set ids = Sets.newHashSet(); + map.values().forEach(ids::addAll); + return ids; } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java index c78c6965..09e735e4 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java @@ -1,26 +1,28 @@ package tech.powerjob.server.web.controller; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.web.bind.annotation.*; import tech.powerjob.common.request.http.SaveWorkflowNodeRequest; import tech.powerjob.common.request.http.SaveWorkflowRequest; import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.common.enums.SwitchableStatus; +import tech.powerjob.server.core.workflow.WorkflowService; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.remote.model.WorkflowInfoDO; import tech.powerjob.server.persistence.remote.model.WorkflowNodeInfoDO; import tech.powerjob.server.persistence.remote.repository.WorkflowInfoRepository; -import tech.powerjob.server.core.workflow.WorkflowService; import tech.powerjob.server.web.request.QueryWorkflowInfoRequest; import tech.powerjob.server.web.response.WorkflowInfoVO; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.apache.commons.lang3.StringUtils; -import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.text.ParseException; import java.util.List; -import java.util.function.LongToDoubleFunction; import java.util.stream.Collectors; /** @@ -40,34 +42,40 @@ public class WorkflowController { private WorkflowInfoRepository workflowInfoRepository; @PostMapping("/save") + @ApiPermission(name = "Workflow-Save", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO save(@RequestBody SaveWorkflowRequest req) throws ParseException { return ResultDTO.success(workflowService.saveWorkflow(req)); } @PostMapping("/copy") + @ApiPermission(name = "Workflow-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO copy(Long workflowId, Long appId) { return ResultDTO.success(workflowService.copyWorkflow(workflowId,appId)); } @GetMapping("/disable") + @ApiPermission(name = "Workflow-Disable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO disableWorkflow(Long workflowId, Long appId) { workflowService.disableWorkflow(workflowId, appId); return ResultDTO.success(null); } @GetMapping("/enable") + @ApiPermission(name = "Workflow-Enable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO enableWorkflow(Long workflowId, Long appId) { workflowService.enableWorkflow(workflowId, appId); return ResultDTO.success(null); } @GetMapping("/delete") + @ApiPermission(name = "Workflow-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO deleteWorkflow(Long workflowId, Long appId) { workflowService.deleteWorkflow(workflowId, appId); return ResultDTO.success(null); } @PostMapping("/list") + @ApiPermission(name = "Workflow-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> list(@RequestBody QueryWorkflowInfoRequest req) { Sort sort = Sort.by(Sort.Direction.DESC, "gmtCreate"); @@ -89,6 +97,7 @@ public class WorkflowController { } @GetMapping("/run") + @ApiPermission(name = "Workflow-Run", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO runWorkflow(Long workflowId, Long appId, @RequestParam(required = false,defaultValue = "0") Long delay, @RequestParam(required = false) String initParams @@ -97,12 +106,14 @@ public class WorkflowController { } @GetMapping("/fetch") + @ApiPermission(name = "Workflow-Fetch", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO fetchWorkflow(Long workflowId, Long appId) { WorkflowInfoDO workflowInfoDO = workflowService.fetchWorkflow(workflowId, appId); return ResultDTO.success(WorkflowInfoVO.from(workflowInfoDO)); } @PostMapping("/saveNode") + @ApiPermission(name = "Workflow-SaveNode", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO> addWorkflowNode(@RequestBody List request) { return ResultDTO.success(workflowService.saveWorkflowNode(request)); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java index 944737dd..0f80016d 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java @@ -2,6 +2,9 @@ package tech.powerjob.server.web.controller; import tech.powerjob.common.enums.WorkflowInstanceStatus; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.remote.model.WorkflowInstanceInfoDO; import tech.powerjob.server.persistence.remote.repository.WorkflowInstanceInfoRepository; @@ -38,18 +41,21 @@ public class WorkflowInstanceController { private WorkflowInstanceInfoRepository workflowInstanceInfoRepository; @GetMapping("/stop") + @ApiPermission(name = "WorkflowInstance-Stop", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO stopWfInstance(Long wfInstanceId, Long appId) { workflowInstanceService.stopWorkflowInstanceEntrance(wfInstanceId, appId); return ResultDTO.success(null); } @RequestMapping("/retry") + @ApiPermission(name = "WorkflowInstance-Retry", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO retryWfInstance(Long wfInstanceId, Long appId) { workflowInstanceService.retryWorkflowInstance(wfInstanceId, appId); return ResultDTO.success(null); } @RequestMapping("/markNodeAsSuccess") + @ApiPermission(name = "WorkflowInstance-MarkNodeAsSuccess", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO markNodeAsSuccess(Long wfInstanceId, Long appId, Long nodeId) { workflowInstanceService.markNodeAsSuccess(appId, wfInstanceId, nodeId); return ResultDTO.success(null); @@ -57,12 +63,14 @@ public class WorkflowInstanceController { @GetMapping("/info") + @ApiPermission(name = "WorkflowInstance-Info", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO getInfo(Long wfInstanceId, Long appId) { WorkflowInstanceInfoDO wfInstanceDO = workflowInstanceService.fetchWfInstance(wfInstanceId, appId); return ResultDTO.success(WorkflowInstanceInfoVO.from(wfInstanceDO, cacheService.getWorkflowName(wfInstanceDO.getWorkflowId()))); } @PostMapping("/list") + @ApiPermission(name = "WorkflowInstance-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> listWfInstance(@RequestBody QueryWorkflowInstanceRequest req) { Sort sort = Sort.by(Sort.Direction.DESC, "gmtModified"); PageRequest pageable = PageRequest.of(req.getIndex(), req.getPageSize(), sort); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java new file mode 100644 index 00000000..ed2852c3 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java @@ -0,0 +1,30 @@ +package tech.powerjob.server.web.converter; + +import org.springframework.beans.BeanUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.common.enums.SwitchableStatus; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.response.NamespaceBaseVO; + +/** + * NamespaceConverter + * + * @author tjq + * @since 2023/9/4 + */ +public class NamespaceConverter { + + public static NamespaceBaseVO do2BaseVo(NamespaceDO d) { + NamespaceBaseVO v = new NamespaceBaseVO(); + + BeanUtils.copyProperties(d, v); + + v.setGmtCreateStr(CommonUtils.formatTime(d.getGmtCreate())); + v.setGmtModifiedStr(CommonUtils.formatTime(d.getGmtModified())); + v.setStatusStr(SwitchableStatus.of(d.getStatus()).name()); + + v.genShowName(); + return v; + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/UserConverter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/UserConverter.java new file mode 100644 index 00000000..283ba46f --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/UserConverter.java @@ -0,0 +1,37 @@ +package tech.powerjob.server.web.converter; + +import tech.powerjob.common.enums.SwitchableStatus; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.web.response.UserBaseVO; + +import java.util.Optional; + +/** + * UserConverter + * + * @author tjq + * @since 2023/9/4 + */ +public class UserConverter { + + public static UserBaseVO do2BaseVo(UserInfoDO x, boolean includeSensitiveInfo) { + + UserBaseVO userBaseVO = new UserBaseVO(); + + userBaseVO.setId(x.getId()); + userBaseVO.setAccountType(x.getAccountType()); + userBaseVO.setUsername(x.getUsername()); + userBaseVO.setNick(x.getNick()); + userBaseVO.setStatus(Optional.ofNullable(x.getStatus()).orElse(SwitchableStatus.ENABLE.getV())); + userBaseVO.setEnable(userBaseVO.getStatus() == SwitchableStatus.ENABLE.getV()); + + if (includeSensitiveInfo) { + userBaseVO.setPhone(x.getPhone()); + userBaseVO.setEmail(x.getEmail()); + } + + userBaseVO.genShowName(); + return userBaseVO; + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/AppAssertRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/AppAssertRequest.java index 826d99ad..bb41c473 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/AppAssertRequest.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/AppAssertRequest.java @@ -12,4 +12,6 @@ import lombok.Data; public class AppAssertRequest { private String appName; private String password; + + private String encryptType; } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ChangePasswordRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ChangePasswordRequest.java new file mode 100644 index 00000000..60b5ddb7 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ChangePasswordRequest.java @@ -0,0 +1,23 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 修改密码 + * + * @author tjq + * @since 2024/2/13 + */ +@Data +public class ChangePasswordRequest implements Serializable { + + private String username; + + private String oldPassword; + + private String newPassword; + + private String newPassword2; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ComponentUserRoleInfo.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ComponentUserRoleInfo.java new file mode 100644 index 00000000..0b2fd925 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ComponentUserRoleInfo.java @@ -0,0 +1,34 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 组件上的用户角色信息 + * + * @author tjq + * @since 2024/2/12 + */ +@Data +@Accessors(chain = true) +public class ComponentUserRoleInfo { + /** + * 观察者 + */ + private List observer; + /** + * 测试 + */ + private List qa; + /** + * 开发者 + */ + private List developer; + /** + * 管理员 + */ + private List admin; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/GrantPermissionRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/GrantPermissionRequest.java new file mode 100644 index 00000000..cc2b71d6 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/GrantPermissionRequest.java @@ -0,0 +1,31 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 授权请求 + * + * @author tjq + * @since 2024/2/12 + */ +@Data +public class GrantPermissionRequest implements Serializable { + + /** + * 目标ID + */ + private Long targetId; + + /** + * 授予的角色 + */ + private Integer role; + + /** + * 授予的用户IDS + */ + private List userIds; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java index 2b5aca6f..4669cf62 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java @@ -15,14 +15,37 @@ import org.apache.commons.lang3.StringUtils; public class ModifyAppInfoRequest { private Long id; - private String oldPassword; private String appName; + + private Long namespaceId; + + private String oldPassword; private String password; + /** + * 描述 + */ + private String title; + + /** + * 管理标签 + */ + private String tags; + /** + * 扩展字段 + */ + private String extra; + + private ComponentUserRoleInfo componentUserRoleInfo; + public void valid() { CommonUtils.requireNonNull(appName, "appName can't be empty"); if (StringUtils.containsWhitespace(appName)) { throw new PowerJobException("appName can't contains white space!"); } + CommonUtils.requireNonNull(password, "password can't be empty"); + + // 后续版本强制要求设置 namespace,方便统一管理 + CommonUtils.requireNonNull(namespaceId, "namespace can't be empty"); } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java new file mode 100644 index 00000000..1edb7eb3 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java @@ -0,0 +1,53 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.utils.CommonUtils; + +/** + * ModifyNamespaceRequest + * + * @author tjq + * @since 2023/9/3 + */ +@Data +public class ModifyNamespaceRequest { + + private Long id; + + /** + * 空间唯一标识 + */ + private String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + private String name; + + private String dept; + + /** + * 标签,扩展性之王,多值逗号分割 + */ + private String tags; + + private Integer status; + + /** + * 扩展字段 + */ + private String extra; + /** + * 权限表单 + */ + private ComponentUserRoleInfo componentUserRoleInfo; + + public void valid() { + CommonUtils.requireNonNull(code, "namespace code can't be empty"); + if (StringUtils.containsWhitespace(code)) { + throw new PowerJobException("namespace code can't contains white space!"); + } + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java index db43e346..428f5c84 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java @@ -14,6 +14,7 @@ public class ModifyUserInfoRequest { private Long id; private String username; + private String nick; private String password; private String webHook; @@ -25,4 +26,6 @@ public class ModifyUserInfoRequest { * 邮箱地址 */ private String email; + + private String extra; } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java new file mode 100644 index 00000000..44f2886a --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java @@ -0,0 +1,44 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +/** + * 查询应用信息 + * + * @author tjq + * @since 2024/2/11 + */ +@Data +public class QueryAppInfoRequest { + + /** + * appId 精确查旋 + */ + private Long appId; + /** + * namespaceId + */ + private Long namespaceId; + /** + * 任务名称 + */ + private String appNameLike; + + private String tagLike; + + /** + * 查询与我相关的任务(我有直接权限的) + */ + private Boolean showMyRelated; + + /* ****************** 分页参数 ****************** */ + /** + * 当前页码 + */ + private Integer index; + /** + * 页大小 + */ + private Integer pageSize; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java new file mode 100644 index 00000000..57f4d8f3 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java @@ -0,0 +1,35 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +/** + * 查询 namespace 请求 + * + * @author tjq + * @since 2024/2/11 + */ +@Data +public class QueryNamespaceRequest { + + /** + * code 模糊查询 + */ + private String codeLike; + + /** + * 名称模糊查询 + */ + private String nameLike; + + private String tagLike; + + /* ****************** 分页参数 ****************** */ + /** + * 当前页码 + */ + private Integer index = 0; + /** + * 页大小 + */ + private Integer pageSize = 10; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryUserRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryUserRequest.java new file mode 100644 index 00000000..cbb90ab5 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryUserRequest.java @@ -0,0 +1,32 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户查询请求 + * + * @author tjq + * @since 2024/3/16 + */ +@Data +public class QueryUserRequest implements Serializable { + + /** + * 通过 userId 精确查询 + */ + private Long userIdEq; + + private String accountTypeEq; + + /** + * nick 模糊查询 + */ + private String nickLike; + + /** + * 手机号模糊查询 + */ + private String phoneLike; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/SaveContainerInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/SaveContainerInfoRequest.java index aa734c14..ae812cc2 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/SaveContainerInfoRequest.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/SaveContainerInfoRequest.java @@ -2,7 +2,7 @@ package tech.powerjob.server.web.request; import tech.powerjob.common.utils.CommonUtils; import tech.powerjob.server.common.constants.ContainerSourceType; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import lombok.Data; /** diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppBaseVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppBaseVO.java new file mode 100644 index 00000000..2c0f778f --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppBaseVO.java @@ -0,0 +1,27 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +/** + * AppBaseVO + * + * @author tjq + * @since 2024/2/13 + */ +@Getter +@Setter +public class AppBaseVO implements Serializable { + + protected Long id; + + protected String appName; + + protected Long namespaceId; + /** + * 描述 + */ + protected String title; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppInfoVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppInfoVO.java new file mode 100644 index 00000000..9e08f1f2 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppInfoVO.java @@ -0,0 +1,47 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import java.util.Date; + +/** + * AppInfoVO + * + * @author tjq + * @since 2024/2/12 + */ +@Getter +@Setter +@ToString +public class AppInfoVO extends AppBaseVO { + + private String password; + + private String tags; + + private String extra; + + private ComponentUserRoleInfo componentUserRoleInfo; + + private Date gmtCreate; + + private String gmtCreateStr; + + private Date gmtModified; + + private String gmtModifiedStr; + + private String creatorShowName; + + private String modifierShowName; + + /** + * Namespace Info + */ + private NamespaceBaseVO namespace; + + private String namespaceName; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/JobInfoVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/JobInfoVO.java index 61c3a084..1a2ec656 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/JobInfoVO.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/JobInfoVO.java @@ -13,7 +13,7 @@ import tech.powerjob.common.model.LifeCycle; import tech.powerjob.common.utils.CommonUtils; import tech.powerjob.server.common.SJ; import tech.powerjob.common.enums.DispatchStrategy; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.persistence.remote.model.JobInfoDO; import com.google.common.collect.Lists; import lombok.Data; diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java new file mode 100644 index 00000000..b7f872c5 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java @@ -0,0 +1,58 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.Date; + +/** + * namespace 基本 VO 对象,用于列表渲染 + * + * @author tjq + * @since 2024/2/12 + */ +@Getter +@Setter +public class NamespaceBaseVO implements Serializable { + + protected Long id; + + /** + * 空间唯一标识 + */ + protected String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + protected String name; + + private String dept; + private String tags; + + /** + * 扩展字段 + */ + private String extra; + + private Integer status; + private String statusStr; + + private Date gmtCreate; + + private String gmtCreateStr; + + private Date gmtModified; + + private String gmtModifiedStr; + + /** + * 前端名称(拼接 code + name,更容易辨认) + */ + protected String showName; + + public void genShowName() { + showName = String.format("%s(%s)", name, code); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceVO.java new file mode 100644 index 00000000..fcd7287b --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceVO.java @@ -0,0 +1,31 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +/** + * 基础版本的命名空间 + * + * @author tjq + * @since 2023/9/3 + */ +@Getter +@Setter +@ToString +public class NamespaceVO extends NamespaceBaseVO { + + /** + * 访问 token + * 仅拥有当前 namespace 权限的访问者可见 + */ + private String token; + + private ComponentUserRoleInfo componentUserRoleInfo; + + private String creatorShowName; + + private String modifierShowName; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java index 0f4f9526..178550d2 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java @@ -1,8 +1,6 @@ package tech.powerjob.server.web.response; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.Getter; import tech.powerjob.server.common.module.ServerInfo; /** @@ -14,6 +12,10 @@ import tech.powerjob.server.common.module.ServerInfo; @Data public class SystemOverviewVO { + private Long appId; + + private String appName; + private long jobCount; private long runningInstanceCount; private long failedInstanceCount; @@ -26,5 +28,12 @@ public class SystemOverviewVO { */ private String serverTime; - private ServerInfo serverInfo; + /** + * 处理当前 WEB 服务的 server 信息 + */ + private ServerInfo webServerInfo; + /** + * 调度服务器信息 + */ + private ServerInfo scheduleServerInfo; } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserBaseVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserBaseVO.java new file mode 100644 index 00000000..83b81648 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserBaseVO.java @@ -0,0 +1,58 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +/** + * 用户基础信息 + * + * @author tjq + * @since 2023/9/3 + */ +@Getter +@Setter +@NoArgsConstructor +public class UserBaseVO { + + protected Long id; + protected String username; + protected String nick; + + /** + * 账户类型 + */ + private String accountType; + + /** + * 手机号 + */ + private String phone; + /** + * 邮箱地址 + */ + private String email; + + /** + * 账号当前状态 + */ + private Integer status; + + private boolean enable; + + /** + * 前端展示名称,更容易辨认 + */ + protected String showName; + + public void genShowName() { + + if (StringUtils.isEmpty(nick)) { + showName = username; + } else { + showName = String.format("%s (%s)", nick, username); + } + } + +} \ No newline at end of file diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserDetailVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserDetailVO.java new file mode 100644 index 00000000..8cca9ff8 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserDetailVO.java @@ -0,0 +1,51 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; +import java.util.Map; + +/** + * 用户详细信息 + * + * @author tjq + * @since 2024/2/13 + */ +@Getter +@Setter +@ToString +public class UserDetailVO extends UserBaseVO { + + + /** + * 密码 + */ + private String password; + + /** + * webHook + */ + private String webHook; + + private String originUsername; + /** + * 扩展字段 + */ + private String extra; + + /** + * 拥有的全局权限 + */ + private List globalRoles; + /** + * 拥有的 namespace 权限 + */ + private Map> role2NamespaceList; + /** + * 拥有的 app 权限 + */ + private Map> role2AppList; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WorkflowInfoVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WorkflowInfoVO.java index bfdcd92c..09c74033 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WorkflowInfoVO.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WorkflowInfoVO.java @@ -6,7 +6,7 @@ import tech.powerjob.common.enums.TimeExpressionType; import tech.powerjob.common.model.LifeCycle; import tech.powerjob.common.model.PEWorkflowDAG; import tech.powerjob.server.common.SJ; -import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.common.enums.SwitchableStatus; import tech.powerjob.server.persistence.remote.model.WorkflowInfoDO; import lombok.Data; import org.springframework.beans.BeanUtils; diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java new file mode 100644 index 00000000..07f9d218 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java @@ -0,0 +1,28 @@ +package tech.powerjob.server.web.service; + +import org.springframework.data.domain.Page; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; + +import java.util.List; +import java.util.Optional; + +/** + * namespace web 服务 + * + * @author tjq + * @since 2024/2/15 + */ +public interface NamespaceWebService { + + NamespaceDO save(ModifyNamespaceRequest req); + + void delete(Long id); + + Optional findById(Long id); + + Page list(QueryNamespaceRequest queryNamespaceRequest); + + List listAll(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java new file mode 100644 index 00000000..36e6ea14 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java @@ -0,0 +1,18 @@ +package tech.powerjob.server.web.service; + +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; + +/** + * PwjbUserWebService + * + * @author tjq + * @since 2024/2/15 + */ +public interface PwjbUserWebService { + + PwjbUserInfoDO save(ModifyUserInfoRequest request); + + void changePassword(ChangePasswordRequest changePasswordRequest); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/UserWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/UserWebService.java new file mode 100644 index 00000000..4db42550 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/UserWebService.java @@ -0,0 +1,21 @@ +package tech.powerjob.server.web.service; + +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.web.request.QueryUserRequest; +import tech.powerjob.server.web.response.UserBaseVO; + +import java.util.List; +import java.util.Optional; + +/** + * 用户 WEB 服务 + * + * @author tjq + * @since 2024/2/17 + */ +public interface UserWebService { + + Optional fetchBaseUserInfo(Long userId); + + List list(QueryUserRequest queryUserRequest); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java new file mode 100644 index 00000000..e504bc81 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java @@ -0,0 +1,155 @@ +package tech.powerjob.server.web.service.impl; + +import com.google.common.collect.Lists; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.common.SJ; +import tech.powerjob.common.enums.SwitchableStatus; +import tech.powerjob.server.persistence.QueryConvertUtils; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; +import tech.powerjob.server.web.service.NamespaceWebService; + +import javax.annotation.Resource; +import javax.persistence.criteria.Predicate; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * NamespaceWebService + * + * @author tjq + * @since 2024/2/15 + */ +@Service +public class NamespaceWebServiceImpl implements NamespaceWebService { + + @Resource + private WebAuthService webAuthService; + @Resource + private AppInfoRepository appInfoRepository; + @Resource + private NamespaceRepository namespaceRepository; + + @Override + public NamespaceDO save(ModifyNamespaceRequest req) { + req.valid(); + + Long id = req.getId(); + NamespaceDO namespaceDO; + + boolean isCreate = id == null; + + if (isCreate) { + namespaceDO = new NamespaceDO(); + namespaceDO.setGmtCreate(new Date()); + + // code 单独拷贝 + namespaceDO.setCode(req.getCode()); + // 创建时生成 token + namespaceDO.setToken(UUID.randomUUID().toString()); + namespaceDO.setCreator(LoginUserHolder.getUserId()); + + } else { + namespaceDO = fetchById(id); + namespaceDO.setModifier(LoginUserHolder.getUserId()); + + if (!namespaceDO.getCode().equalsIgnoreCase(req.getCode())) { + throw new IllegalArgumentException("NOT_ALLOW_CHANGE_THE_NAMESPACE_CODE"); + } + } + + // 拷贝通用变更属性(code 不允许更改) + namespaceDO.setTags(req.getTags()); + namespaceDO.setName(req.getName()); + namespaceDO.setExtra(req.getExtra()); + namespaceDO.setStatus(Optional.ofNullable(req.getStatus()).orElse(SwitchableStatus.ENABLE.getV())); + + namespaceDO.setGmtModified(new Date()); + NamespaceDO savedNamespace = namespaceRepository.save(namespaceDO); + + // 授权 + webAuthService.processPermissionOnSave(RoleScope.NAMESPACE, savedNamespace.getId(), req.getComponentUserRoleInfo()); + + return savedNamespace; + } + + @Override + public void delete(Long id) { + List appInfosInNamespace = appInfoRepository.findAllByNamespaceId(id); + if (CollectionUtils.isNotEmpty(appInfosInNamespace)) { + List relatedApps = appInfosInNamespace.stream().map(AppInfoDO::getAppName).collect(Collectors.toList()); + throw new PowerJobException("Unable to delete due to associated apps: " + SJ.COMMA_JOINER.join(relatedApps)); + } + + namespaceRepository.deleteById(id); + } + + @Override + public Optional findById(Long id) { + if (id == null) { + return Optional.empty(); + } + return namespaceRepository.findById(id); + } + + @Override + public Page list(QueryNamespaceRequest queryNamespaceRequest) { + String codeLike = queryNamespaceRequest.getCodeLike(); + String nameLike = queryNamespaceRequest.getNameLike(); + String tagLike = queryNamespaceRequest.getTagLike(); + + Pageable pageable = PageRequest.of(queryNamespaceRequest.getIndex(), queryNamespaceRequest.getPageSize()); + Specification specification = (root, query, cb) -> { + + List predicates = Lists.newArrayList(); + + if (StringUtils.isNotEmpty(codeLike)) { + predicates.add(cb.like(root.get("code"), QueryConvertUtils.convertLikeParams(codeLike))); + } + + if (StringUtils.isNotEmpty(nameLike)) { + predicates.add(cb.like(root.get("name"), QueryConvertUtils.convertLikeParams(nameLike))); + } + if (StringUtils.isNotEmpty(tagLike)) { + predicates.add(cb.like(root.get("tags"), QueryConvertUtils.convertLikeParams(tagLike))); + } + + if (predicates.isEmpty()) { + return null; + } + return query.where(predicates.toArray(new Predicate[0])).getRestriction(); + }; + + return namespaceRepository.findAll(specification, pageable); + } + + @Override + public List listAll() { + return namespaceRepository.findAll(); + } + + private NamespaceDO fetchById(Long id) { + Optional namespaceDoOpt = namespaceRepository.findById(id); + if (!namespaceDoOpt.isPresent()) { + throw new IllegalArgumentException("can't find namespace by id: " + id); + } + return namespaceDoOpt.get(); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java new file mode 100644 index 00000000..356ff200 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java @@ -0,0 +1,99 @@ +package tech.powerjob.server.web.service.impl; + +import com.google.common.collect.Sets; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.common.enums.ErrorCodes; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.common.utils.DigestUtils; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.Optional; +import java.util.Set; + +/** + * PwjbUserWebService + * + * @author tjq + * @since 2024/2/15 + */ +@Service +public class PwjbUserWebServiceImplImpl implements PwjbUserWebService { + + @Resource + private PwjbUserInfoRepository pwjbUserInfoRepository; + + private static final Set NOT_ALLOWED_CHANGE_PASSWORD_ACCOUNTS = Sets.newHashSet("powerjob_trial_account"); + + @Override + @SneakyThrows + public PwjbUserInfoDO save(ModifyUserInfoRequest request) { + String username = request.getUsername(); + CommonUtils.requireNonNull(username, "userName can't be null or empty!"); + CommonUtils.requireNonNull(request.getPassword(), "password can't be null or empty!"); + + Optional oldUserOpt = pwjbUserInfoRepository.findByUsername(username); + if (oldUserOpt.isPresent()) { + throw new IllegalArgumentException("username already exist, please change one!"); + } + + PwjbUserInfoDO pwjbUserInfoDO = new PwjbUserInfoDO(); + + pwjbUserInfoDO.setUsername(username); + pwjbUserInfoDO.setGmtCreate(new Date()); + pwjbUserInfoDO.setGmtModified(new Date()); + + // 二次加密密码 + final String password = request.getPassword(); + if (StringUtils.isNotEmpty(password)) { + pwjbUserInfoDO.setPassword(DigestUtils.rePassword(password, pwjbUserInfoDO.getUsername())); + } + + // 其他参数存入 extra,在回调创建真正的内部 USER 时回填 + ModifyUserInfoRequest cpRequest = JsonUtils.parseObject(JsonUtils.toJSONString(request), ModifyUserInfoRequest.class); + cpRequest.setPassword(null); + cpRequest.setUsername(null); + cpRequest.setNick(null); + pwjbUserInfoDO.setExtra(JsonUtils.toJSONString(cpRequest)); + + return pwjbUserInfoRepository.save(pwjbUserInfoDO); + } + + @Override + public void changePassword(ChangePasswordRequest changePasswordRequest) { + if (!StringUtils.equals(changePasswordRequest.getNewPassword(), changePasswordRequest.getNewPassword2())) { + throw new IllegalArgumentException("Inconsistent passwords"); + } + + String username = changePasswordRequest.getUsername(); + Optional userOpt = pwjbUserInfoRepository.findByUsername(username); + if (!userOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by username: " + username); + } + + PwjbUserInfoDO dbUser = userOpt.get(); + String oldPasswordInDb = dbUser.getPassword(); + String oldPasswordInReq = DigestUtils.rePassword(changePasswordRequest.getOldPassword(), dbUser.getUsername()); + if (!StringUtils.equals(oldPasswordInDb, oldPasswordInReq)) { + throw new PowerJobAuthException(ErrorCodes.INCORRECT_PASSWORD); + } + + // 测试账号特殊处理 + if (NOT_ALLOWED_CHANGE_PASSWORD_ACCOUNTS.contains(username)) { + throw new IllegalArgumentException("this account not allowed change the password"); + } + + dbUser.setPassword(DigestUtils.rePassword(changePasswordRequest.getNewPassword(), dbUser.getUsername())); + dbUser.setGmtModified(new Date()); + pwjbUserInfoRepository.saveAndFlush(dbUser); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/UserWebServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/UserWebServiceImpl.java new file mode 100644 index 00000000..04cc3812 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/UserWebServiceImpl.java @@ -0,0 +1,103 @@ +package tech.powerjob.server.web.service.impl; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import tech.powerjob.server.persistence.QueryConvertUtils; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; +import tech.powerjob.server.web.converter.UserConverter; +import tech.powerjob.server.web.request.QueryUserRequest; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.service.UserWebService; + +import javax.annotation.Resource; +import javax.persistence.criteria.Predicate; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * UserWebService + * + * @author tjq + * @since 2024/2/17 + */ +@Service +public class UserWebServiceImpl implements UserWebService { + + /** + * 展示用的 user 查询缓存,对延迟不敏感 + */ + private final Cache userCache4Show = CacheBuilder.newBuilder() + .softValues() + .maximumSize(256) + .expireAfterWrite(3, TimeUnit.MINUTES) + .build(); + + @Resource + private UserInfoRepository userInfoRepository; + + @Override + public Optional fetchBaseUserInfo(Long userId) { + + if (userId == null) { + return Optional.empty(); + } + + try { + UserInfoDO userInfoDO = userCache4Show.get(userId, () -> { + Optional userInfoOpt = userInfoRepository.findById(userId); + if (userInfoOpt.isPresent()) { + return userInfoOpt.get(); + } + throw new IllegalArgumentException("can't find user by userId: " + userId); + }); + + return Optional.of(UserConverter.do2BaseVo(userInfoDO, false)); + } catch (Exception e) { + return Optional.empty(); + } + } + + @Override + public List list(QueryUserRequest q) { + + Long userIdEq = q.getUserIdEq(); + String accountTypeEq = q.getAccountTypeEq(); + String nickLike = q.getNickLike(); + String phoneLike = q.getPhoneLike(); + + + Specification specification = (root, query, cb) -> { + + List predicates = Lists.newArrayList(); + + if (userIdEq != null) { + predicates.add(cb.equal(root.get("id"), userIdEq)); + } + + if (StringUtils.isNotEmpty(accountTypeEq)) { + predicates.add(cb.equal(root.get("accountType"), accountTypeEq)); + } + + if (StringUtils.isNotEmpty(nickLike)) { + predicates.add(cb.like(root.get("nick"), QueryConvertUtils.convertLikeParams(nickLike))); + } + + if (StringUtils.isNotEmpty(phoneLike)) { + predicates.add(cb.like(root.get("phone"), QueryConvertUtils.convertLikeParams(phoneLike))); + } + + if (predicates.isEmpty()) { + return null; + } + return query.where(predicates.toArray(new Predicate[0])).getRestriction(); + }; + + return userInfoRepository.findAll(specification); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java new file mode 100644 index 00000000..e1f263ce --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java @@ -0,0 +1,9 @@ +/** + * 处理 WEB 服务的 service 层 + * 如果有共用逻辑可以单独抽成 service,否则直接写在 controller 即可。PowerJob 的 WEB 领域模型不复杂,没必要过度封装。 + * LESS IS MORE + * + * @author tjq + * @since 2024/2/15 + */ +package tech.powerjob.server.web.service; \ No newline at end of file diff --git a/powerjob-server/powerjob-server-starter/src/main/resources/application.properties b/powerjob-server/powerjob-server-starter/src/main/resources/application.properties index 1b74735b..1bb4ea0d 100644 --- a/powerjob-server/powerjob-server-starter/src/main/resources/application.properties +++ b/powerjob-server/powerjob-server-starter/src/main/resources/application.properties @@ -19,4 +19,8 @@ oms.transporter.main.protocol=HTTP oms.akka.port=10086 oms.http.port=10010 # Prefix for all tables. Default empty string. Config if you have needs, i.e. pj_ -oms.table-prefix= \ No newline at end of file +oms.table-prefix= + +###### PowerJob User and Permission Configuration Configuration ###### +oms.auth.initiliaze.admin.password=powerjob_admin +oms.auth.openapi.enable=false \ No newline at end of file diff --git a/powerjob-server/powerjob-server-starter/src/main/resources/static/img/banner.f4c75b86.jpg b/powerjob-server/powerjob-server-starter/src/main/resources/static/img/banner.f4c75b86.jpg deleted file mode 100644 index 5ea2145a..00000000 Binary files a/powerjob-server/powerjob-server-starter/src/main/resources/static/img/banner.f4c75b86.jpg and /dev/null differ diff --git a/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html b/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html index 1cef0c30..89a0076b 100644 --- a/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html +++ b/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html @@ -6,7 +6,7 @@ PowerJob - +