zhaojinyu 1 mese fa
commit
ccb2e867cb
100 ha cambiato i file con 6764 aggiunte e 0 eliminazioni
  1. 8 0
      .idea/.gitignore
  2. 25 0
      .idea/compiler.xml
  3. 15 0
      .idea/encodings.xml
  4. 20 0
      .idea/jarRepositories.xml
  5. 13 0
      .idea/misc.xml
  6. 4 0
      .idea/vcs.xml
  7. 240 0
      README.md
  8. 51 0
      jnpf-scheduletask-client/pom.xml
  9. 59 0
      jnpf-scheduletask-client/src/main/java/jnpf/scheduletask/config/RegisterAddressConfig.java
  10. 78 0
      jnpf-scheduletask-client/src/main/java/jnpf/scheduletask/config/XxlJobConfig.java
  11. 179 0
      jnpf-scheduletask-client/src/main/java/jnpf/scheduletask/rest/RestScheduleTaskUtil.java
  12. 69 0
      jnpf-scheduletask-client/target/classes/META-INF/spring-configuration-metadata.json
  13. BIN
      jnpf-scheduletask-client/target/classes/jnpf/scheduletask/config/RegisterAddressConfig.class
  14. BIN
      jnpf-scheduletask-client/target/classes/jnpf/scheduletask/config/XxlJobConfig.class
  15. BIN
      jnpf-scheduletask-client/target/classes/jnpf/scheduletask/rest/RestScheduleTaskUtil.class
  16. BIN
      jnpf-scheduletask-client/target/jnpf-scheduletask-client-6.0.0-RELEASE.jar
  17. 3 0
      jnpf-scheduletask-client/target/maven-archiver/pom.properties
  18. 4 0
      jnpf-scheduletask-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  19. 3 0
      jnpf-scheduletask-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
  20. 30 0
      jnpf-scheduletask-model/pom.xml
  21. 37 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/HandlerNameEntity.java
  22. 148 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/TimeTaskEntity.java
  23. 50 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/TimeTaskLogEntity.java
  24. 51 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/XxlJobGroup.java
  25. 91 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/XxlJobInfo.java
  26. 60 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/XxlJobLog.java
  27. 71 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/ContentNewModel.java
  28. 41 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskCrForm.java
  29. 22 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskInfoVO.java
  30. 21 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskLogVO.java
  31. 34 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskMethodsVO.java
  32. 20 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskPage.java
  33. 46 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskParameterModel.java
  34. 15 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskUpForm.java
  35. 33 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskVO.java
  36. 16 0
      jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/UpdateTaskModel.java
  37. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/HandlerNameEntity.class
  38. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/TimeTaskEntity.class
  39. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/TimeTaskLogEntity.class
  40. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/XxlJobGroup.class
  41. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/XxlJobInfo.class
  42. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/XxlJobLog.class
  43. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/ContentNewModel.class
  44. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskCrForm.class
  45. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskInfoVO.class
  46. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskLogVO.class
  47. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskMethodsVO.class
  48. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskPage.class
  49. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskParameterModel.class
  50. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskUpForm.class
  51. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskVO.class
  52. BIN
      jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/UpdateTaskModel.class
  53. BIN
      jnpf-scheduletask-model/target/jnpf-scheduletask-model-6.0.0-RELEASE.jar
  54. 3 0
      jnpf-scheduletask-model/target/maven-archiver/pom.properties
  55. 16 0
      jnpf-scheduletask-model/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  56. 16 0
      jnpf-scheduletask-model/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
  57. 35 0
      pom.xml
  58. 22 0
      xxl-job-admin/Dockerfile
  59. 195 0
      xxl-job-admin/pom.xml
  60. 20 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java
  61. 33 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/config/XxlJobListener.java
  62. 98 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java
  63. 105 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java
  64. 106 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java
  65. 206 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java
  66. 163 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java
  67. 258 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java
  68. 189 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobUserController.java
  69. 29 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java
  70. 42 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java
  71. 131 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java
  72. 27 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java
  73. 65 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java
  74. 31 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/HandlerController.java
  75. 52 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/LogController.java
  76. 131 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/ScheduleTaskController.java
  77. 22 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/XxlJobInfoController.java
  78. 20 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java
  79. 65 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java
  80. 118 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java
  81. 104 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java
  82. 178 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java
  83. 1623 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
  84. 14 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java
  85. 33 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java
  86. 34 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java
  87. 29 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java
  88. 45 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java
  89. 32 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java
  90. 413 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java
  91. 58 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java
  92. 48 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java
  93. 24 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java
  94. 48 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java
  95. 85 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java
  96. 48 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java
  97. 19 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java
  98. 79 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java
  99. 76 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java
  100. 19 0
      xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 25 - 0
.idea/compiler.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <annotationProcessing>
+      <profile name="Maven default annotation processors profile" enabled="true">
+        <sourceOutputDir name="target/generated-sources/annotations" />
+        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
+        <outputRelativeToContentRoot value="true" />
+        <module name="jnpf-scheduletask-client" />
+        <module name="xxl-job-core" />
+        <module name="xxl-job-admin" />
+        <module name="jnpf-scheduletask-model" />
+      </profile>
+    </annotationProcessing>
+  </component>
+  <component name="JavacSettings">
+    <option name="ADDITIONAL_OPTIONS_OVERRIDE">
+      <module name="jnpf-scheduletask-client" options="-parameters" />
+      <module name="jnpf-scheduletask-model" options="-parameters" />
+      <module name="jnpf-scheduletask-starter" options="-parameters" />
+      <module name="xxl-job-admin" options="-parameters" />
+      <module name="xxl-job-core" options="-parameters" />
+    </option>
+  </component>
+</project>

+ 15 - 0
.idea/encodings.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="file://$PROJECT_DIR$/jnpf-scheduletask-client/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/jnpf-scheduletask-client/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/jnpf-scheduletask-model/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/jnpf-scheduletask-model/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/xxl-job-admin/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/xxl-job-admin/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/xxl-job-core/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/xxl-job-core/src/main/resources" charset="UTF-8" />
+  </component>
+</project>

+ 20 - 0
.idea/jarRepositories.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Central Repository" />
+      <option name="url" value="http://127.0.0.1:9999/repository/maven-public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+  </component>
+</project>

+ 13 - 0
.idea/misc.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+        <option value="$PROJECT_DIR$/xxl-job-admin/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
+</project>

+ 4 - 0
.idea/vcs.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings" defaultProject="true" />
+</project>

+ 240 - 0
README.md

@@ -0,0 +1,240 @@
+> 特别说明:源码、JDK、MySQL、Redis等安装或存放路径禁止包含中文、空格、特殊字符等
+
+## 一 项目结构
+
+```text
+jnpf_scheduletask
+    ├── jnpf-scheduletask-client - 调度客户端配置模块
+    ├── jnpf-scheduletask-model- 实体模型模块
+    ├── xxl-job-admin - 调度服务端
+    └── xxl-job-core - 调度服务端核心模块
+```
+
+## 二 环境要求
+
+### 2.1 开发环境
+
+| 类目 | 版本说明或建议           |
+| --- |------------------|
+| 硬件 | 开发电脑建议使用I3及以上CPU,16G及以上内存  |
+| 操作系统 | Windows 10/11,MacOS |
+| JDK | 默认使用JDK 21,如需要切换JDK 8/11/17版本请参考文档调整代码,推荐使用 `OpenJDK`,如 `Liberica JDK`、`Eclipse Temurin`、`Alibaba Dragonwell`、`BiSheng`等发行版; |
+| Maven | 依赖管理工具,推荐使用 `3.6.3` 及以上版本  |
+| Redis | 数据缓存,推荐使用 `5.0` 及以上版本 |
+| 数据库 | 兼容 `MySQL 5.7.x/8.x`、`SQLServer 2012+`、`Oracle 11g`、`PostgreSQL 12+`、`达梦数据库(DM8)`、`人大金仓数据库(KingbaseES_V8R6)` |
+| IDE   | 代码集成开发环境,推荐使用 `IDEA2024` 及以上版本,兼容 `Eclipse`、 `Spring Tool Suite` 等IDE工具 |
+
+### 2.2 运行环境
+
+> 服务端运行环境,适用于测试或生产环境
+
+| 类目 | 版本说明或建议                               |
+| --- |-----------------------------------------------|
+| 服务器配置 | 建议至少在 `4C/16G/50G` 的机器配置下运行;|
+| 操作系统 | 建议使用 `Windows Server 2019` 及以上版本或主流 `Linux` 发行版本,推荐使用 `Linux` 环境;兼容 `统信UOS`,`OpenEuler`,`麒麟服务器版` 等信创环境;    |
+| JRE | 默认使用JRE 21,如需要切换JRE 8/11/17版本请参考文档调整代码;推荐使用 `OpenJDK`,如 `Liberica JDK`、`Eclipse Temurin`、`Alibaba Dragonwell`、`BiSheng`等发行版;   |
+| Redis | 数据缓存,推荐使用 `5.0` 及以上版本 |
+| 数据库 | 兼容 `MySQL 5.7.x/8.x`、`SQLServer 2012+`、`Oracle 11g`、`PostgreSQL 12+`、`达梦数据库(DM8)`、`人大金仓数据库(KingbaseES_V8R6)` |
+
+## 三 关联项目
+
+> 为以下项目提供基础依赖
+
+| 项目 | 分支 |  说明 |
+| --- | --- | --- |
+| jnpf-common  | v6.0.x-stable  | Java基础依赖项目源码 |
+| jnpf-java-boot | v6.0.x-stable  | Java单体后端项目源码 |
+| jnpf-java-cloud | v6.0.x-stable  | Java微服务后端项目源码 |
+
+## 四 使用方式
+
+> 本项目为任务调度的基础依赖和服务端,<br/>作为客户端依赖时需要上传到私服或使用本地安装的方式引用该项目,<br/>作为服务端时需要单独部署
+
+### 4.1 作为客户端依赖
+
+#### 4.1.1 前置条件
+
+##### 4.1.1.1 本地安装jnpf-common-core
+
+IDEA中打开 `jnpf-common` 项目, 双击右侧 `Maven` 中 `jnpf-common` > `jnpf-boot-common` > `jnpf-common-core` > `Lifecycle` > `install`,将 `jnpf-common-core` 包安装至本地
+
+##### 4.1.1.2 本地安装dependencies
+
+IDEA中打开 `jnpf-common` 项目,双击右侧 `Maven` 中 `jnpf-common` > `jnpf-dependencies` > `Lifecycle` > `install`,将 `jnpf-dependencies` 包安装至本地
+
+#### 4.1.2 本地安装
+
+在IDEA中,双击右侧 `Maven` 中 `jnpf-scheduletask-starter` > `Lifecycle` > `install`,将 `jnpf-scheduletask-client` 包安装至本地
+
+#### 4.1.3 私服发布
+> 若无Maven私服,忽略本节内容
+
+##### 4.1.3.1 配置Maven
+
+打开Maven安装目录中的 `conf/setttings.xml`,
+
+在 `<servers></servers>` 节点增加 `<server></server>` ,如下所示:
+
+```xml
+  <!-- 发布版 -->
+  <server>
+    <id>maven-releases</id>
+    <username>jnpf-user(账号,结合私服配置设置)</username>
+    <password>123456(密码,结合私服配置设置)</password>
+  </server>
+```
+
+##### 4.1.3.2 配置项目
+
+> 注意:pom.xml里 `<id>` 和 setting.xml 配置里 `<id>` 对应。
+
+IDEA打开 `jnpf-common` 项目, 修改 `jnpf-dependencies/pom.xml` 文件中私服配置
+
+```xml
+<distributionManagement>
+    <repository>
+        <id>maven-releases</id>
+        <name>maven-releases</name>
+        <url>http://nexus.jnpfsoft.com/repository/maven-releases/</url>
+    </repository>
+</distributionManagement>
+```
+
+##### 4.1.3.3 发布到私服
+
+在IDEA中,双击右侧 `Maven` 中 `jnpf-scheduletask-starter` > `Lifecycle` > `deploy` 发布至私服。
+
+### 4.2 作为服务端
+
+#### 4.2.1 项目配置
+
+##### 4.2.1.1 调整运行端口
+> 根据实际需求调整
+
+打开编辑 `xxl-job-admin/src/main/resources/application.yml`,第 19 行
+
+```yaml
+  port: 30020
+```
+
+##### 4.2.1.2 指定环境配置
+
+打开编辑 `xxl-job-admin/src/main/resources/application.yml`,第 25 行
+
+环境说明:
+
+- `application-dev.yml` 开发环境(默认)
+- `application-preview.yml` 预生产环境
+- `application-test.yml` 测试环境
+- `application-prod.yml` 生产环境
+
+> 以开发环境为例,根据实际需求修改
+
+```yaml
+# application.yml第 6 行,可选值:dev(开发环境-默认)、test(测试环境)、preview(预生产环境)、prod(生产环境)
+active: dev
+```
+
+#### 4.2.1 数据源配置
+
+打开 `xxl-job-admin/src/main/resources/application-dev.yml` 修改数据源,配置示例如下:
+
+##### 4.2.1.1 MySQL数据库
+
+```yaml
+  datasource:
+    url: jdbc:mysql://127.0.0.1:3306/jnpf_xxljob?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=GMT%2B8&useSSL=false
+    username: root
+    password: 123456
+    driver-class-name: com.mysql.cj.jdbc.Driver
+```
+
+##### 4.2.1.2 SQLServer数据库
+
+```yaml
+  datasource:
+    url: jdbc:sqlserver://127.0.0.1:1433;SelectMethod=cursor;Databasename=jnpf_xxljob
+    username: sa
+    password: 123456
+    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
+```
+
+##### 4.2.1.3 Oracle数据库
+
+```yaml
+  datasource:
+    url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
+    username: JNPF_XXLJOB
+    password: dbpasswd
+    driver-class-name: oracle.jdbc.OracleDriver
+```
+
+##### 4.2.1.4 PostgreSQL数据库
+
+```yaml
+  datasource:
+    url: jdbc:postgresql://127.0.0.1:5432/jnpf_xxljob
+    username: dbuser
+    password: dbpasswd
+    driver-class-name: org.postgresql.Driver
+```
+
+##### 4.2.1.5 达梦(DM8)数据库
+
+```yaml
+  datasource:
+    url: jdbc:dm://127.0.0.1:5236/JNPF_XXLJOB?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
+    username: dbuser
+    password: dbpasswd
+    driver-class-name: dm.jdbc.driver.DmDriver
+```
+
+##### 4.2.1.6 人大金仓(KingbaseES_V8R6)数据库
+
+```yaml
+  datasource:
+    url: jdbc:kingbase8://127.0.0.1:54321/jnpf_xxljob
+    username: dbuser
+    password: dbpasswd
+    driver-class-name: com.kingbase8.Driver
+```
+
+#### 4.2.2 打包部署
+
+在IDEA中,在左侧 `Project` 中,右击 `jnpf-scheduletask` > `xxl-job-admin` > `pom.xml` 并选择 `Add as Maven Project` 将 `xxl-job-admin` 转为 Maven 项目,然后双击右侧 `Maven` 中 `xxl-job-admin` > `Lifecycle` > `package`, 将 `/xxl-job-admin/target/xxl-job-admin-5.2.0-RELEASE.jar` 上传至服务器部署即可。
+
+#### 4.2.3 关联项目配置
+
+配置如下所示
+
+```yaml
+# ===================== 任务调度配置 =====================
+xxl:
+  job:
+    accessToken: '432e62f3b488bc861d91b0e274e850cc'
+    i18n: zh_CN
+    logretentiondays: 30
+    triggerpool:
+      fast:
+        max: 200
+      slow:
+        max: 100
+    admin:
+      # xxl-job服务端地址
+      addresses: http://127.0.0.1:30020/xxl-job-admin/
+    executor:
+      address: ''
+      appname: xxl-job-executor-sample1
+      ip: ''
+      logpath: /data/applogs/xxl-job/jobhandler
+      logretentiondays: 30
+      port: 9999
+```
+
+##### 4.2.3.1 jnpf-java-boot项目
+
+IDEA打开 `jnpf-java-boot` 项目, 编辑 `jnpf-admin/src/main/resources/application-x.yml` 文件( `application-x.yml` 为环境配置,如 `application-dev.yml` )
+
+##### 4.2.3.2 jnpf-java-cloud项目
+
+登录 `Nacos` 控制台,依次点击 `配置管理` >  `配置列表` > `develop`,编辑 `datasource-scheduletask.yaml`。

+ 51 - 0
jnpf-scheduletask-client/pom.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>jnpf-scheduletask-starter</artifactId>
+        <groupId>com.jnpf</groupId>
+        <version>6.0.0-RELEASE</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>jnpf-scheduletask-client</artifactId>
+    <description>xxl-job客户端</description>
+
+    <properties>
+    </properties>
+
+    <dependencies>
+        <!--xxl-job任务调度核心-->
+        <dependency>
+            <groupId>com.jnpf</groupId>
+            <artifactId>xxl-job-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.jnpf</groupId>
+            <artifactId>jnpf-scheduletask-model</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.jnpf</groupId>
+            <artifactId>jnpf-common-core</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <!--自动装配-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+        </dependency>
+
+    </dependencies>
+
+
+</project>

+ 59 - 0
jnpf-scheduletask-client/src/main/java/jnpf/scheduletask/config/RegisterAddressConfig.java

@@ -0,0 +1,59 @@
+package jnpf.scheduletask.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@ConfigurationProperties(prefix = RegisterAddressConfig.PREFIX)
+@Component
+public class RegisterAddressConfig {
+
+    public static final String PREFIX = "xxl.admin.register";
+
+    /**
+     * 获取执行器列表
+     */
+    private String handle_query_address;
+
+    /**
+     * 获取任务详情
+     */
+    private String job_info_address;
+
+    /**
+     * 通过任务id获取日志列表
+     */
+    private String log_query_address;
+
+    /**
+     * 获取分页数据
+     */
+    private String task_list_address;
+
+    /**
+     * 通过任务id获取任务详情
+     */
+    private String task_info_address;
+
+    /**
+     * 保存任务调度
+     */
+    private String task_save_address;
+
+    /**
+     * 修改任务调度
+     */
+    private String task_update_address;
+
+    /**
+     * 删除任务调度
+     */
+    private String task_remove_address;
+
+    /**
+     * 启动或停止任务
+     */
+    private String task_startOrRemove_address;
+
+}

+ 78 - 0
jnpf-scheduletask-client/src/main/java/jnpf/scheduletask/config/XxlJobConfig.java

@@ -0,0 +1,78 @@
+package jnpf.scheduletask.config;
+
+import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * xxl-job config
+ *
+ * @author xuxueli 2017-04-28
+ */
+@Configuration
+public class XxlJobConfig {
+    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
+
+    @Value("${xxl.job.admin.addresses}")
+    private String adminAddresses;
+
+    @Value("${xxl.job.accessToken}")
+    private String accessToken;
+
+    @Value("${xxl.job.executor.appname}")
+    private String appname;
+
+    @Value("${xxl.job.executor.address}")
+    private String address;
+
+    @Value("${xxl.job.executor.ip}")
+    private String ip;
+
+    @Value("${xxl.job.executor.port}")
+    private int port;
+
+    @Value("${xxl.job.executor.logpath}")
+    private String logPath;
+
+    @Value("${xxl.job.executor.logretentiondays}")
+    private int logRetentionDays;
+
+
+    @Bean
+    public XxlJobSpringExecutor xxlJobExecutor() {
+        logger.info(">>>>>>>>>>> xxl-job config init.");
+        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
+        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
+        xxlJobSpringExecutor.setAppname(appname);
+        xxlJobSpringExecutor.setAddress(address);
+        xxlJobSpringExecutor.setIp(ip);
+        xxlJobSpringExecutor.setPort(port);
+        xxlJobSpringExecutor.setAccessToken(accessToken);
+        xxlJobSpringExecutor.setLogPath(logPath);
+        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
+
+        return xxlJobSpringExecutor;
+    }
+
+    /**
+     * 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
+     *
+     *      1、引入依赖:
+     *          <dependency>
+     *             <groupId>org.springframework.cloud</groupId>
+     *             <artifactId>spring-cloud-commons</artifactId>
+     *             <version>${version}</version>
+     *         </dependency>
+     *
+     *      2、配置文件,或者容器启动变量
+     *          spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
+     *
+     *      3、获取IP
+     *          String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
+     */
+
+
+}

+ 179 - 0
jnpf-scheduletask-client/src/main/java/jnpf/scheduletask/rest/RestScheduleTaskUtil.java

@@ -0,0 +1,179 @@
+package jnpf.scheduletask.rest;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSONObject;
+import jnpf.base.Pagination;
+import jnpf.base.UserInfo;
+import jnpf.base.vo.PaginationVO;
+import jnpf.scheduletask.config.RegisterAddressConfig;
+import jnpf.scheduletask.entity.*;
+import jnpf.scheduletask.model.*;
+import jnpf.util.JsonUtil;
+import jnpf.util.wxutil.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Component
+public class RestScheduleTaskUtil {
+
+    private static RegisterAddressConfig registerAddressConfig;
+
+    public RestScheduleTaskUtil(RegisterAddressConfig registerAddressConfig) {
+        RestScheduleTaskUtil.registerAddressConfig = registerAddressConfig;
+    }
+
+    /**
+     * 获取执行器列表
+     *
+     * @return
+     */
+    public static List<HandlerNameEntity> getHandlerList() {
+        String handlerList = cn.hutool.http.HttpUtil.get(registerAddressConfig.getHandle_query_address());
+        if (handlerList == null) {
+            return new ArrayList<>();
+        }
+        return JsonUtil.getJsonToList(handlerList, HandlerNameEntity.class);
+    }
+
+    /**
+     * 通过任务id获取日志列表
+     *
+     * @param id 任务id
+     * @param userInfo
+     * @param taskPage 分页参数
+     * @return
+     */
+    public static JSONObject getLogList(String id, UserInfo userInfo, TaskPage taskPage) {
+        String param = taskPage.getRunResult() == null ? "&runResult=" : "&runResult=" + taskPage.getRunResult().toString();
+        String timeSelect = "";
+        if (ObjectUtil.isNotNull(taskPage.getStartTime()) || ObjectUtil.isNotNull(taskPage.getEndTime())) {
+            timeSelect = "&startTime=" + taskPage.getStartTime()
+                    + "&endTime=" + taskPage.getEndTime();
+        }
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getLog_query_address() + "/" + id
+                        + "?currentPage=" + taskPage.getCurrentPage()
+                        + "&pageSize=" + taskPage.getPageSize()
+                        + "&sort=" + taskPage.getSort()
+                        + "&sidx=" + taskPage.getSidx() + param + timeSelect,
+                "POST", JsonUtil.getObjectToString(userInfo), null);
+        JSONObject jsonObject = (JSONObject) get.get("data");
+        List<TaskLogVO> data = JsonUtil.getJsonToList(jsonObject.get("list"), TaskLogVO.class);
+        PaginationVO page = JsonUtil.getJsonToBean(jsonObject.get("pagination"), PaginationVO.class);
+        jsonObject.put("list", data);
+        jsonObject.put("pagination", page);
+        get.put("data", jsonObject);
+        return get;
+    }
+
+    /**
+     * 获取分页数据
+     *
+     * @param pagination 分页参数
+     * @return
+     */
+    public static JSONObject getList(Pagination pagination, UserInfo userInfo) {
+        JSONObject get = null;
+        try {
+            get = HttpUtil.httpRequest(registerAddressConfig.getTask_list_address()
+                            + "?currentPage=" + pagination.getCurrentPage()
+                            + "&pageSize=" + pagination.getPageSize()
+                            + "&keyword=" + URLEncoder.encode(pagination.getKeyword(), "utf-8"),
+                    "POST", JsonUtil.getObjectToString(userInfo), null);
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+        JSONObject jsonObject = (JSONObject) get.get("data");
+        List<TaskVO> data = JsonUtil.getJsonToList(jsonObject.get("list"), TaskVO.class);
+        PaginationVO page = JsonUtil.getJsonToBean(jsonObject.get("pagination"), PaginationVO.class);
+        jsonObject.put("list", data);
+        jsonObject.put("pagination", page);
+        get.put("data", jsonObject);
+        return get;
+    }
+
+    /**
+     * 通过任务id获取任务详情
+     *
+     * @param id 任务id
+     * @return
+     */
+    public static TimeTaskEntity getInfo(String id, UserInfo userInfo) {
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getTask_info_address() + "?taskId=" + id,
+                "POST", JsonUtil.getObjectToString(userInfo), null);
+        return JsonUtil.getJsonToBean(get, TimeTaskEntity.class);
+    }
+
+    /**
+     * 保存任务调度
+     *
+     * @param taskCrForm
+     * @return
+     */
+    public static JSONObject create(TaskCrForm taskCrForm) {
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getTask_save_address(),
+                "POST", JsonUtil.getObjectToString(taskCrForm), null);
+        return get;
+    }
+
+    /**
+     * 日程任务调度
+     *
+     * @param taskCrForm
+     * @return
+     */
+    public static JSONObject schedule(TaskCrForm taskCrForm) {
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getTask_save_address()+"/schedule",
+                "POST", JsonUtil.getObjectToString(taskCrForm), null);
+        return get;
+    }
+
+    /**
+     * 修改任务调度
+     *
+     * @param id
+     * @param taskUpForm
+     * @return
+     */
+    public static JSONObject update(String id, TaskUpForm taskUpForm) {
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getTask_update_address() + "/" + id,
+                "PUT", JsonUtil.getObjectToString(taskUpForm), null);
+        return get;
+    }
+
+    /**
+     * 删除任务调度
+     *
+     * @param id
+     * @return
+     */
+    public static JSONObject delete(String id, UserInfo userInfo) {
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getTask_remove_address() + "/" + id,
+                "POST", JsonUtil.getObjectToString(userInfo), null);
+        return get;
+    }
+
+    /**
+     * 启动任务调度
+     *
+     * @param updateTaskModel
+     * @return
+     */
+    public static JSONObject updateTask(UpdateTaskModel updateTaskModel) {
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getTask_startOrRemove_address(),
+                "POST", JsonUtil.getObjectToString(updateTaskModel), null);
+        return get;
+    }
+
+
+    public static XxlJobInfo getInfoByTaskId(String taskId) {
+        JSONObject get = HttpUtil.httpRequest(registerAddressConfig.getJob_info_address() + "?taskId=" + taskId,
+                "GET", null, null);
+        return JsonUtil.getJsonToBean(get, XxlJobInfo.class);
+    }
+}

+ 69 - 0
jnpf-scheduletask-client/target/classes/META-INF/spring-configuration-metadata.json

@@ -0,0 +1,69 @@
+{
+  "groups": [
+    {
+      "name": "xxl.admin.register",
+      "type": "jnpf.scheduletask.config.RegisterAddressConfig",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    }
+  ],
+  "properties": [
+    {
+      "name": "xxl.admin.register.handle-query-address",
+      "type": "java.lang.String",
+      "description": "获取执行器列表",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.job-info-address",
+      "type": "java.lang.String",
+      "description": "获取任务详情",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.log-query-address",
+      "type": "java.lang.String",
+      "description": "通过任务id获取日志列表",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.task-info-address",
+      "type": "java.lang.String",
+      "description": "通过任务id获取任务详情",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.task-list-address",
+      "type": "java.lang.String",
+      "description": "获取分页数据",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.task-remove-address",
+      "type": "java.lang.String",
+      "description": "删除任务调度",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.task-save-address",
+      "type": "java.lang.String",
+      "description": "保存任务调度",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.task-start-or-remove-address",
+      "type": "java.lang.String",
+      "description": "启动或停止任务",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    },
+    {
+      "name": "xxl.admin.register.task-update-address",
+      "type": "java.lang.String",
+      "description": "修改任务调度",
+      "sourceType": "jnpf.scheduletask.config.RegisterAddressConfig"
+    }
+  ],
+  "hints": [],
+  "ignored": {
+    "properties": []
+  }
+}

BIN
jnpf-scheduletask-client/target/classes/jnpf/scheduletask/config/RegisterAddressConfig.class


BIN
jnpf-scheduletask-client/target/classes/jnpf/scheduletask/config/XxlJobConfig.class


BIN
jnpf-scheduletask-client/target/classes/jnpf/scheduletask/rest/RestScheduleTaskUtil.class


BIN
jnpf-scheduletask-client/target/jnpf-scheduletask-client-6.0.0-RELEASE.jar


+ 3 - 0
jnpf-scheduletask-client/target/maven-archiver/pom.properties

@@ -0,0 +1,3 @@
+artifactId=jnpf-scheduletask-client
+groupId=com.jnpf
+version=6.0.0-RELEASE

+ 4 - 0
jnpf-scheduletask-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst

@@ -0,0 +1,4 @@
+jnpf\scheduletask\config\XxlJobConfig.class
+jnpf\scheduletask\config\RegisterAddressConfig.class
+jnpf\scheduletask\rest\RestScheduleTaskUtil.class
+META-INF\spring-configuration-metadata.json

+ 3 - 0
jnpf-scheduletask-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst

@@ -0,0 +1,3 @@
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-client\src\main\java\jnpf\scheduletask\config\RegisterAddressConfig.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-client\src\main\java\jnpf\scheduletask\config\XxlJobConfig.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-client\src\main\java\jnpf\scheduletask\rest\RestScheduleTaskUtil.java

+ 30 - 0
jnpf-scheduletask-model/pom.xml

@@ -0,0 +1,30 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>jnpf-scheduletask-starter</artifactId>
+		<groupId>com.jnpf</groupId>
+		<version>6.0.0-RELEASE</version>
+	</parent>
+	<artifactId>jnpf-scheduletask-model</artifactId>
+	<packaging>jar</packaging>
+	<description>共同模型</description>
+
+	<properties>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>mybatis-plus-annotation</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.jnpf</groupId>
+			<artifactId>jnpf-common-core</artifactId>
+			<version>${project.version}</version>
+			<scope>provided</scope>
+		</dependency>
+	</dependencies>
+
+</project>

+ 37 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/HandlerNameEntity.java

@@ -0,0 +1,37 @@
+package jnpf.scheduletask.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+/**
+ * 任务调度实体类
+ *
+ * @author :JNPF开发平台组
+ * @version: V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date :2022/3/23 9:51
+ */
+@Data
+@TableName("base_handlername")
+public class HandlerNameEntity {
+    /**
+     * 定时任务主键
+     */
+    @TableId("F_ID")
+    private String id;
+
+    /**
+     * 任务编码
+     */
+    @TableField("F_HANDLERNAME")
+    private String handlerName;
+
+    /**
+     * 任务名称
+     */
+    @TableField("F_EXECUTOR")
+    private String executor;
+
+}

+ 148 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/TimeTaskEntity.java

@@ -0,0 +1,148 @@
+package jnpf.scheduletask.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 任务调度实体类
+ *
+ * @author :JNPF开发平台组
+ * @version: V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date :2022/3/23 9:51
+ */
+@Data
+@TableName("base_timetask")
+public class TimeTaskEntity {
+    /**
+     * 定时任务主键
+     */
+    @TableId("F_ID")
+    private String id;
+
+    /**
+     * 任务编码
+     */
+    @TableField("F_ENCODE")
+    private String enCode;
+
+    /**
+     * 任务名称
+     */
+    @TableField("F_FULLNAME")
+    private String fullName;
+
+    /**
+     * 执行类型 1.接口 2.存储过程
+     */
+    @TableField("F_EXECUTETYPE")
+    private String executeType;
+
+    /**
+     * 执行内容
+     */
+    @TableField("F_EXECUTECONTENT")
+    private String executeContent;
+
+    /**
+     * 执行周期
+     */
+    @TableField("F_EXECUTECYCLEJSON")
+    private String executeCycleJson;
+
+    /**
+     * 最后运行时间
+     */
+    @TableField("F_LASTRUNTIME")
+    private Date lastRunTime;
+
+    /**
+     * 下次运行时间
+     */
+    @TableField("F_NEXTRUNTIME")
+    private Date nextRunTime;
+
+    /**
+     * 运行次数
+     */
+    @TableField("F_RUNCOUNT")
+    private Integer runCount;
+
+    /**
+     * 描述
+     */
+    @TableField("F_DESCRIPTION")
+    private String description;
+
+    /**
+     * 排序码
+     */
+    @TableField("F_SORTCODE")
+    private Long sortCode;
+
+    /**
+     * 有效标志
+     */
+    @TableField("F_ENABLEDMARK")
+    private Integer enabledMark;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "F_CREATORTIME")
+    private Date creatorTime;
+
+    /**
+     * 创建用户
+     */
+    @TableField(value = "F_CREATORUSERID")
+    private String creatorUserId;
+
+    /**
+     * 修改时间
+     */
+    @TableField(value = "F_LASTMODIFYTIME")
+    private Date lastModifyTime;
+
+    /**
+     * 修改用户
+     */
+    @TableField(value = "F_LASTMODIFYUSERID")
+    private String lastModifyUserId;
+
+    /**
+     * 删除标志
+     */
+    @TableField("F_DELETEMARK")
+    private Integer deleteMark;
+
+    /**
+     * 删除时间
+     */
+    @TableField("F_DELETETIME")
+    private Date deleteTime;
+
+    /**
+     * 删除用户
+     */
+    @TableField("F_DELETEUSERID")
+    private String deleteUserId;
+
+    /**
+     * 删除用户
+     */
+    @TableField("F_EXECUTORNAME")
+    private String executorName;
+
+    /**
+     * 删除用户
+     */
+    @TableField("F_TenantId")
+    private String tenantId;
+
+}

+ 50 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/TimeTaskLogEntity.java

@@ -0,0 +1,50 @@
+package jnpf.scheduletask.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 定时任务记录
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2019年9月27日 上午9:18
+ */
+@Data
+@TableName("base_timetasklog")
+public class TimeTaskLogEntity {
+    /**
+     * 执行任务主键
+     */
+    @TableId("F_ID")
+    private String id;
+
+    /**
+     * 定时任务主键
+     */
+    @TableField("F_TASKID")
+    private String taskId;
+
+    /**
+     * 执行时间
+     */
+    @TableField("F_RUNTIME")
+    private Date runTime;
+
+    /**
+     * 执行结果
+     */
+    @TableField("F_RUNRESULT")
+    private Integer runResult;
+
+    /**
+     * 执行说明
+     */
+    @TableField("F_DESCRIPTION")
+    private String description;
+}

+ 51 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/XxlJobGroup.java

@@ -0,0 +1,51 @@
+package jnpf.scheduletask.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Created by xuxueli on 16/9/30.
+ */
+@Data
+@TableName("xxl_job_group")
+public class XxlJobGroup {
+
+    @TableId(type = IdType.ASSIGN_ID)
+	private String id;
+
+    @TableField("APP_NAME")
+    private String appname;
+
+    @TableField("TITLE")
+    private String title;
+
+    @TableField("ADDRESS_TYPE")
+    private int addressType;
+
+    // 执行器地址类型:0=自动注册、1=手动录入
+    @TableField("ADDRESS_LIST")
+    private String addressList;     // 执行器地址列表,多地址逗号分隔(手动录入)
+
+    @TableField("UPDATE_TIME")
+    private Date updateTime;
+
+    // registry list
+    @TableField(exist = false)
+    private List<String> registryList;  // 执行器地址列表(系统注册)
+
+    public List<String> getRegistryList() {
+        if (addressList!=null && addressList.trim().length()>0) {
+            registryList = new ArrayList<String>(Arrays.asList(addressList.split(",")));
+        }
+        return registryList;
+    }
+
+}

+ 91 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/XxlJobInfo.java

@@ -0,0 +1,91 @@
+package jnpf.scheduletask.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * xxl-job info
+ *
+ * @author xuxueli  2016-1-12 18:25:49
+ */
+@Data
+@TableName("xxl_job_info")
+public class XxlJobInfo {
+
+	@TableId(type = IdType.ASSIGN_ID)
+	private String id;				// 主键ID
+
+	@TableField("JOB_GROUP")
+	private String jobGroup;		// 执行器主键ID
+
+	@TableField("JOB_DESC")
+	private String jobDesc;
+
+	@TableField("ADD_TIME")
+	private Date addTime;
+
+	@TableField("UPDATE_TIME")
+	private Date updateTime;
+
+	@TableField("AUTHOR")
+	private String author;		// 负责人
+	@TableField("ALARM_EMAIL")
+	private String alarmEmail;	// 报警邮件
+
+	@TableField("SCHEDULE_TYPE")
+	private String scheduleType;			// 调度类型
+	@TableField("SCHEDULE_CONF")
+	private String scheduleConf;			// 调度配置,值含义取决于调度类型
+	@TableField("MISFIRE_STRATEGY")
+	private String misfireStrategy;			// 调度过期策略
+
+	@TableField("EXECUTOR_ROUTE_STRATEGY")
+	private String executorRouteStrategy;	// 执行器路由策略
+	@TableField("EXECUTOR_HANDLER")
+	private String executorHandler;		    // 执行器,任务Handler名称
+	@TableField("EXECUTOR_PARAM")
+	private String executorParam;		    // 执行器,任务参数
+	@TableField("EXECUTOR_BLOCK_STRATEGY")
+	private String executorBlockStrategy;	// 阻塞处理策略
+	@TableField("EXECUTOR_TIMEOUT")
+	private int executorTimeout;     		// 任务执行超时时间,单位秒
+	@TableField("EXECUTOR_FAIL_RETRY_COUNT")
+	private int executorFailRetryCount;		// 失败重试次数
+
+	@TableField("GLUE_TYPE")
+	private String glueType;		// GLUE类型	#com.xxl.job.core.glue.GlueTypeEnum
+	@TableField("GLUE_SOURCE")
+	private String glueSource;		// GLUE源代码
+	@TableField("GLUE_REMARK")
+	private String glueRemark;		// GLUE备注
+	@TableField("GLUE_UPDATETIME")
+	private Date glueUpdatetime;	// GLUE更新时间
+
+	@TableField("CHILD_JOBID")
+	private String childJobId;		// 子任务ID,多个逗号分隔
+
+	@TableField("TRIGGER_STATUS")
+	private int triggerStatus;		// 调度状态:0-停止,1-运行
+	@TableField("TRIGGER_LAST_TIME")
+	private long triggerLastTime;	// 上次调度时间
+	@TableField("TRIGGER_NEXT_TIME")
+	private long triggerNextTime;	// 下次调度时间
+
+	/**
+	 * 租户编码
+	 * @return
+	 */
+	@TableField("TENANTID")
+	private String tenantId;
+	/**
+	 * 租户编码
+	 * @return
+	 */
+	@TableField("TASKID")
+	private String taskId;
+}

+ 60 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/entity/XxlJobLog.java

@@ -0,0 +1,60 @@
+package jnpf.scheduletask.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * xxl-job log, used to track trigger process
+ * @author xuxueli  2015-12-19 23:19:09
+ */
+@Data
+@TableName("xxl_job_log")
+public class XxlJobLog {
+
+	@TableId(type = IdType.ASSIGN_ID)
+	private String id;
+	
+	// job info
+	@TableField("JOB_GROUP")
+	private String jobGroup;
+	@TableField("JOB_ID")
+	private String jobId;
+
+	// execute info
+	@TableField("EXECUTOR_ADDRESS")
+	private String executorAddress;
+	@TableField("EXECUTOR_HANDLER")
+	private String executorHandler;
+	@TableField("EXECUTOR_PARAM")
+	private String executorParam;
+	@TableField("EXECUTOR_SHARDING_PARAM")
+	private String executorShardingParam;
+	@TableField("EXECUTOR_FAIL_RETRY_COUNT")
+	private int executorFailRetryCount;
+	
+	// trigger info
+	@TableField("TRIGGER_TIME")
+	private Date triggerTime;
+	@TableField("TRIGGER_CODE")
+	private int triggerCode;
+	@TableField("TRIGGER_MSG")
+	private String triggerMsg;
+	
+	// handle info
+	@TableField("HANDLE_TIME")
+	private Date handleTime;
+	@TableField("HANDLE_CODE")
+	private int handleCode;
+	@TableField("HANDLE_MSG")
+	private String handleMsg;
+
+	// alarm info
+	@TableField("ALARM_STATUS")
+	private int alarmStatus;
+
+}

+ 71 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/ContentNewModel.java

@@ -0,0 +1,71 @@
+package jnpf.scheduletask.model;
+
+import jnpf.base.UserInfo;
+import jnpf.model.visualJson.TemplateJsonModel;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2021/3/12 15:31
+ */
+@Data
+public class ContentNewModel {
+    /**
+     * 表达式设置
+     */
+    private String cron;
+    /**
+     * 数据接口Id
+     */
+    private String interfaceId;
+    /**
+     * 数据接口名称
+     */
+    private String interfaceName;
+    /**
+     * 本地任务Id
+     */
+    private String localHostTaskId;
+
+//    /**
+//     * 租户id
+//     */
+//    private String tenantId;
+//
+//    /**
+//     * 租户库
+//     */
+//    private String tenantDbConnectionString;
+
+    /**
+     * 用户信息
+     */
+    private UserInfo userInfo;
+
+    /**
+     * token
+     */
+    private String token;
+
+    /**
+     * 开始时间
+     */
+    private Long startTime;
+
+    /**
+     * 结束时间
+     */
+    private Long endTime;
+
+    private String executeType;
+
+    /**
+     * 请求参数
+     */
+    private List<TemplateJsonModel> parameter;
+
+}

+ 41 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskCrForm.java

@@ -0,0 +1,41 @@
+package jnpf.scheduletask.model;
+
+import jnpf.base.UserInfo;
+import lombok.Data;
+
+import jakarta.validation.constraints.NotBlank;
+
+/**
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2021/3/12 15:31
+ */
+@Data
+public class TaskCrForm {
+    private String id;
+    @NotBlank(message = "必填")
+    private String fullName;
+    @NotBlank(message = "必填")
+    private String executeType;
+    private String description;
+    @NotBlank(message = "必填")
+    private String executeContent;
+    private long sortCode;
+    private String enCode;
+
+    private Integer enabledMark;
+
+    private UserInfo userInfo;
+
+    /**
+     * 开始时间
+     */
+    private Long startTime;
+
+    /**
+     * 结束时间
+     */
+    private Long endTime;
+}

+ 22 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskInfoVO.java

@@ -0,0 +1,22 @@
+package jnpf.scheduletask.model;
+
+import lombok.Data;
+
+/**
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2021/3/12 15:31
+ */
+@Data
+public class TaskInfoVO {
+    private String id;
+    private String fullName;
+    private String executeType;
+    private String description;
+    private String executeContent;
+    private Long sortCode;
+    private String enCode;
+    private Integer enabledMark;
+}

+ 21 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskLogVO.java

@@ -0,0 +1,21 @@
+package jnpf.scheduletask.model;
+
+import lombok.Data;
+
+/**
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2021/3/12 15:31
+ */
+@Data
+public class TaskLogVO {
+   private String description;
+
+  private String id;
+
+  private Integer runResult;
+
+   private Long runTime;
+}

+ 34 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskMethodsVO.java

@@ -0,0 +1,34 @@
+package jnpf.scheduletask.model;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 展示本地方法列表使用
+ *
+ * @author :JNPF开发平台组
+ * @version: V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date :2022/3/9 15:40
+ */
+@Data
+public class TaskMethodsVO implements Serializable {
+
+    // 展示使用-------------
+    /**
+     * id
+     */
+    private String id;
+
+    /**
+     * 展示的方法名
+     */
+    private String fullName;
+
+    /**
+     * 方法说明
+     */
+    private String description;
+
+}

+ 20 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskPage.java

@@ -0,0 +1,20 @@
+package jnpf.scheduletask.model;
+
+import jnpf.base.Pagination;
+import lombok.Data;
+
+/**
+ *
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com)
+ * @date  2021/5/18
+ */
+@Data
+public class TaskPage extends Pagination {
+    private Integer runResult;
+
+    private Long startTime;
+    private Long endTime;
+}

+ 46 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskParameterModel.java

@@ -0,0 +1,46 @@
+package jnpf.scheduletask.model;
+
+import lombok.Data;
+
+/**
+ * 任务调度参数
+ *
+ * @author :JNPF开发平台组
+ * @version: V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date :2022/3/8 17:10
+ */
+@Data
+public class TaskParameterModel {
+
+    private String id;
+    /**
+     * 默认值
+     */
+    private String defaultValue;
+    /**
+     * 字段名
+     */
+    private String field;
+    /**
+     * 数据类型
+     */
+    private String dataType;
+    /**
+     * 是否必填
+     */
+    private String required;
+    /**
+     * 字段说明
+     */
+    private String fieldName;
+    /**
+     * 值
+     */
+    private String value;
+    /**
+     * 值
+     */
+    private String relationField;
+
+}

+ 15 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskUpForm.java

@@ -0,0 +1,15 @@
+package jnpf.scheduletask.model;
+
+import lombok.Data;
+
+/**
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2021/3/12 15:31
+ */
+@Data
+public class TaskUpForm extends TaskCrForm {
+
+}

+ 33 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/TaskVO.java

@@ -0,0 +1,33 @@
+package jnpf.scheduletask.model;
+
+import lombok.Data;
+
+/**
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2021/3/12 15:31
+ */
+@Data
+public class TaskVO {
+    private String fullName;
+    private String enCode;
+    private String runCount;
+    private Long lastRunTime;
+    private Long nextRunTime;
+    private String description;
+    private String id;
+    private Integer enabledMark;
+    private Long sortCode;
+
+    /**
+     * 开始时间
+     */
+    private Long startTime;
+
+    /**
+     * 结束时间
+     */
+    private Long endTime;
+}

+ 16 - 0
jnpf-scheduletask-model/src/main/java/jnpf/scheduletask/model/UpdateTaskModel.java

@@ -0,0 +1,16 @@
+package jnpf.scheduletask.model;
+
+import jnpf.base.UserInfo;
+import jnpf.scheduletask.entity.TimeTaskEntity;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class UpdateTaskModel implements Serializable {
+
+    private TimeTaskEntity entity;
+
+    private UserInfo userInfo;
+
+}

BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/HandlerNameEntity.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/TimeTaskEntity.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/TimeTaskLogEntity.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/XxlJobGroup.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/XxlJobInfo.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/entity/XxlJobLog.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/ContentNewModel.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskCrForm.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskInfoVO.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskLogVO.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskMethodsVO.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskPage.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskParameterModel.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskUpForm.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/TaskVO.class


BIN
jnpf-scheduletask-model/target/classes/jnpf/scheduletask/model/UpdateTaskModel.class


BIN
jnpf-scheduletask-model/target/jnpf-scheduletask-model-6.0.0-RELEASE.jar


+ 3 - 0
jnpf-scheduletask-model/target/maven-archiver/pom.properties

@@ -0,0 +1,3 @@
+artifactId=jnpf-scheduletask-model
+groupId=com.jnpf
+version=6.0.0-RELEASE

+ 16 - 0
jnpf-scheduletask-model/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst

@@ -0,0 +1,16 @@
+jnpf\scheduletask\model\TaskInfoVO.class
+jnpf\scheduletask\entity\TimeTaskEntity.class
+jnpf\scheduletask\model\TaskMethodsVO.class
+jnpf\scheduletask\model\UpdateTaskModel.class
+jnpf\scheduletask\model\TaskParameterModel.class
+jnpf\scheduletask\model\ContentNewModel.class
+jnpf\scheduletask\model\TaskPage.class
+jnpf\scheduletask\model\TaskVO.class
+jnpf\scheduletask\entity\XxlJobLog.class
+jnpf\scheduletask\model\TaskCrForm.class
+jnpf\scheduletask\model\TaskUpForm.class
+jnpf\scheduletask\entity\TimeTaskLogEntity.class
+jnpf\scheduletask\entity\XxlJobInfo.class
+jnpf\scheduletask\entity\HandlerNameEntity.class
+jnpf\scheduletask\entity\XxlJobGroup.class
+jnpf\scheduletask\model\TaskLogVO.class

+ 16 - 0
jnpf-scheduletask-model/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst

@@ -0,0 +1,16 @@
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\entity\HandlerNameEntity.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\entity\TimeTaskEntity.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\entity\TimeTaskLogEntity.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\entity\XxlJobGroup.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\entity\XxlJobInfo.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\entity\XxlJobLog.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\ContentNewModel.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskCrForm.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskInfoVO.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskLogVO.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskMethodsVO.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskPage.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskParameterModel.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskUpForm.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\TaskVO.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-scheduletask-v6x\jnpf-scheduletask-model\src\main\java\jnpf\scheduletask\model\UpdateTaskModel.java

+ 35 - 0
pom.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>jnpf-dependencies</artifactId>
+        <groupId>com.jnpf</groupId>
+        <version>6.0.0-RELEASE</version>
+    </parent>
+
+    <artifactId>jnpf-scheduletask-starter</artifactId>
+    <packaging>pom</packaging>
+    <version>6.0.0-RELEASE</version>
+
+    <description>任务调度项目,集成xxl-job</description>
+
+    <modules>
+        <module>xxl-job-core</module>
+        <module>jnpf-scheduletask-client</module>
+        <module>jnpf-scheduletask-model</module>
+    </modules>
+
+    <properties>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 22 - 0
xxl-job-admin/Dockerfile

@@ -0,0 +1,22 @@
+# 基础镜像
+FROM bellsoft/liberica-openjre-rocky:21
+# FROM bellsoft/liberica-openjre-rocky:17
+# FROM bellsoft/liberica-openjre-rocky:11
+# FROM bellsoft/liberica-openjre-rocky:8
+LABEL maintainer=jnpf-team
+
+# 设置时区
+ENV TZ=Asia/Shanghai
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+# 指定运行时的工作目录
+WORKDIR /data/jnpfsoft/scheduletaskApi
+
+# 将构建产物jar包拷贝到运行时目录中
+COPY target/*.jar ./jnpf-scheduletask-server.jar
+
+# 指定容器内运行端口
+EXPOSE 30020
+
+# 指定容器启动时要运行的命令
+ENTRYPOINT ["/bin/sh","-c","java -Dfile.encoding=utf8 -Djava.security.egd=file:/dev/./urandom -jar jnpf-scheduletask-server.jar"]

+ 195 - 0
xxl-job-admin/pom.xml

@@ -0,0 +1,195 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<artifactId>jnpf-dependencies</artifactId>
+		<groupId>com.jnpf</groupId>
+		<version>6.0.0-RELEASE</version>
+		<relativePath/>
+	</parent>
+
+	<groupId>com.jnpf</groupId>
+	<artifactId>xxl-job-admin</artifactId>
+	<packaging>jar</packaging>
+	<version>6.0.0-RELEASE</version>
+
+	<properties>
+		<maven.deploy.skip>true</maven.deploy.skip>
+	</properties>
+
+	<dependencyManagement>
+	</dependencyManagement>
+
+	<dependencies>
+		<!-- starter-web:spring-webmvc + autoconfigure + logback + yaml + tomcat -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<!-- starter-test:junit + spring-test + mockito -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+		</dependency>
+
+		<!-- freemarker-starter -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-freemarker</artifactId>
+		</dependency>
+
+		<!-- mail-starter -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-mail</artifactId>
+		</dependency>
+
+		<!-- starter-actuator -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.jnpf</groupId>
+			<artifactId>jnpf-common-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<!-- xxl-job-core -->
+		<dependency>
+			<groupId>com.jnpf</groupId>
+			<artifactId>xxl-job-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+
+		<dependency>
+			<groupId>com.jnpf</groupId>
+			<artifactId>jnpf-scheduletask-model</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.mysql</groupId>
+			<artifactId>mysql-connector-j</artifactId>
+		</dependency>
+
+		<!--sqlserver-->
+		<dependency>
+			<groupId>com.microsoft.sqlserver</groupId>
+			<artifactId>mssql-jdbc</artifactId>
+		</dependency>
+
+		<!-- oracle -->
+		<dependency>
+			<groupId>com.oracle.database.jdbc</groupId>
+			<artifactId>ojdbc8</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.oracle.database.nls</groupId>
+			<artifactId>orai18n</artifactId>
+		</dependency>
+		<!-- dm -->
+		<dependency>
+			<groupId>com.dameng</groupId>
+			<artifactId>DmJdbcDriver18</artifactId>
+		</dependency>
+		<!-- 金仓 -->
+		<dependency>
+			<groupId>cn.com.kingbase</groupId>
+			<artifactId>kingbase8</artifactId>
+		</dependency>
+		<!--PostGre-->
+		<dependency>
+			<groupId>org.postgresql</groupId>
+			<artifactId>postgresql</artifactId>
+		</dependency>
+		<!--GaussDB-->
+		<!--<dependency>
+            <groupId>org.opengauss</groupId>
+            <artifactId>opengauss-jdbc</artifactId>
+        </dependency>-->
+		<!--HighgoDB-->
+		<!--<dependency>
+            <groupId>com.highgo</groupId>
+            <artifactId>hgdb-pgjdbc</artifactId>
+        </dependency>-->
+    </dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<executions>
+					<execution>
+						<goals>
+							<goal>repackage</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<!-- docker -->
+			<!--<plugin>
+				<groupId>com.spotify</groupId>
+				<artifactId>docker-maven-plugin</artifactId>
+				<version>0.4.13</version>
+				<configuration>
+					&lt;!&ndash; made of '[a-z0-9-_.]' &ndash;&gt;
+					<imageName>${project.artifactId}:${project.version}</imageName>
+					<dockerDirectory>${project.basedir}</dockerDirectory>
+					<resources>
+						<resource>
+							<targetPath>/</targetPath>
+							<directory>${project.build.directory}</directory>
+							<include>${project.build.finalName}.jar</include>
+						</resource>
+					</resources>
+				</configuration>
+			</plugin>-->
+		</plugins>
+	</build>
+
+
+	<profiles>
+		<profile>
+			<id>boot3</id>
+			<activation>
+				<jdk>[17,)</jdk>
+			</activation>
+			<dependencies>
+				<dependency>
+					<groupId>com.baomidou</groupId>
+					<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+					<version>${mybatis-plus.vesion}</version>
+				</dependency>
+				<dependency>
+					<groupId>com.baomidou</groupId>
+					<artifactId>mybatis-plus-jsqlparser</artifactId>
+					<version>${mybatis-plus.vesion}</version>
+				</dependency>
+			</dependencies>
+		</profile>
+		<profile>
+			<id>boot2</id>
+			<activation>
+				<jdk>(,17)</jdk>
+			</activation>
+			<dependencies>
+				<dependency>
+					<groupId>com.baomidou</groupId>
+					<artifactId>mybatis-plus-boot-starter</artifactId>
+					<version>${mybatis-plus.vesion}</version>
+				</dependency>
+				<dependency>
+					<groupId>com.baomidou</groupId>
+					<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
+					<version>${mybatis-plus.vesion}</version>
+				</dependency>
+			</dependencies>
+		</profile>
+	</profiles>
+
+</project>

+ 20 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java

@@ -0,0 +1,20 @@
+package com.xxl.job.admin;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+/**
+ * @author xuxueli 2018-10-28 00:38:13
+ */
+@EnableAsync
+@SpringBootApplication(scanBasePackages = {"com.xxl.job", "jnpf"})
+@MapperScan(basePackages = {"com.xxl.job.admin.dao", "com.xxl.job.admin.mapper"})
+public class XxlJobAdminApplication {
+
+	public static void main(String[] args) {
+        SpringApplication.run(XxlJobAdminApplication.class, args);
+	}
+
+}

+ 33 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/config/XxlJobListener.java

@@ -0,0 +1,33 @@
+package com.xxl.job.admin.config;
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.xxl.job.admin.service.HandlerNameService;
+import jnpf.util.context.SpringContext;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * @author JNPF开发平台组
+ * @version V3.1.0
+ * @copyright 引迈信息技术有限公司
+ * @date 2021/3/16 8:49
+ */
+@Component
+public class XxlJobListener implements ApplicationRunner {
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        HandlerNameService handlerNameService = SpringContext.getBean(HandlerNameService.class);
+        handlerNameService.removeAll();
+    }
+
+    @Bean
+    public MybatisPlusInterceptor pageHelper() {
+        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
+        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
+        return mybatisPlusInterceptor;
+    }
+}

+ 98 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java

@@ -0,0 +1,98 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.service.XxlJobService;
+import com.xxl.job.admin.service.impl.XxlJobLoginService;
+import com.xxl.job.core.biz.model.ReturnT;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.propertyeditors.CustomDateEditor;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.view.RedirectView;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * index controller
+ * @author xuxueli 2015-12-19 16:13:16
+ */
+@Controller
+public class IndexController {
+
+	@Resource
+	private XxlJobService xxlJobService;
+	@Resource
+	private XxlJobLoginService loginService;
+
+
+	@RequestMapping("/")
+	public String index(Model model) {
+
+		Map<String, Object> dashboardMap = xxlJobService.dashboardInfo();
+		model.addAllAttributes(dashboardMap);
+
+		return "index";
+	}
+
+    @RequestMapping("/chartInfo")
+	@ResponseBody
+	public ReturnT<Map<String, Object>> chartInfo(@RequestParam("startDate") Date startDate, @RequestParam("endDate") Date endDate) {
+        ReturnT<Map<String, Object>> chartInfo = xxlJobService.chartInfo(startDate, endDate);
+        return chartInfo;
+    }
+	
+	@RequestMapping("/toLogin")
+	@PermissionLimit(limit=false)
+	public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView) {
+		if (loginService.ifLogin(request, response) != null) {
+			modelAndView.setView(new RedirectView("/",true,false));
+			return modelAndView;
+		}
+		return new ModelAndView("login");
+	}
+	
+	@RequestMapping(value="login", method=RequestMethod.POST)
+	@ResponseBody
+	@PermissionLimit(limit=false)
+	public ReturnT<String> loginDo(HttpServletRequest request,
+								   HttpServletResponse response,
+								   @RequestParam("userName") String userName,
+								   @RequestParam("password") String password,
+								   @RequestParam(value = "ifRemember", required = false) String ifRemember){
+
+		boolean ifRem = (ifRemember!=null && ifRemember.trim().length()>0 && "on".equals(ifRemember))?true:false;
+		return loginService.login(request, response, userName, password, ifRem);
+	}
+	
+	@RequestMapping(value="logout", method=RequestMethod.POST)
+	@ResponseBody
+	@PermissionLimit(limit=false)
+	public ReturnT<String> logout(HttpServletRequest request, HttpServletResponse response){
+		return loginService.logout(request, response);
+	}
+	
+	@RequestMapping("/help")
+	public String help() {
+
+		/*if (!PermissionInterceptor.ifLogin(request)) {
+			return "redirect:/toLogin";
+		}*/
+
+		return "help";
+	}
+
+	@InitBinder
+	public void initBinder(WebDataBinder binder) {
+		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+		dateFormat.setLenient(false);
+		binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
+	}
+	
+}

+ 105 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java

@@ -0,0 +1,105 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
+import com.xxl.job.admin.service.HandlerNameService;
+import com.xxl.job.core.biz.AdminBiz;
+import com.xxl.job.core.biz.model.HandleCallbackParam;
+import com.xxl.job.core.biz.model.RegistryParam;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.util.GsonTool;
+import com.xxl.job.core.util.XxlJobRemotingUtil;
+import jnpf.scheduletask.entity.HandlerNameEntity;
+import jnpf.util.JsonUtil;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import com.xxl.job.core.biz.model.RegistryHandlerName;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/5/10.
+ */
+@Controller
+@RequestMapping("/api")
+public class JobApiController {
+
+    @Resource
+    private AdminBiz adminBiz;
+    @Autowired
+    private HandlerNameService handlerNameService;
+
+    /**
+     * api
+     *
+     * @param uri
+     * @param data
+     * @return
+     */
+    @RequestMapping("/{uri}")
+    @ResponseBody
+    @PermissionLimit(limit=false)
+    public ReturnT<String> api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) {
+
+        // valid
+        if (!"POST".equalsIgnoreCase(request.getMethod())) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
+        }
+        if (uri==null || uri.trim().length()==0) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
+        }
+        if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null
+                && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0
+                && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
+        }
+
+        // services mapping
+        if ("callback".equals(uri)) {
+            List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
+            return adminBiz.callback(callbackParamList);
+        } else if ("registry".equals(uri)) {
+            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
+            return adminBiz.registry(registryParam);
+        } else if ("registryRemove".equals(uri)) {
+            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
+            return adminBiz.registryRemove(registryParam);
+        } else {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
+        }
+
+    }
+
+    /**
+     * api
+     *
+     * @param data
+     * @return
+     */
+    @RequestMapping("/{uri}/handlerName")
+    @ResponseBody
+    @PermissionLimit(limit=false)
+    public ReturnT<String> registryHandler(@PathVariable("uri") String uri, @RequestBody(required = false) String data) {
+        RegistryHandlerName handlerName = JsonUtil.getJsonToBean(data, RegistryHandlerName.class);
+        if (handlerName != null) {
+            for (String hand : handlerName.getHandlerList()) {
+                HandlerNameEntity entity = new HandlerNameEntity();
+                entity.setExecutor(handlerName.getExecutorName());
+                entity.setHandlerName(hand);
+                // 注册前先删除
+                handlerNameService.delete(entity);
+                if (!"remove".equals(uri)) {
+                    handlerNameService.create(entity);
+                }
+            }
+        }
+        return new ReturnT<>(ReturnT.SUCCESS_CODE, "处理成功");
+    }
+
+}

+ 106 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java

@@ -0,0 +1,106 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.controller.interceptor.PermissionInterceptor;
+import com.xxl.job.admin.service.XxlJobInfoService;
+import com.xxl.job.admin.service.XxlJobLogGlueService;
+import jnpf.scheduletask.entity.XxlJobInfo;
+import com.xxl.job.admin.core.model.XxlJobLogGlue;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.glue.GlueTypeEnum;
+import org.springframework.beans.factory.annotation.Autowired;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * job code controller
+ * @author xuxueli 2015-12-19 16:13:16
+ */
+@Controller
+@RequestMapping("/jobcode")
+public class JobCodeController {
+
+	@Autowired
+	private XxlJobInfoService xxlJobInfoService;
+	@Resource
+	private XxlJobLogGlueService xxlJobLogGlueService;
+
+	@RequestMapping
+	public String index(HttpServletRequest request, Model model, @RequestParam("jobId") String jobId) {
+		XxlJobInfo jobInfo = xxlJobInfoService.loadById(jobId);
+		List<XxlJobLogGlue> jobLogGlues = xxlJobLogGlueService.findByJobId(jobId);
+
+		if (jobInfo == null) {
+			throw new RuntimeException(I18nUtil.getString("jobinfo_glue_jobid_unvalid"));
+		}
+		if (GlueTypeEnum.BEAN == GlueTypeEnum.match(jobInfo.getGlueType())) {
+			throw new RuntimeException(I18nUtil.getString("jobinfo_glue_gluetype_unvalid"));
+		}
+
+		// valid permission
+		PermissionInterceptor.validJobGroupPermission(request, jobInfo.getJobGroup());
+
+		// Glue类型-字典
+		model.addAttribute("GlueTypeEnum", GlueTypeEnum.values());
+
+		model.addAttribute("jobInfo", jobInfo);
+		model.addAttribute("jobLogGlues", jobLogGlues);
+		return "jobcode/jobcode.index";
+	}
+	
+	@RequestMapping("/save")
+	@ResponseBody
+	public ReturnT<String> save(HttpServletRequest request,
+								@RequestParam("id") String id,
+								@RequestParam("glueSource") String glueSource,
+								@RequestParam("glueRemark") String glueRemark) {
+
+		// valid
+		if (glueRemark==null) {
+			return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_remark")) );
+		}
+		if (glueRemark.length()<4 || glueRemark.length()>100) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_remark_limit"));
+		}
+		XxlJobInfo existsJobInfo = xxlJobInfoService.loadById(id);
+		if (existsJobInfo == null) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid"));
+		}
+
+		// valid permission
+		PermissionInterceptor.validJobGroupPermission(request, existsJobInfo.getJobGroup());
+		
+		// update new code
+		existsJobInfo.setGlueSource(glueSource);
+		existsJobInfo.setGlueRemark(glueRemark);
+		existsJobInfo.setGlueUpdatetime(new Date());
+
+		existsJobInfo.setUpdateTime(new Date());
+		xxlJobInfoService.update(existsJobInfo);
+
+		// log old code
+		XxlJobLogGlue xxlJobLogGlue = new XxlJobLogGlue();
+		xxlJobLogGlue.setJobId(existsJobInfo.getId());
+		xxlJobLogGlue.setGlueType(existsJobInfo.getGlueType());
+		xxlJobLogGlue.setGlueSource(glueSource);
+		xxlJobLogGlue.setGlueRemark(glueRemark);
+
+		xxlJobLogGlue.setAddTime(new Date());
+		xxlJobLogGlue.setUpdateTime(new Date());
+		xxlJobLogGlueService.save(xxlJobLogGlue);
+
+		// remove code backup more than 30
+		xxlJobLogGlueService.removeOld(existsJobInfo.getId(), 30);
+
+		return ReturnT.SUCCESS;
+	}
+	
+}

+ 206 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java

@@ -0,0 +1,206 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.service.XxlJobGroupService;
+import com.xxl.job.admin.service.XxlJobInfoService;
+import com.xxl.job.admin.service.XxlJobRegistryService;
+import jnpf.scheduletask.entity.XxlJobGroup;
+import com.xxl.job.admin.core.model.XxlJobRegistry;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.enums.RegistryConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.util.*;
+
+/**
+ * job group controller
+ * @author xuxueli 2016-10-02 20:52:56
+ */
+@Controller
+@RequestMapping("/jobgroup")
+public class JobGroupController {
+
+	@Autowired
+	public XxlJobInfoService xxlJobInfoService;
+	@Autowired
+	public XxlJobGroupService xxlJobGroupService;
+	@Autowired
+	private XxlJobRegistryService xxlJobRegistryService;
+
+	@RequestMapping
+	@PermissionLimit(adminuser = true)
+	public String index(Model model) {
+		return "jobgroup/jobgroup.index";
+	}
+
+	@RequestMapping("/pageList")
+	@ResponseBody
+	@PermissionLimit(adminuser = true)
+	public Map<String, Object> pageList(HttpServletRequest request,
+										@RequestParam(value = "start", required = false, defaultValue = "0") int start,
+										@RequestParam(value = "length", required = false, defaultValue = "10") int length,
+										@RequestParam("appname") String appname,
+										@RequestParam("title") String title) {
+
+		// page query
+		List<XxlJobGroup> list = xxlJobGroupService.pageList(start, length, appname, title);
+		long list_count = xxlJobGroupService.pageListCount(start, length, appname, title);
+
+		// package result
+		Map<String, Object> maps = new HashMap<String, Object>();
+		maps.put("recordsTotal", list_count);		// 总记录数
+		maps.put("recordsFiltered", list_count);	// 过滤后的总记录数
+		maps.put("data", list);  					// 分页列表
+		return maps;
+	}
+
+	@RequestMapping("/save")
+	@ResponseBody
+	@PermissionLimit(adminuser = true)
+	public ReturnT<String> save(XxlJobGroup xxlJobGroup){
+
+		// valid
+		if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) {
+			return new ReturnT<String>(500, (I18nUtil.getString("system_please_input")+"AppName") );
+		}
+		if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_appname_length") );
+		}
+		if (xxlJobGroup.getAppname().contains(">") || xxlJobGroup.getAppname().contains("<")) {
+			return new ReturnT<String>(500, "AppName"+I18nUtil.getString("system_unvalid") );
+		}
+		if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) {
+			return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) );
+		}
+		if (xxlJobGroup.getTitle().contains(">") || xxlJobGroup.getTitle().contains("<")) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_title")+I18nUtil.getString("system_unvalid") );
+		}
+		if (xxlJobGroup.getAddressType()!=0) {
+			if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) {
+				return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_addressType_limit") );
+			}
+			if (xxlJobGroup.getAddressList().contains(">") || xxlJobGroup.getAddressList().contains("<")) {
+				return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList")+I18nUtil.getString("system_unvalid") );
+			}
+
+			String[] addresss = xxlJobGroup.getAddressList().split(",");
+			for (String item: addresss) {
+				if (item==null || item.trim().length()==0) {
+					return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") );
+				}
+			}
+		}
+
+		// process
+		xxlJobGroup.setUpdateTime(new Date());
+
+		int ret = xxlJobGroupService.create(xxlJobGroup);
+		return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL;
+	}
+
+	@RequestMapping("/update")
+	@ResponseBody
+	@PermissionLimit(adminuser = true)
+	public ReturnT<String> update(XxlJobGroup xxlJobGroup){
+		// valid
+		if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) {
+			return new ReturnT<String>(500, (I18nUtil.getString("system_please_input")+"AppName") );
+		}
+		if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_appname_length") );
+		}
+		if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) {
+			return new ReturnT<String>(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) );
+		}
+		if (xxlJobGroup.getAddressType() == 0) {
+			// 0=自动注册
+			List<String> registryList = findRegistryByAppName(xxlJobGroup.getAppname());
+			String addressListStr = null;
+			if (registryList!=null && !registryList.isEmpty()) {
+				Collections.sort(registryList);
+				addressListStr = "";
+				for (String item:registryList) {
+					addressListStr += item + ",";
+				}
+				addressListStr = addressListStr.substring(0, addressListStr.length()-1);
+			}
+			xxlJobGroup.setAddressList(addressListStr);
+		} else {
+			// 1=手动录入
+			if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) {
+				return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_addressType_limit") );
+			}
+			String[] addresss = xxlJobGroup.getAddressList().split(",");
+			for (String item: addresss) {
+				if (item==null || item.trim().length()==0) {
+					return new ReturnT<String>(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") );
+				}
+			}
+		}
+
+		// process
+		xxlJobGroup.setUpdateTime(new Date());
+
+		int ret = xxlJobGroupService.update(xxlJobGroup);
+		return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL;
+	}
+
+	private List<String> findRegistryByAppName(String appnameParam){
+		HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
+		List<XxlJobRegistry> list = xxlJobRegistryService.findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
+		if (list != null) {
+			for (XxlJobRegistry item: list) {
+				if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
+					String appname = item.getRegistryKey();
+					List<String> registryList = appAddressMap.get(appname);
+					if (registryList == null) {
+						registryList = new ArrayList<String>();
+					}
+
+					if (!registryList.contains(item.getRegistryValue())) {
+						registryList.add(item.getRegistryValue());
+					}
+					appAddressMap.put(appname, registryList);
+				}
+			}
+		}
+		return appAddressMap.get(appnameParam);
+	}
+
+	@RequestMapping("/remove")
+	@ResponseBody
+	@PermissionLimit(adminuser = true)
+	public ReturnT<String> remove(@RequestParam("id") String id){
+
+		// valid
+		long count = xxlJobInfoService.pageListCount(0, 10, id, -1,  null, null, null);
+		if (count > 0) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobgroup_del_limit_0") );
+		}
+
+		List<XxlJobGroup> allList = xxlJobGroupService.findAll();
+		if (allList.size() == 1) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobgroup_del_limit_1") );
+		}
+
+		int ret = xxlJobGroupService.remove(id);
+		return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL;
+	}
+
+	@RequestMapping("/loadById")
+	@ResponseBody
+	@PermissionLimit(adminuser = true)
+	public ReturnT<XxlJobGroup> loadById(@RequestParam("id") String id){
+		XxlJobGroup jobGroup = xxlJobGroupService.load(id);
+		return jobGroup!=null?new ReturnT<XxlJobGroup>(jobGroup):new ReturnT<XxlJobGroup>(ReturnT.FAIL_CODE, null);
+	}
+
+}

+ 163 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java

@@ -0,0 +1,163 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.controller.interceptor.PermissionInterceptor;
+import com.xxl.job.admin.core.exception.XxlJobException;
+import com.xxl.job.admin.service.XxlJobGroupService;
+import jnpf.scheduletask.entity.XxlJobGroup;
+import jnpf.scheduletask.entity.XxlJobInfo;
+import com.xxl.job.admin.core.model.XxlJobUser;
+import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
+import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
+import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
+import com.xxl.job.admin.core.thread.JobScheduleHelper;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.admin.service.XxlJobService;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
+import com.xxl.job.core.glue.GlueTypeEnum;
+import com.xxl.job.core.util.DateUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.*;
+
+/**
+ * index controller
+ * @author xuxueli 2015-12-19 16:13:16
+ */
+@Controller
+@RequestMapping("/jobinfo")
+public class JobInfoController {
+	private static Logger logger = LoggerFactory.getLogger(JobInfoController.class);
+
+	@Autowired
+	private XxlJobGroupService xxlJobGroupService;
+	@Autowired
+	private XxlJobService xxlJobService;
+
+	@RequestMapping
+	public String index(HttpServletRequest request, Model model, @RequestParam(value = "jobGroup", required = false, defaultValue = "-1") String jobGroup) {
+		// 枚举-字典
+		model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values());	    // 路由策略-列表
+		model.addAttribute("GlueTypeEnum", GlueTypeEnum.values());								// Glue类型-字典
+		model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values());	    // 阻塞处理策略-字典
+		model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values());	    				// 调度类型
+		model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values());	    			// 调度过期策略
+
+		// 执行器列表
+		List<XxlJobGroup> jobGroupList_all =  xxlJobGroupService.findAll();
+
+		// filter group
+		List<XxlJobGroup> jobGroupList = PermissionInterceptor.filterJobGroupByRole(request, jobGroupList_all);
+		if (jobGroupList==null || jobGroupList.size()==0) {
+			throw new XxlJobException(I18nUtil.getString("jobgroup_empty"));
+		}
+
+		model.addAttribute("JobGroupList", jobGroupList);
+		model.addAttribute("jobGroup", jobGroup);
+
+		return "jobinfo/jobinfo.index";
+	}
+
+	@RequestMapping("/pageList")
+	@ResponseBody
+	public Map<String, Object> pageList(@RequestParam(value = "start", required = false, defaultValue = "0") int start,
+										@RequestParam(value = "length", required = false, defaultValue = "10") int length,
+										@RequestParam("jobGroup") String jobGroup,
+										@RequestParam("triggerStatus") int triggerStatus,
+										@RequestParam("jobDesc") String jobDesc,
+										@RequestParam("executorHandler") String executorHandler,
+										@RequestParam("author") String author) {
+		
+		return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);
+	}
+	
+	@RequestMapping("/add")
+	@ResponseBody
+	public ReturnT<String> add(HttpServletRequest request, XxlJobInfo jobInfo) {
+		// valid permission
+		PermissionInterceptor.validJobGroupPermission(request, jobInfo.getJobGroup());
+
+		// opt
+		XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
+		return xxlJobService.add(jobInfo, loginUser);
+	}
+	
+	@RequestMapping("/update")
+	@ResponseBody
+	public ReturnT<String> update(HttpServletRequest request, XxlJobInfo jobInfo) {
+		// valid permission
+		PermissionInterceptor.validJobGroupPermission(request, jobInfo.getJobGroup());
+
+		// opt
+		XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
+		return xxlJobService.update(jobInfo, loginUser);
+	}
+	
+	@RequestMapping("/remove")
+	@ResponseBody
+	public ReturnT<String> remove(@RequestParam("id") String id) {
+		return xxlJobService.remove(id);
+	}
+	
+	@RequestMapping("/stop")
+	@ResponseBody
+	public ReturnT<String> pause(@RequestParam("id") String id) {
+		return xxlJobService.stop(id);
+	}
+	
+	@RequestMapping("/start")
+	@ResponseBody
+	public ReturnT<String> start(@RequestParam("id") String id) {
+		return xxlJobService.start(id);
+	}
+	
+	@RequestMapping("/trigger")
+	@ResponseBody
+	public ReturnT<String> triggerJob(HttpServletRequest request,
+									  @RequestParam("id") String id,
+									  @RequestParam("executorParam") String executorParam,
+									  @RequestParam("addressList") String addressList) {
+
+		// login user
+		XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
+		// trigger
+		return xxlJobService.trigger(loginUser, id, executorParam, addressList);
+	}
+
+	@RequestMapping("/nextTriggerTime")
+	@ResponseBody
+	public ReturnT<List<String>> nextTriggerTime(@RequestParam("scheduleType") String scheduleType,
+												 @RequestParam("scheduleConf") String scheduleConf) {
+
+		XxlJobInfo paramXxlJobInfo = new XxlJobInfo();
+		paramXxlJobInfo.setScheduleType(scheduleType);
+		paramXxlJobInfo.setScheduleConf(scheduleConf);
+
+		List<String> result = new ArrayList<>();
+		try {
+			Date lastTime = new Date();
+			for (int i = 0; i < 5; i++) {
+				lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime);
+				if (lastTime != null) {
+					result.add(DateUtil.formatDateTime(lastTime));
+				} else {
+					break;
+				}
+			}
+		} catch (Exception e) {
+			logger.error("nextTriggerTime error. scheduleType = {}, scheduleConf= {}", scheduleType, scheduleConf, e);
+			return new ReturnT<List<String>>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage());
+		}
+		return new ReturnT<List<String>>(result);
+
+	}
+	
+}

+ 258 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java

@@ -0,0 +1,258 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.controller.interceptor.PermissionInterceptor;
+import com.xxl.job.admin.core.complete.XxlJobCompleter;
+import com.xxl.job.admin.core.exception.XxlJobException;
+import com.xxl.job.admin.core.complete.XxlJobCompleter;
+import com.xxl.job.admin.service.XxlJobLogService;
+import com.xxl.job.admin.service.XxlJobGroupService;
+import com.xxl.job.admin.service.XxlJobInfoService;
+import jnpf.scheduletask.entity.XxlJobGroup;
+import jnpf.scheduletask.entity.XxlJobInfo;
+import jnpf.scheduletask.entity.XxlJobLog;
+import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.ExecutorBiz;
+import com.xxl.job.core.biz.model.KillParam;
+import com.xxl.job.core.biz.model.LogParam;
+import com.xxl.job.core.biz.model.LogResult;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.util.DateUtil;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.util.HtmlUtils;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * index controller
+ * @author xuxueli 2015-12-19 16:13:16
+ */
+@Controller
+@RequestMapping("/joblog")
+public class JobLogController {
+	private static Logger logger = LoggerFactory.getLogger(JobLogController.class);
+
+	@Autowired
+	private XxlJobGroupService xxlJobGroupService;
+	@Autowired
+	public XxlJobInfoService xxlJobInfoDao;
+	@Autowired
+	public XxlJobLogService xxlJobLogService;
+
+	@RequestMapping
+	public String index(HttpServletRequest request, Model model, @RequestParam(value = "jobId", required = false, defaultValue = "0") String jobId) {
+
+		// 执行器列表
+		List<XxlJobGroup> jobGroupList_all =  xxlJobGroupService.findAll();
+
+		// filter group
+		List<XxlJobGroup> jobGroupList = PermissionInterceptor.filterJobGroupByRole(request, jobGroupList_all);
+		if (jobGroupList==null || jobGroupList.size()==0) {
+			throw new XxlJobException(I18nUtil.getString("jobgroup_empty"));
+		}
+
+		model.addAttribute("JobGroupList", jobGroupList);
+
+		// 任务
+		if (!jobId.equals("0")) {
+			XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId);
+			if (jobInfo == null) {
+				throw new RuntimeException(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_unvalid"));
+			}
+
+			model.addAttribute("jobInfo", jobInfo);
+
+			// valid permission
+			PermissionInterceptor.validJobGroupPermission(request, jobInfo.getJobGroup());
+		}
+
+		return "joblog/joblog.index";
+	}
+
+	@RequestMapping("/getJobsByGroup")
+	@ResponseBody
+	public ReturnT<List<XxlJobInfo>> getJobsByGroup(@RequestParam("jobGroup") String jobGroup){
+		List<XxlJobInfo> list = xxlJobInfoDao.getJobsByGroup(jobGroup);
+		return new ReturnT<List<XxlJobInfo>>(list);
+	}
+	
+	@RequestMapping("/pageList")
+	@ResponseBody
+	public Map<String, Object> pageList(HttpServletRequest request,
+										@RequestParam(value = "start", required = false, defaultValue = "0") int start,
+										@RequestParam(value = "length", required = false, defaultValue = "10") int length,
+										@RequestParam("jobGroup") String jobGroup,
+										@RequestParam("jobId") String jobId,
+										@RequestParam("logStatus") int logStatus,
+										@RequestParam("filterTime") String filterTime) {
+
+		// valid permission
+		PermissionInterceptor.validJobGroupPermission(request, jobGroup);	// 仅管理员支持查询全部;普通用户仅支持查询有权限的 jobGroup
+		
+		// parse param
+		Date triggerTimeStart = null;
+		Date triggerTimeEnd = null;
+		if (filterTime!=null && filterTime.trim().length()>0) {
+			String[] temp = filterTime.split(" - ");
+			if (temp.length == 2) {
+				triggerTimeStart = DateUtil.parseDateTime(temp[0]);
+				triggerTimeEnd = DateUtil.parseDateTime(temp[1]);
+			}
+		}
+		
+		// page query
+		List<XxlJobLog> list = xxlJobLogService.pageList(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus);
+		int list_count = xxlJobLogService.pageListCount(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus);
+		
+		// package result
+		Map<String, Object> maps = new HashMap<String, Object>();
+	    maps.put("recordsTotal", list_count);		// 总记录数
+	    maps.put("recordsFiltered", list_count);	// 过滤后的总记录数
+	    maps.put("data", list);  					// 分页列表
+		return maps;
+	}
+
+	@RequestMapping("/logDetailPage")
+	public String logDetailPage(@RequestParam("id") String id, Model model){
+
+		// base check
+		ReturnT<String> logStatue = ReturnT.SUCCESS;
+		XxlJobLog jobLog = xxlJobLogService.load(id);
+		if (jobLog == null) {
+            throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid"));
+		}
+
+        model.addAttribute("triggerCode", jobLog.getTriggerCode());
+        model.addAttribute("handleCode", jobLog.getHandleCode());
+        model.addAttribute("logId", jobLog.getId());
+		return "joblog/joblog.detail";
+	}
+
+	@RequestMapping("/logDetailCat")
+	@ResponseBody
+	public ReturnT<LogResult> logDetailCat(@RequestParam("logId") String logId, @RequestParam("fromLineNum") int fromLineNum){
+		try {
+			// valid
+			XxlJobLog jobLog = xxlJobLogService.load(logId);	// todo, need to improve performance
+			if (jobLog == null) {
+				return new ReturnT<LogResult>(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_logid_unvalid"));
+			}
+
+			// log cat
+			ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(jobLog.getExecutorAddress());
+			ReturnT<LogResult> logResult = executorBiz.log(new LogParam(jobLog.getTriggerTime().getTime(), logId, fromLineNum));
+
+			// is end
+            if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) {
+                if (jobLog.getHandleCode() > 0) {
+                    logResult.getContent().setEnd(true);
+                }
+            }
+
+			// fix xss
+			if (logResult.getContent()!=null && StringUtils.hasText(logResult.getContent().getLogContent())) {
+				String newLogContent = logResult.getContent().getLogContent();
+				newLogContent = HtmlUtils.htmlEscape(newLogContent, "UTF-8");
+				logResult.getContent().setLogContent(newLogContent);
+			}
+
+			return logResult;
+		} catch (Exception e) {
+			logger.error(e.getMessage(), e);
+			return new ReturnT<LogResult>(ReturnT.FAIL_CODE, e.getMessage());
+		}
+	}
+
+	@RequestMapping("/logKill")
+	@ResponseBody
+	public ReturnT<String> logKill(@RequestParam("id") String id){
+		// base check
+		XxlJobLog log = xxlJobLogService.load(id);
+		XxlJobInfo jobInfo = xxlJobInfoDao.loadById(log.getJobId());
+		if (jobInfo==null) {
+			return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid"));
+		}
+		if (ReturnT.SUCCESS_CODE != log.getTriggerCode()) {
+			return new ReturnT<String>(500, I18nUtil.getString("joblog_kill_log_limit"));
+		}
+
+		// request of kill
+		ReturnT<String> runResult = null;
+		try {
+			ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(log.getExecutorAddress());
+			runResult = executorBiz.kill(new KillParam(jobInfo.getId()));
+		} catch (Exception e) {
+			logger.error(e.getMessage(), e);
+			runResult = new ReturnT<String>(500, e.getMessage());
+		}
+
+		if (ReturnT.SUCCESS_CODE == runResult.getCode()) {
+			log.setHandleCode(ReturnT.FAIL_CODE);
+			log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():""));
+			log.setHandleTime(new Date());
+			XxlJobCompleter.updateHandleInfoAndFinish(log);
+			return new ReturnT<String>(runResult.getMsg());
+		} else {
+			return new ReturnT<String>(500, runResult.getMsg());
+		}
+	}
+
+	@RequestMapping("/clearLog")
+	@ResponseBody
+	public ReturnT<String> clearLog(HttpServletRequest request,
+									@RequestParam("jobGroup") String jobGroup,
+									@RequestParam("jobId") String jobId,
+									@RequestParam("type") int type){
+		// valid permission
+		PermissionInterceptor.validJobGroupPermission(request, jobGroup);
+
+		// opt
+		Date clearBeforeTime = null;
+		int clearBeforeNum = 0;
+		if (type == 1) {
+			clearBeforeTime = DateUtil.addMonths(new Date(), -1);	// 清理一个月之前日志数据
+		} else if (type == 2) {
+			clearBeforeTime = DateUtil.addMonths(new Date(), -3);	// 清理三个月之前日志数据
+		} else if (type == 3) {
+			clearBeforeTime = DateUtil.addMonths(new Date(), -6);	// 清理六个月之前日志数据
+		} else if (type == 4) {
+			clearBeforeTime = DateUtil.addYears(new Date(), -1);	// 清理一年之前日志数据
+		} else if (type == 5) {
+			clearBeforeNum = 1000;		// 清理一千条以前日志数据
+		} else if (type == 6) {
+			clearBeforeNum = 10000;		// 清理一万条以前日志数据
+		} else if (type == 7) {
+			clearBeforeNum = 30000;		// 清理三万条以前日志数据
+		} else if (type == 8) {
+			clearBeforeNum = 100000;	// 清理十万条以前日志数据
+		} else if (type == 9) {
+			clearBeforeNum = 0;			// 清理所有日志数据
+		} else {
+			return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_clean_type_unvalid"));
+		}
+
+		List<String> logIds = null;
+		do {
+			logIds = xxlJobLogService.findClearLogIds(jobGroup, jobId, clearBeforeTime, clearBeforeNum, 1000);
+			if (logIds!=null && logIds.size()>0) {
+				xxlJobLogService.clearLog(logIds);
+			}
+		} while (logIds!=null && logIds.size()>0);
+
+		return ReturnT.SUCCESS;
+	}
+
+}

+ 189 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobUserController.java

@@ -0,0 +1,189 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.controller.interceptor.PermissionInterceptor;
+import com.xxl.job.admin.service.XxlJobGroupService;
+import com.xxl.job.admin.service.XxlJobUserService;
+import com.xxl.job.admin.core.model.XxlJobUser;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.model.ReturnT;
+import jnpf.scheduletask.entity.XxlJobGroup;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.util.DigestUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author xuxueli 2019-05-04 16:39:50
+ */
+@Controller
+@RequestMapping("/user")
+public class JobUserController {
+
+    @Resource
+    private XxlJobUserService xxlJobUserService;
+    @Resource
+    private XxlJobGroupService xxlJobGroupService;
+
+    @RequestMapping
+    @PermissionLimit(adminuser = true)
+    public String index(Model model) {
+
+        // 执行器列表
+        List<XxlJobGroup> groupList = xxlJobGroupService.findAll();
+        model.addAttribute("groupList", groupList);
+
+        return "user/user.index";
+    }
+
+    @RequestMapping("/pageList")
+    @ResponseBody
+    @PermissionLimit(adminuser = true)
+    public Map<String, Object> pageList(@RequestParam(value = "start", required = false, defaultValue = "0") int start,
+                                        @RequestParam(value = "length", required = false, defaultValue = "10") int length,
+                                        @RequestParam("username") String username,
+                                        @RequestParam("role") int role) {
+
+        // page list
+        List<XxlJobUser> list = xxlJobUserService.pageList(start, length, username, role);
+        int list_count = xxlJobUserService.pageListCount(start, length, username, role);
+
+        // filter
+        if (list!=null && list.size()>0) {
+            for (XxlJobUser item: list) {
+                item.setPassword(null);
+            }
+        }
+
+        // package result
+        Map<String, Object> maps = new HashMap<String, Object>();
+        maps.put("recordsTotal", list_count);		// 总记录数
+        maps.put("recordsFiltered", list_count);	// 过滤后的总记录数
+        maps.put("data", list);  					// 分页列表
+        return maps;
+    }
+
+    @RequestMapping("/add")
+    @ResponseBody
+    @PermissionLimit(adminuser = true)
+    public ReturnT<String> add(XxlJobUser xxlJobUser) {
+
+        // valid username
+        if (!StringUtils.hasText(xxlJobUser.getUsername())) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") );
+        }
+        xxlJobUser.setUsername(xxlJobUser.getUsername().trim());
+        if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
+        }
+        // valid password
+        if (!StringUtils.hasText(xxlJobUser.getPassword())) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") );
+        }
+        xxlJobUser.setPassword(xxlJobUser.getPassword().trim());
+        if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
+        }
+        // md5 password
+        xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes()));
+
+        // check repeat
+        XxlJobUser existUser = xxlJobUserService.loadByUserName(xxlJobUser.getUsername());
+        if (existUser != null) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") );
+        }
+
+        // write
+        xxlJobUserService.create(xxlJobUser);
+        return ReturnT.SUCCESS;
+    }
+
+    @RequestMapping("/update")
+    @ResponseBody
+    @PermissionLimit(adminuser = true)
+    public ReturnT<String> update(HttpServletRequest request, XxlJobUser xxlJobUser) {
+
+        // avoid opt login seft
+        XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
+        if (loginUser.getUsername().equals(xxlJobUser.getUsername())) {
+            return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit"));
+        }
+
+        // valid password
+        if (StringUtils.hasText(xxlJobUser.getPassword())) {
+            xxlJobUser.setPassword(xxlJobUser.getPassword().trim());
+            if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) {
+                return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
+            }
+            // md5 password
+            xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes()));
+        } else {
+            xxlJobUser.setPassword(null);
+        }
+
+        // write
+        xxlJobUserService.update(xxlJobUser);
+        return ReturnT.SUCCESS;
+    }
+
+    @RequestMapping("/remove")
+    @ResponseBody
+    @PermissionLimit(adminuser = true)
+    public ReturnT<String> remove(HttpServletRequest request, @RequestParam("id") String id) {
+
+        // avoid opt login seft
+        XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
+        if (loginUser.getId().equals(id)) {
+            return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit"));
+        }
+
+        xxlJobUserService.delete(id);
+        return ReturnT.SUCCESS;
+    }
+
+    @RequestMapping("/updatePwd")
+    @ResponseBody
+    public ReturnT<String> updatePwd(HttpServletRequest request,
+                                     @RequestParam("password") String password,
+                                     @RequestParam("oldPassword") String oldPassword){
+
+        // valid
+        if (oldPassword==null || oldPassword.trim().isEmpty()){
+            return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("system_please_input") + I18nUtil.getString("change_pwd_field_oldpwd"));
+        }
+        if (password==null || password.trim().isEmpty()){
+            return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("system_please_input") + I18nUtil.getString("change_pwd_field_oldpwd"));
+        }
+        password = password.trim();
+        if (!(password.length()>=4 && password.length()<=20)) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
+        }
+
+        // md5 password
+        String md5OldPassword = DigestUtils.md5DigestAsHex(oldPassword.getBytes());
+        String md5Password = DigestUtils.md5DigestAsHex(password.getBytes());
+
+        // valid old pwd
+        XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
+        XxlJobUser existUser = xxlJobUserService.loadByUserName(loginUser.getUsername());
+        if (!md5OldPassword.equals(existUser.getPassword())) {
+            return new ReturnT<String>(ReturnT.FAIL.getCode(), I18nUtil.getString("change_pwd_field_oldpwd") + I18nUtil.getString("system_unvalid"));
+        }
+
+        // write new
+        existUser.setPassword(md5Password);
+        xxlJobUserService.update(existUser);
+
+        return ReturnT.SUCCESS;
+    }
+
+}

+ 29 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java

@@ -0,0 +1,29 @@
+package com.xxl.job.admin.controller.annotation;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 权限限制
+ * @author xuxueli 2015-12-12 18:29:02
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PermissionLimit {
+	
+	/**
+	 * 登录拦截 (默认拦截)
+	 */
+	boolean limit() default true;
+
+	/**
+	 * 要求管理员权限
+	 *
+	 * @return
+	 */
+	boolean adminuser() default false;
+
+}

+ 42 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java

@@ -0,0 +1,42 @@
+package com.xxl.job.admin.controller.interceptor;
+
+import com.xxl.job.admin.core.util.FtlUtil;
+import com.xxl.job.admin.core.util.I18nUtil;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.AsyncHandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.util.HashMap;
+
+/**
+ * push cookies to model as cookieMap
+ *
+ * @author xuxueli 2015-12-12 18:09:04
+ */
+@Component
+public class CookieInterceptor implements AsyncHandlerInterceptor {
+
+	@Override
+	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
+						   ModelAndView modelAndView) throws Exception {
+
+		// cookie
+		if (modelAndView!=null && request.getCookies()!=null && request.getCookies().length>0) {
+			HashMap<String, Cookie> cookieMap = new HashMap<String, Cookie>();
+			for (Cookie ck : request.getCookies()) {
+				cookieMap.put(ck.getName(), ck);
+			}
+			modelAndView.addObject("cookieMap", cookieMap);
+		}
+
+		// static method
+		if (modelAndView != null) {
+			modelAndView.addObject("I18nUtil", FtlUtil.generateStaticModel(I18nUtil.class.getName()));
+		}
+
+	}
+	
+}

+ 131 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java

@@ -0,0 +1,131 @@
+package com.xxl.job.admin.controller.interceptor;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.core.model.XxlJobUser;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.admin.service.impl.XxlJobLoginService;
+import jnpf.scheduletask.entity.XxlJobGroup;
+import org.springframework.beans.factory.annotation.Autowired;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.AsyncHandlerInterceptor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 权限拦截
+ *
+ * @author xuxueli 2015-12-12 18:09:04
+ */
+@Component
+public class PermissionInterceptor implements AsyncHandlerInterceptor {
+
+	@Autowired
+	private XxlJobLoginService loginService;
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+		
+		if (!(handler instanceof HandlerMethod)) {
+			return true;	// proceed with the next interceptor
+		}
+
+		// if need login
+		boolean needLogin = true;
+		boolean needAdminuser = false;
+		HandlerMethod method = (HandlerMethod)handler;
+		PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class);
+		if (permission!=null) {
+			needLogin = permission.limit();
+			needAdminuser = permission.adminuser();
+		}
+
+		if (needLogin) {
+			XxlJobUser loginUser = loginService.ifLogin(request, response);
+			if (loginUser == null) {
+				response.setStatus(302);
+				response.setHeader("location", request.getContextPath()+"/toLogin");
+				return false;
+			}
+			if (needAdminuser && loginUser.getRole()!=1) {
+				throw new RuntimeException(I18nUtil.getString("system_permission_limit"));
+			}
+
+			// set loginUser, with request
+			setLoginUser(request, loginUser);
+		}
+
+		return true;	// proceed with the next interceptor
+	}
+
+
+	// -------------------- permission tool --------------------
+
+	/**
+	 * set loginUser
+	 *
+	 * @param request
+	 * @param loginUser
+	 */
+	private static void setLoginUser(HttpServletRequest request, XxlJobUser loginUser){
+		request.setAttribute("loginUser", loginUser);
+	}
+
+	/**
+	 * get loginUser
+	 *
+	 * @param request
+	 * @return
+	 */
+	public static XxlJobUser getLoginUser(HttpServletRequest request){
+		XxlJobUser loginUser = (XxlJobUser) request.getAttribute("loginUser");	// get loginUser, with request
+		return loginUser;
+	}
+
+	/**
+	 * valid permission by JobGroup
+	 *
+	 * @param request
+	 * @param jobGroup
+	 */
+	public static void validJobGroupPermission(HttpServletRequest request, String jobGroup) {
+		XxlJobUser loginUser = getLoginUser(request);
+		if (!loginUser.validPermission(jobGroup)) {
+			throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]");
+		}
+	}
+
+	/**
+	 * filter XxlJobGroup by role
+	 *
+	 * @param request
+	 * @param jobGroupList_all
+	 * @return
+	 */
+	public static List<XxlJobGroup> filterJobGroupByRole(HttpServletRequest request, List<XxlJobGroup> jobGroupList_all){
+		List<XxlJobGroup> jobGroupList = new ArrayList<>();
+		if (jobGroupList_all!=null && !jobGroupList_all.isEmpty()) {
+			XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
+			if (loginUser.getRole() == 1) {
+				jobGroupList = jobGroupList_all;
+			} else {
+				List<String> groupIdStrs = new ArrayList<>();
+				if (loginUser.getPermission()!=null && !loginUser.getPermission().trim().isEmpty()) {
+					groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(","));
+				}
+				for (XxlJobGroup groupItem:jobGroupList_all) {
+					if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) {
+						jobGroupList.add(groupItem);
+					}
+				}
+			}
+		}
+		return jobGroupList;
+	}
+
+	
+}

+ 27 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java

@@ -0,0 +1,27 @@
+package com.xxl.job.admin.controller.interceptor;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * web mvc config
+ *
+ * @author xuxueli 2018-04-02 20:48:20
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Resource
+    private PermissionInterceptor permissionInterceptor;
+    @Resource
+    private CookieInterceptor cookieInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(permissionInterceptor).addPathPatterns("/**");
+        registry.addInterceptor(cookieInterceptor).addPathPatterns("/**");
+    }
+
+}

+ 65 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java

@@ -0,0 +1,65 @@
+package com.xxl.job.admin.controller.resolver;
+
+import com.xxl.job.admin.core.exception.XxlJobException;
+import com.xxl.job.admin.core.util.JacksonUtil;
+import com.xxl.job.core.biz.model.ReturnT;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.io.IOException;
+
+/**
+ * common exception resolver
+ *
+ * @author xuxueli 2016-1-6 19:22:18
+ */
+@Component
+public class WebExceptionResolver implements HandlerExceptionResolver {
+	private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class);
+
+	@Override
+	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+
+		if (!(ex instanceof XxlJobException)) {
+			logger.error("WebExceptionResolver:{}", ex);
+		}
+
+		// if json
+		boolean isJson = false;
+		if (handler instanceof HandlerMethod) {
+			HandlerMethod method = (HandlerMethod)handler;
+			ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class);
+			if (responseBody != null) {
+				isJson = true;
+			}
+		}
+
+		// error result
+		ReturnT<String> errorResult = new ReturnT<String>(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "<br/>"));
+
+		// response
+		ModelAndView mv = new ModelAndView();
+		if (isJson) {
+			try {
+				response.setContentType("application/json;charset=utf-8");
+				response.getWriter().print(JacksonUtil.writeValueAsString(errorResult));
+			} catch (IOException e) {
+				logger.error(e.getMessage(), e);
+			}
+			return mv;
+		} else {
+
+			mv.addObject("exceptionMsg", errorResult.getMsg());
+			mv.setViewName("/common/common.exception");
+			return mv;
+		}
+	}
+	
+}

+ 31 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/HandlerController.java

@@ -0,0 +1,31 @@
+package com.xxl.job.admin.controller.rest;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.service.HandlerNameService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jnpf.scheduletask.entity.HandlerNameEntity;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@Tag(name = "执行器", description = "Handler")
+@RestController
+@RequestMapping("/api/handler")
+public class HandlerController {
+
+    @Autowired
+    private HandlerNameService handlerNameService;
+
+    @Operation(summary = "获取执行器列表")
+    @GetMapping("/queryList")
+    @PermissionLimit(limit=false)
+    public List<HandlerNameEntity> queryList() {
+        List<HandlerNameEntity> queryList = handlerNameService.queryList();
+        return queryList;
+    }
+
+}

+ 52 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/LogController.java

@@ -0,0 +1,52 @@
+package com.xxl.job.admin.controller.rest;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jnpf.base.ActionResult;
+import jnpf.base.UserInfo;
+import jnpf.base.vo.PageListVO;
+import jnpf.base.vo.PaginationVO;
+import jnpf.scheduletask.entity.XxlJobInfo;
+import jnpf.scheduletask.entity.XxlJobLog;
+import com.xxl.job.admin.service.XxlJobLogService;
+import jnpf.scheduletask.model.TaskLogVO;
+import jnpf.scheduletask.model.TaskPage;
+import jnpf.util.JsonUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Tag(name = "任务调度日志", description = "Log")
+@RestController
+@RequestMapping("/api/log")
+public class LogController {
+
+    @Autowired
+    private XxlJobLogService xxlJobLogService;
+
+    @Operation(summary = "通过任务id获取日志列表")
+    @PostMapping("/{taskId}")
+    @PermissionLimit(limit=false)
+    public ActionResult<PageListVO<TaskLogVO>> getList(@PathVariable("taskId") String taskId, @RequestBody UserInfo userInfo, TaskPage taskPage) {
+        XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoService().queryByTaskId(taskId);
+        List<TaskLogVO> voList = new ArrayList<>(16);
+        if (xxlJobInfo != null) {
+            List<XxlJobLog> list = xxlJobLogService.getList(xxlJobInfo.getId(), taskPage);
+            for (XxlJobLog xxlJobLog : list) {
+                TaskLogVO taskLogVO = new TaskLogVO();
+                taskLogVO.setId(String.valueOf(xxlJobLog.getId()));
+                taskLogVO.setRunTime(xxlJobLog.getTriggerTime().getTime());
+                taskLogVO.setDescription(xxlJobLog.getTriggerMsg());
+                taskLogVO.setRunResult(xxlJobLog.getHandleCode() == 200 ? 0 : 1);
+                voList.add(taskLogVO);
+            }
+        }
+        PaginationVO pageModel = JsonUtil.getJsonToBean(taskPage, PaginationVO.class);
+        return ActionResult.page(voList, pageModel);
+    }
+
+}

+ 131 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/ScheduleTaskController.java

@@ -0,0 +1,131 @@
+package com.xxl.job.admin.controller.rest;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.service.TimetaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jnpf.base.ActionResult;
+import jnpf.base.Pagination;
+import jnpf.base.UserInfo;
+import jnpf.base.vo.PageListVO;
+import jnpf.base.vo.PaginationVO;
+import jnpf.constant.MsgCode;
+import jnpf.scheduletask.entity.TimeTaskEntity;
+import com.xxl.job.admin.service.XxlJobService;
+import jnpf.scheduletask.model.*;
+import jnpf.util.JsonUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "任务调度", description = "ScheduleTask")
+@RestController
+@RequestMapping("/api/ScheduleTask")
+public class ScheduleTaskController {
+
+    @Autowired
+    private XxlJobService xxlJobService;
+    @Autowired
+    private TimetaskService timetaskService;
+
+    @Operation(summary = "通过任务id获取任务详情")
+    @PostMapping("/List")
+    @PermissionLimit(limit=false)
+    public ActionResult<PageListVO<TaskVO>> getList(Pagination pagination, @RequestBody UserInfo userInfo) {
+        List<TimeTaskEntity> data = timetaskService.getList(pagination, userInfo);
+        List<TaskVO> list = JsonUtil.getJsonToList(data, TaskVO.class);
+        for (int i = 0; i < list.size(); i++) {
+            TimeTaskEntity timeTaskEntity = data.get(i);
+            TaskVO taskVO = list.get(i);
+            if (timeTaskEntity == null || taskVO == null) {
+                continue;
+            }
+            ContentNewModel contentNewModel = JsonUtil.getJsonToBean(timeTaskEntity.getExecuteContent(), ContentNewModel.class);
+            if (contentNewModel == null) {
+                continue;
+            }
+            taskVO.setStartTime(contentNewModel.getStartTime());
+            taskVO.setEndTime(contentNewModel.getEndTime());
+            taskVO.setNextRunTime(ObjectUtil.equal(taskVO.getNextRunTime(), 0L) ? null : taskVO.getNextRunTime());
+        }
+        PaginationVO paginationVO = JsonUtil.getJsonToBean(pagination, PaginationVO.class);
+        return ActionResult.page(list, paginationVO);
+    }
+
+    @Operation(summary = "通过任务id获取任务详情")
+    @PostMapping("/getInfo")
+    @PermissionLimit(limit=false)
+    public TimeTaskEntity queryByTaskId(@RequestParam(value = "taskId", required = false) String taskId, @RequestBody UserInfo userInfo) {
+        TimeTaskEntity entity = timetaskService.getInfo(taskId, userInfo);
+        return entity;
+    }
+
+    @Operation(summary = "日程任务调度")
+    @PostMapping("/schedule")
+    @PermissionLimit(limit=false)
+    public ActionResult schedule(@RequestBody TaskCrForm taskCrForm) {
+        TimeTaskEntity entity = JsonUtil.getJsonToBean(taskCrForm, TimeTaskEntity.class);
+        timetaskService.schedule(entity);
+        return ActionResult.success(MsgCode.SU001.get());
+    }
+
+    @Operation(summary = "保存任务调度")
+    @PostMapping
+    @PermissionLimit(limit=false)
+    public ActionResult save(@RequestBody TaskCrForm taskCrForm) {
+        UserInfo userInfo = taskCrForm.getUserInfo();
+        TimeTaskEntity entity = JsonUtil.getJsonToBean(taskCrForm, TimeTaskEntity.class);
+        if (timetaskService.isExistByFullName(entity.getFullName(), entity.getId())) {
+            return ActionResult.fail("任务名称不能重复");
+        }
+        timetaskService.create(entity, userInfo);
+        return ActionResult.success(MsgCode.SU001.get());
+    }
+
+    @Operation(summary = "修改任务调度")
+    @PutMapping("/{id}")
+    @PermissionLimit(limit=false)
+    public ActionResult update(@PathVariable("id") String id, @RequestBody TaskUpForm taskUpForm) {
+        UserInfo userInfo = taskUpForm.getUserInfo();
+        TimeTaskEntity entity = JsonUtil.getJsonToBean(taskUpForm, TimeTaskEntity.class);
+        TimeTaskEntity taskEntity = timetaskService.getInfo(id, userInfo);
+        if (taskEntity == null) {
+            return ActionResult.fail(MsgCode.FA002.get());
+        }
+        if (timetaskService.isExistByFullName(entity.getFullName(), id)) {
+            return ActionResult.fail("任务名称不能重复");
+        }
+        entity.setRunCount(taskEntity.getRunCount());
+        boolean update = timetaskService.update(id, entity, userInfo);
+        if (!update) {
+            return ActionResult.fail(MsgCode.FA002.get());
+        }
+        return ActionResult.success(MsgCode.SU004.get());
+    }
+
+    @Operation(summary = "删除任务调度")
+    @PostMapping("/remove/{id}")
+    @PermissionLimit(limit=false)
+    public ActionResult remove(@PathVariable("id") String id, @RequestBody UserInfo userInfo) {
+        TimeTaskEntity entity = timetaskService.getInfo(id, userInfo);
+        if (entity != null) {
+            timetaskService.delete(entity);
+            return ActionResult.success(MsgCode.SU003.get());
+        }
+        return ActionResult.fail(MsgCode.FA003.get());
+    }
+
+    @Operation(summary = "修改任务调度")
+    @PostMapping("/updateTask")
+    @PermissionLimit(limit=false)
+    public ActionResult update(@RequestBody UpdateTaskModel model) {
+        boolean flag = timetaskService.update(model.getEntity().getId(), model.getEntity(), model.getUserInfo());
+        if (flag) {
+            return ActionResult.fail(MsgCode.FA002.get());
+        }
+        return ActionResult.success(MsgCode.SU004.get());
+    }
+
+}

+ 22 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/rest/XxlJobInfoController.java

@@ -0,0 +1,22 @@
+package com.xxl.job.admin.controller.rest;
+
+import com.xxl.job.admin.controller.annotation.PermissionLimit;
+import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
+import jnpf.scheduletask.entity.XxlJobInfo;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/jobinfo")
+public class XxlJobInfoController {
+
+    @GetMapping
+    @PermissionLimit(limit=false)
+    public XxlJobInfo getInfoByTaskId(@RequestParam(value = "taskId", required = false) String taskId) {
+        XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoService().queryByTaskId(taskId);
+        return xxlJobInfo;
+    }
+
+}

+ 20 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java

@@ -0,0 +1,20 @@
+package com.xxl.job.admin.core.alarm;
+
+import jnpf.scheduletask.entity.XxlJobInfo;
+import jnpf.scheduletask.entity.XxlJobLog;
+
+/**
+ * @author xuxueli 2020-01-19
+ */
+public interface JobAlarm {
+
+    /**
+     * job alarm
+     *
+     * @param info
+     * @param jobLog
+     * @return
+     */
+    public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog);
+
+}

+ 65 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java

@@ -0,0 +1,65 @@
+package com.xxl.job.admin.core.alarm;
+
+import jnpf.scheduletask.entity.XxlJobInfo;
+import jnpf.scheduletask.entity.XxlJobLog;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class JobAlarmer implements ApplicationContextAware, InitializingBean {
+    private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class);
+
+    private ApplicationContext applicationContext;
+    private List<JobAlarm> jobAlarmList;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        Map<String, JobAlarm> serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class);
+        if (serviceBeanMap != null && serviceBeanMap.size() > 0) {
+            jobAlarmList = new ArrayList<JobAlarm>(serviceBeanMap.values());
+        }
+    }
+
+    /**
+     * job alarm
+     *
+     * @param info
+     * @param jobLog
+     * @return
+     */
+    public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) {
+
+        boolean result = false;
+        if (jobAlarmList!=null && jobAlarmList.size()>0) {
+            result = true;  // success means all-success
+            for (JobAlarm alarm: jobAlarmList) {
+                boolean resultItem = false;
+                try {
+                    resultItem = alarm.doAlarm(info, jobLog);
+                } catch (Exception e) {
+                    logger.error(e.getMessage(), e);
+                }
+                if (!resultItem) {
+                    result = false;
+                }
+            }
+        }
+
+        return result;
+    }
+
+}

+ 118 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java

@@ -0,0 +1,118 @@
+package com.xxl.job.admin.core.alarm.impl;
+
+import com.xxl.job.admin.core.alarm.JobAlarm;
+import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
+import jnpf.scheduletask.entity.XxlJobGroup;
+import jnpf.scheduletask.entity.XxlJobInfo;
+import jnpf.scheduletask.entity.XxlJobLog;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.model.ReturnT;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Component;
+
+import jakarta.mail.internet.MimeMessage;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * job alarm by email
+ *
+ * @author xuxueli 2020-01-19
+ */
+@Component
+public class EmailJobAlarm implements JobAlarm {
+    private static Logger logger = LoggerFactory.getLogger(EmailJobAlarm.class);
+
+    /**
+     * fail alarm
+     *
+     * @param jobLog
+     */
+    @Override
+    public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog){
+        boolean alarmResult = true;
+
+        // send monitor email
+        if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) {
+
+            // alarmContent
+            String alarmContent = "Alarm Job LogId=" + jobLog.getId();
+            if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) {
+                alarmContent += "<br>TriggerMsg=<br>" + jobLog.getTriggerMsg();
+            }
+            if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != ReturnT.SUCCESS_CODE) {
+                alarmContent += "<br>HandleCode=" + jobLog.getHandleMsg();
+            }
+
+            // email info
+            XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupService().load(info.getJobGroup());
+            String personal = I18nUtil.getString("admin_name_full");
+            String title = I18nUtil.getString("jobconf_monitor");
+            String content = MessageFormat.format(loadEmailJobAlarmTemplate(),
+                    group!=null?group.getTitle():"null",
+                    info.getId(),
+                    info.getJobDesc(),
+                    alarmContent);
+
+            Set<String> emailSet = new HashSet<String>(Arrays.asList(info.getAlarmEmail().split(",")));
+            for (String email: emailSet) {
+
+                // make mail
+                try {
+                    MimeMessage mimeMessage = XxlJobAdminConfig.getAdminConfig().getMailSender().createMimeMessage();
+
+                    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
+                    helper.setFrom(XxlJobAdminConfig.getAdminConfig().getEmailFrom(), personal);
+                    helper.setTo(email);
+                    helper.setSubject(title);
+                    helper.setText(content, true);
+
+                    XxlJobAdminConfig.getAdminConfig().getMailSender().send(mimeMessage);
+                } catch (Exception e) {
+                    logger.error(">>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:{}", jobLog.getId(), e);
+
+                    alarmResult = false;
+                }
+
+            }
+        }
+
+        return alarmResult;
+    }
+
+    /**
+     * load email job alarm template
+     *
+     * @return
+     */
+    private static final String loadEmailJobAlarmTemplate(){
+        String mailBodyTemplate = "<h5>" + I18nUtil.getString("jobconf_monitor_detail") + ":</span>" +
+                "<table border=\"1\" cellpadding=\"3\" style=\"border-collapse:collapse; width:80%;\" >\n" +
+                "   <thead style=\"font-weight: bold;color: #ffffff;background-color: #ff8c00;\" >" +
+                "      <tr>\n" +
+                "         <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobgroup") +"</td>\n" +
+                "         <td width=\"10%\" >"+ I18nUtil.getString("jobinfo_field_id") +"</td>\n" +
+                "         <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobdesc") +"</td>\n" +
+                "         <td width=\"10%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_title") +"</td>\n" +
+                "         <td width=\"40%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_content") +"</td>\n" +
+                "      </tr>\n" +
+                "   </thead>\n" +
+                "   <tbody>\n" +
+                "      <tr>\n" +
+                "         <td>{0}</td>\n" +
+                "         <td>{1}</td>\n" +
+                "         <td>{2}</td>\n" +
+                "         <td>"+ I18nUtil.getString("jobconf_monitor_alarm_type") +"</td>\n" +
+                "         <td>{3}</td>\n" +
+                "      </tr>\n" +
+                "   </tbody>\n" +
+                "</table>";
+
+        return mailBodyTemplate;
+    }
+
+}

+ 104 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java

@@ -0,0 +1,104 @@
+package com.xxl.job.admin.core.complete;
+
+import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
+import jnpf.scheduletask.entity.XxlJobInfo;
+import jnpf.scheduletask.entity.XxlJobLog;
+import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
+import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.context.XxlJobContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.MessageFormat;
+import java.util.Objects;
+
+/**
+ * @author xuxueli 2020-10-30 20:43:10
+ */
+public class XxlJobCompleter {
+    private static Logger logger = LoggerFactory.getLogger(XxlJobCompleter.class);
+
+    /**
+     * common fresh handle entrance (limit only once)
+     *
+     * @param xxlJobLog
+     * @return
+     */
+    public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) {
+
+        // finish
+        finishJob(xxlJobLog);
+
+        // text最大64kb 避免长度过长
+        if (xxlJobLog.getHandleMsg().length() > 15000) {
+            xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) );
+        }
+
+        // fresh handle
+        return XxlJobAdminConfig.getAdminConfig().getXxlJobLogService().updateHandleInfo(xxlJobLog);
+    }
+
+
+    /**
+     * do somethind to finish job
+     */
+    private static void finishJob(XxlJobLog xxlJobLog){
+
+        // 1、handle success, to trigger child job
+        String triggerChildMsg = null;
+        if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) {
+            XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoService().loadById(xxlJobLog.getJobId());
+            if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) {
+                triggerChildMsg = ","+ I18nUtil.getString("jobconf_trigger_child_run") +",";
+
+                String[] childJobIds = xxlJobInfo.getChildJobId().split(",");
+                for (int i = 0; i < childJobIds.length; i++) {
+                    long childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Long.valueOf(childJobIds[i]):-1L;
+                    if (Objects.equals(childJobIds[i], xxlJobLog.getJobId())) {
+                        logger.info("jobid {} is self, ignore it.", childJobId);
+                        continue;
+                    }
+                    if (childJobId > 0) {
+
+                        JobTriggerPoolHelper.trigger(childJobIds[i], TriggerTypeEnum.PARENT, -1, null, null, null);
+                        ReturnT<String> triggerChildResult = ReturnT.SUCCESS;
+
+                        // add msg
+                        triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"),
+                                (i+1),
+                                childJobIds.length,
+                                childJobIds[i],
+                                (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")),
+                                triggerChildResult.getMsg());
+                    } else {
+                        triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"),
+                                (i+1),
+                                childJobIds.length,
+                                childJobIds[i]);
+                    }
+                }
+
+            }
+        }
+
+        if (triggerChildMsg != null) {
+            xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg );
+        }
+
+        // 2、fix_delay trigger next
+        // on the way
+
+    }
+
+    private static boolean isNumeric(String str){
+        try {
+            int result = Integer.valueOf(str);
+            return true;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
+}

+ 178 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java

@@ -0,0 +1,178 @@
+package com.xxl.job.admin.core.conf;
+
+import com.xxl.job.admin.core.alarm.JobAlarmer;
+import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
+import com.xxl.job.admin.service.*;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Component;
+
+import jakarta.annotation.Resource;
+import javax.sql.DataSource;
+import java.util.Arrays;
+
+/**
+ * xxl-job config
+ *
+ * @author xuxueli 2017-04-28
+ */
+
+@Component
+public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
+
+    private static XxlJobAdminConfig adminConfig = null;
+    public static XxlJobAdminConfig getAdminConfig() {
+        return adminConfig;
+    }
+
+
+    // ---------------------- XxlJobScheduler ----------------------
+
+    private XxlJobScheduler xxlJobScheduler;
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        adminConfig = this;
+
+        xxlJobScheduler = new XxlJobScheduler();
+        xxlJobScheduler.init();
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        xxlJobScheduler.destroy();
+    }
+
+
+    // ---------------------- XxlJobScheduler ----------------------
+
+    // conf
+    @Value("${xxl.job.i18n}")
+    private String i18n;
+
+    @Value("${xxl.job.accessToken}")
+    private String accessToken;
+
+    @Value("${xxl.job.timeout}")
+    private int timeout;
+
+    @Value("${spring.mail.from}")
+    private String emailFrom;
+
+    @Value("${xxl.job.triggerpool.fast.max}")
+    private int triggerPoolFastMax;
+
+    @Value("${xxl.job.triggerpool.slow.max}")
+    private int triggerPoolSlowMax;
+
+    @Value("${xxl.job.logretentiondays}")
+    private int logretentiondays;
+
+    // dao, service
+
+    @Resource
+    private XxlJobInfoService xxlJobInfoService;
+    @Resource
+    private XxlJobRegistryService xxlJobRegistryService;
+    @Resource
+    private XxlJobGroupService xxlJobGroupService;
+    @Resource
+    private XxlJobLogReportService xxlJobLogReportService;
+    @Resource
+    private JavaMailSender mailSender;
+    @Resource
+    private DataSource dataSource;
+    @Resource
+    private JobAlarmer jobAlarmer;
+    @Resource
+    private XxlJobLogService xxlJobLogService;
+    @Resource
+    private TimetaskService timetaskService;
+
+
+    public String getI18n() {
+        if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) {
+            return "zh_CN";
+        }
+        return i18n;
+    }
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public int getTimeout() {
+        return timeout;
+    }
+
+    public String getEmailFrom() {
+        return emailFrom;
+    }
+
+    public int getTriggerPoolFastMax() {
+        if (triggerPoolFastMax < 200) {
+            return 200;
+        }
+        return triggerPoolFastMax;
+    }
+
+    public int getTriggerPoolSlowMax() {
+        if (triggerPoolSlowMax < 100) {
+            return 100;
+        }
+        return triggerPoolSlowMax;
+    }
+
+    public int getLogretentiondays() {
+        if (logretentiondays < 7) {
+            return -1;  // Limit greater than or equal to 7, otherwise close
+        }
+        return logretentiondays;
+    }
+
+    public XxlJobInfoService getXxlJobInfoService() {
+        return xxlJobInfoService;
+    }
+
+    public XxlJobRegistryService getXxlJobRegistryService() {
+        return xxlJobRegistryService;
+    }
+
+    public XxlJobGroupService getXxlJobGroupService() {
+        return xxlJobGroupService;
+    }
+
+    public XxlJobLogReportService getXxlJobLogReportService() {
+        return xxlJobLogReportService;
+    }
+
+    public JavaMailSender getMailSender() {
+        return mailSender;
+    }
+
+    public DataSource getDataSource() {
+        return dataSource;
+    }
+
+    public JobAlarmer getJobAlarmer() {
+        return jobAlarmer;
+    }
+
+    public XxlJobLogService getXxlJobLogService() {
+        return xxlJobLogService;
+    }
+
+    public void setXxlJobLogService(XxlJobLogService xxlJobLogService) {
+        this.xxlJobLogService = xxlJobLogService;
+    }
+
+    public TimetaskService getTimetaskService() {
+        return timetaskService;
+    }
+
+    public void setTimetaskService(TimetaskService timetaskService) {
+        this.timetaskService = timetaskService;
+    }
+}

+ 1623 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java

@@ -0,0 +1,1623 @@
+package com.xxl.job.admin.core.cron;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.SortedSet;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.TreeSet;
+
+/**
+ * Provides a parser and evaluator for unix-like cron expressions. Cron
+ * expressions provide the ability to specify complex time combinations such as
+ * &quot;At 8:00am every Monday through Friday&quot; or &quot;At 1:30am every
+ * last Friday of the month&quot;.
+ * <p>
+ * Cron expressions are comprised of 6 required fields and one optional field
+ * separated by white space. The fields respectively are described as follows:
+ * </p>
+ * <table>
+ * <caption>Examples of cron expressions and their meanings.</caption>
+ * <tr>
+ * <th>Field Name</th>
+ * <th>&nbsp;</th>
+ * <th>Allowed Values</th>
+ * <th>&nbsp;</th>
+ * <th>Allowed Special Characters</th>
+ * </tr>
+ * <tr>
+ * <td><code>Seconds</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>0-59</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>, - * /</code></td>
+ * </tr>
+ * <tr>
+ * <td><code>Minutes</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>0-59</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>, - * /</code></td>
+ * </tr>
+ * <tr>
+ * <td><code>Hours</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>0-23</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>, - * /</code></td>
+ * </tr>
+ * <tr>
+ * <td><code>Day-of-month</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>1-31</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>, - * ? / L W</code></td>
+ * </tr>
+ * <tr>
+ * <td><code>Month</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>0-11 or JAN-DEC</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>, - * /</code></td>
+ * </tr>
+ * <tr>
+ * <td><code>Day-of-Week</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>1-7 or SUN-SAT</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>, - * ? / L #</code></td>
+ * </tr>
+ * <tr>
+ * <td><code>Year (Optional)</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>empty, 1970-2199</code></td>
+ * <td>&nbsp;</td>
+ * <td><code>, - * /</code></td>
+ * </tr>
+ * </table>
+ * <p>
+ * The '*' character is used to specify all values. For example, &quot;*&quot;
+ * in the minute field means &quot;every minute&quot;.
+ * </p>
+ * <p>
+ * The '?' character is allowed for the day-of-month and day-of-week fields. It
+ * is used to specify 'no specific value'. This is useful when you need to
+ * specify something in one of the two fields, but not the other.
+ * <p>
+ * The '-' character is used to specify ranges For example &quot;10-12&quot; in
+ * the hour field means &quot;the hours 10, 11 and 12&quot;.
+ * <p>
+ * The ',' character is used to specify additional values. For example
+ * &quot;MON,WED,FRI&quot; in the day-of-week field means &quot;the days Monday,
+ * Wednesday, and Friday&quot;.
+ * </p>
+ * <p>
+ * The '/' character is used to specify increments. For example &quot;0/15&quot;
+ * in the seconds field means &quot;the seconds 0, 15, 30, and 45&quot;. And
+ * &quot;5/15&quot; in the seconds field means &quot;the seconds 5, 20, 35, and
+ * 50&quot;.  Specifying '*' before the  '/' is equivalent to specifying 0 is
+ * the value to start with. Essentially, for each field in the expression, there
+ * is a set of numbers that can be turned on or off. For seconds and minutes,
+ * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
+ * 31, and for months 0 to 11 (JAN to DEC). The &quot;/&quot; character simply helps you turn
+ * on every &quot;nth&quot; value in the given set. Thus &quot;7/6&quot; in the
+ * month field only turns on month &quot;7&quot;, it does NOT mean every 6th
+ * month, please note that subtlety.
+ * </p>
+ * <p>
+ * The 'L' character is allowed for the day-of-month and day-of-week fields.
+ * This character is short-hand for &quot;last&quot;, but it has different
+ * meaning in each of the two fields. For example, the value &quot;L&quot; in
+ * the day-of-month field means &quot;the last day of the month&quot; - day 31
+ * for January, day 28 for February on non-leap years. If used in the
+ * day-of-week field by itself, it simply means &quot;7&quot; or
+ * &quot;SAT&quot;. But if used in the day-of-week field after another value, it
+ * means &quot;the last xxx day of the month&quot; - for example &quot;6L&quot;
+ * means &quot;the last friday of the month&quot;. You can also specify an offset
+ * from the last day of the month, such as "L-3" which would mean the third-to-last
+ * day of the calendar month. <i>When using the 'L' option, it is important not to
+ * specify lists, or ranges of values, as you'll get confusing/unexpected results.</i>
+ * </p>
+ * <p>
+ * The 'W' character is allowed for the day-of-month field.  This character
+ * is used to specify the weekday (Monday-Friday) nearest the given day.  As an
+ * example, if you were to specify &quot;15W&quot; as the value for the
+ * day-of-month field, the meaning is: &quot;the nearest weekday to the 15th of
+ * the month&quot;. So if the 15th is a Saturday, the trigger will fire on
+ * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
+ * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
+ * However if you specify &quot;1W&quot; as the value for day-of-month, and the
+ * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
+ * 'jump' over the boundary of a month's days.  The 'W' character can only be
+ * specified when the day-of-month is a single day, not a range or list of days.
+ * </p>
+ * <p>
+ * The 'L' and 'W' characters can also be combined for the day-of-month
+ * expression to yield 'LW', which translates to &quot;last weekday of the
+ * month&quot;.
+ * </p>
+ * <p>
+ * The '#' character is allowed for the day-of-week field. This character is
+ * used to specify &quot;the nth&quot; XXX day of the month. For example, the
+ * value of &quot;6#3&quot; in the day-of-week field means the third Friday of
+ * the month (day 6 = Friday and &quot;#3&quot; = the 3rd one in the month).
+ * Other examples: &quot;2#1&quot; = the first Monday of the month and
+ * &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify
+ * &quot;#5&quot; and there is not 5 of the given day-of-week in the month, then
+ * no firing will occur that month.  If the '#' character is used, there can
+ * only be one expression in the day-of-week field (&quot;3#1,6#3&quot; is
+ * not valid, since there are two expressions).
+ * </p>
+ * <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
+ * This character is short-hand for "calendar". This means values are
+ * calculated against the associated calendar, if any. If no calendar is
+ * associated, then it is equivalent to having an all-inclusive calendar. A
+ * value of "5C" in the day-of-month field means "the first day included by the
+ * calendar on or after the 5th". A value of "1C" in the day-of-week field
+ * means "the first day included by the calendar on or after Sunday".-->
+ * <p>
+ * The legal characters and the names of months and days of the week are not
+ * case sensitive.
+ *
+ * <p>
+ * <b>NOTES:</b>
+ * </p>
+ * <ul>
+ * <li>Support for specifying both a day-of-week and a day-of-month value is
+ * not complete (you'll need to use the '?' character in one of these fields).
+ * </li>
+ * <li>Overflowing ranges is supported - that is, having a larger number on
+ * the left hand side than the right. You might do 22-2 to catch 10 o'clock
+ * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
+ * very important to note that overuse of overflowing ranges creates ranges
+ * that don't make sense and no effort has been made to determine which
+ * interpretation CronExpression chooses. An example would be
+ * "0 0 14-6 ? * FRI-MON". </li>
+ * </ul>
+ *
+ *
+ * @author Sharada Jambula, James House
+ * @author Contributions from Mads Henderson
+ * @author Refactoring from CronTrigger to CronExpression by Aaron Craven
+ *
+ * Borrowed from quartz v2.5.0
+ */
+public final class CronExpression implements Serializable, Cloneable {
+
+    private static final long serialVersionUID = 12423409423L;
+
+    protected static final int SECOND = 0;
+    protected static final int MINUTE = 1;
+    protected static final int HOUR = 2;
+    protected static final int DAY_OF_MONTH = 3;
+    protected static final int MONTH = 4;
+    protected static final int DAY_OF_WEEK = 5;
+    protected static final int YEAR = 6;
+    protected static final int ALL_SPEC_INT = 99; // '*'
+    protected static final int NO_SPEC_INT = 98; // '?'
+    protected static final int MAX_LAST_DAY_OFFSET = 30;
+    protected static final int LAST_DAY_OFFSET_START = 32; // "L-30"
+    protected static final int LAST_DAY_OFFSET_END = LAST_DAY_OFFSET_START + MAX_LAST_DAY_OFFSET; // 'L'
+    protected static final Integer ALL_SPEC = ALL_SPEC_INT;
+    protected static final Integer NO_SPEC = NO_SPEC_INT;
+
+    protected static final Map<String, Integer> monthMap = new HashMap<>(20);
+    protected static final Map<String, Integer> dayMap = new HashMap<>(60);
+    static {
+        monthMap.put("JAN", 0);
+        monthMap.put("FEB", 1);
+        monthMap.put("MAR", 2);
+        monthMap.put("APR", 3);
+        monthMap.put("MAY", 4);
+        monthMap.put("JUN", 5);
+        monthMap.put("JUL", 6);
+        monthMap.put("AUG", 7);
+        monthMap.put("SEP", 8);
+        monthMap.put("OCT", 9);
+        monthMap.put("NOV", 10);
+        monthMap.put("DEC", 11);
+
+        dayMap.put("SUN", 1);
+        dayMap.put("MON", 2);
+        dayMap.put("TUE", 3);
+        dayMap.put("WED", 4);
+        dayMap.put("THU", 5);
+        dayMap.put("FRI", 6);
+        dayMap.put("SAT", 7);
+    }
+
+    private final String cronExpression;
+    private TimeZone timeZone = null;
+    protected transient TreeSet<Integer> seconds;
+    protected transient TreeSet<Integer> minutes;
+    protected transient TreeSet<Integer> hours;
+    protected transient TreeSet<Integer> daysOfMonth;
+    protected transient TreeSet<Integer> nearestWeekdays;
+    protected transient TreeSet<Integer> months;
+    protected transient TreeSet<Integer> daysOfWeek;
+    protected transient TreeSet<Integer> years;
+
+    protected transient boolean lastDayOfWeek = false;
+    protected transient int nthDayOfWeek = 0;
+    protected transient boolean expressionParsed = false;
+
+    public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;
+
+    /**
+     * Constructs a new <CODE>CronExpression</CODE> based on the specified
+     * parameter.
+     *
+     * @param cronExpression String representation of the cron expression the
+     *                       new object should represent
+     * @throws java.text.ParseException
+     *         if the string expression cannot be parsed into a valid
+     *         <CODE>CronExpression</CODE>
+     */
+    public CronExpression(String cronExpression) throws ParseException {
+        if (cronExpression == null) {
+            throw new IllegalArgumentException("cronExpression cannot be null");
+        }
+
+        this.cronExpression = cronExpression.toUpperCase(Locale.US);
+
+        buildExpression(this.cronExpression);
+    }
+
+    /**
+     * Constructs a new {@code CronExpression} as a copy of an existing
+     * instance.
+     *
+     * @param expression
+     *            The existing cron expression to be copied
+     */
+    public CronExpression(CronExpression expression) {
+        /*
+         * We don't call the other constructor here since we need to swallow the
+         * ParseException. We also elide some of the sanity checking as it is
+         * not logically trippable.
+         */
+        this.cronExpression = expression.getCronExpression();
+        try {
+            buildExpression(cronExpression);
+        } catch (ParseException ex) {
+            throw new AssertionError("Could not parse expression!", ex);
+        }
+        if (expression.getTimeZone() != null) {
+            setTimeZone((TimeZone) expression.getTimeZone().clone());
+        }
+    }
+
+    /**
+     * Indicates whether the given date satisfies the cron expression. Note that
+     * milliseconds are ignored, so two Dates falling on different milliseconds
+     * of the same second will always have the same result here.
+     *
+     * @param date the date to evaluate
+     * @return a boolean indicating whether the given date satisfies the cron
+     *         expression
+     */
+    public boolean isSatisfiedBy(Date date) {
+        Calendar testDateCal = Calendar.getInstance(getTimeZone());
+        testDateCal.setTime(date);
+        testDateCal.set(Calendar.MILLISECOND, 0);
+        Date originalDate = testDateCal.getTime();
+
+        testDateCal.add(Calendar.SECOND, -1);
+
+        Date timeAfter = getTimeAfter(testDateCal.getTime());
+
+        return ((timeAfter != null) && (timeAfter.equals(originalDate)));
+    }
+
+    /**
+     * Returns the next date/time <I>after</I> the given date/time which
+     * satisfies the cron expression.
+     *
+     * @param date the date/time at which to begin the search for the next valid
+     *             date/time
+     * @return the next valid date/time
+     */
+    public Date getNextValidTimeAfter(Date date) {
+        return getTimeAfter(date);
+    }
+
+    /**
+     * Returns the next date/time <I>after</I> the given date/time which does
+     * <I>not</I> satisfy the expression
+     *
+     * @param date the date/time at which to begin the search for the next
+     *             invalid date/time
+     * @return the next valid date/time
+     */
+    public Date getNextInvalidTimeAfter(Date date) {
+        long difference = 1000;
+
+        //move back to the nearest second so differences will be accurate
+        Calendar adjustCal = Calendar.getInstance(getTimeZone());
+        adjustCal.setTime(date);
+        adjustCal.set(Calendar.MILLISECOND, 0);
+        Date lastDate = adjustCal.getTime();
+
+        Date newDate;
+
+        //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
+
+        //keep getting the next included time until it's farther than one second
+        // apart. At that point, lastDate is the last valid fire time. We return
+        // the second immediately following it.
+        while (difference == 1000) {
+            newDate = getTimeAfter(lastDate);
+            if(newDate == null)
+                break;
+
+            difference = newDate.getTime() - lastDate.getTime();
+
+            if (difference == 1000) {
+                lastDate = newDate;
+            }
+        }
+
+        return new Date(lastDate.getTime() + 1000);
+    }
+
+    /**
+     * Returns the time zone for which this <code>CronExpression</code>
+     * will be resolved.
+     */
+    public TimeZone getTimeZone() {
+        if (timeZone == null) {
+            timeZone = TimeZone.getDefault();
+        }
+
+        return timeZone;
+    }
+
+    /**
+     * Sets the time zone for which  this <code>CronExpression</code>
+     * will be resolved.
+     */
+    public void setTimeZone(TimeZone timeZone) {
+        this.timeZone = timeZone;
+    }
+
+    /**
+     * Returns the string representation of the <CODE>CronExpression</CODE>
+     *
+     * @return a string representation of the <CODE>CronExpression</CODE>
+     */
+    @Override
+    public String toString() {
+        return cronExpression;
+    }
+
+    /**
+     * Indicates whether the specified cron expression can be parsed into a
+     * valid cron expression
+     *
+     * @param cronExpression the expression to evaluate
+     * @return a boolean indicating whether the given expression is a valid cron
+     *         expression
+     */
+    public static boolean isValidExpression(String cronExpression) {
+
+        try {
+            new CronExpression(cronExpression);
+        } catch (ParseException pe) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public static void validateExpression(String cronExpression) throws ParseException {
+
+        new CronExpression(cronExpression);
+    }
+
+
+    ////////////////////////////////////////////////////////////////////////////
+    //
+    // Expression Parsing Functions
+    //
+    ////////////////////////////////////////////////////////////////////////////
+
+    protected void buildExpression(String expression) throws ParseException {
+        expressionParsed = true;
+
+        try {
+
+            if (seconds == null) {
+                seconds = new TreeSet<>();
+            }
+            if (minutes == null) {
+                minutes = new TreeSet<>();
+            }
+            if (hours == null) {
+                hours = new TreeSet<>();
+            }
+            if (daysOfMonth == null) {
+                daysOfMonth = new TreeSet<>();
+            }
+            if (nearestWeekdays == null) {
+                nearestWeekdays = new TreeSet<>();
+            }
+            if (months == null) {
+                months = new TreeSet<>();
+            }
+            if (daysOfWeek == null) {
+                daysOfWeek = new TreeSet<>();
+            }
+            if (years == null) {
+                years = new TreeSet<>();
+            }
+
+            int exprOn = SECOND;
+
+            StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
+                    false);
+
+            if(exprsTok.countTokens() > 7) {
+                throw new ParseException("Invalid expression has too many terms: " + expression, -1);
+            }
+
+            while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
+                String expr = exprsTok.nextToken().trim();
+
+                // throw an exception if L is used with other days of the week
+                if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1  && expr.contains(",")) {
+                    throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
+                }
+                if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) {
+                    throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1);
+                }
+
+                StringTokenizer vTok = new StringTokenizer(expr, ",");
+                while (vTok.hasMoreTokens()) {
+                    String v = vTok.nextToken();
+                    storeExpressionVals(0, v, exprOn);
+                }
+
+                exprOn++;
+            }
+
+            if (exprOn <= DAY_OF_WEEK) {
+                throw new ParseException("Unexpected end of expression.",
+                        expression.length());
+            }
+
+            if (exprOn <= YEAR) {
+                storeExpressionVals(0, "*", YEAR);
+            }
+
+            TreeSet<Integer> dow = getSet(DAY_OF_WEEK);
+            TreeSet<Integer> dom = getSet(DAY_OF_MONTH);
+
+            // Copying the logic from the UnsupportedOperationException below
+            boolean dayOfMSpec = !dom.contains(NO_SPEC);
+            boolean dayOfWSpec = !dow.contains(NO_SPEC);
+
+            if (!dayOfMSpec || dayOfWSpec) {
+                if (!dayOfWSpec || dayOfMSpec) {
+                    throw new ParseException(
+                            "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0);
+                }
+            }
+        } catch (ParseException pe) {
+            throw pe;
+        } catch (Exception e) {
+            throw new ParseException("Illegal cron expression format ("
+                    + e + ")", 0);
+        }
+    }
+
+    protected int storeExpressionVals(int pos, String s, int type)
+            throws ParseException {
+
+        int incr = 0;
+        int i = skipWhiteSpace(pos, s);
+        if (i >= s.length()) {
+            return i;
+        }
+        char c = s.charAt(i);
+        if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) {
+            String sub = s.substring(i, i + 3);
+            int sval = -1;
+            int eval = -1;
+            if (type == MONTH) {
+                sval = getMonthNumber(sub) + 1;
+                if (sval <= 0) {
+                    throw new ParseException("Invalid Month value: '" + sub + "'", i);
+                }
+                if (s.length() > i + 3) {
+                    c = s.charAt(i + 3);
+                    if (c == '-') {
+                        i += 4;
+                        sub = s.substring(i, i + 3);
+                        eval = getMonthNumber(sub) + 1;
+                        if (eval <= 0) {
+                            throw new ParseException("Invalid Month value: '" + sub + "'", i);
+                        }
+                    }
+                }
+            } else if (type == DAY_OF_WEEK) {
+                sval = getDayOfWeekNumber(sub);
+                if (sval < 0) {
+                    throw new ParseException("Invalid Day-of-Week value: '"
+                            + sub + "'", i);
+                }
+                if (s.length() > i + 3) {
+                    c = s.charAt(i + 3);
+                    if (c == '-') {
+                        i += 4;
+                        sub = s.substring(i, i + 3);
+                        eval = getDayOfWeekNumber(sub);
+                        if (eval < 0) {
+                            throw new ParseException(
+                                    "Invalid Day-of-Week value: '" + sub
+                                            + "'", i);
+                        }
+                    } else if (c == '#') {
+                        try {
+                            i += 4;
+                            nthDayOfWeek = Integer.parseInt(s.substring(i));
+                            if (nthDayOfWeek < 1 || nthDayOfWeek > 5) {
+                                throw new Exception();
+                            }
+                        } catch (Exception e) {
+                            throw new ParseException(
+                                    "A numeric value between 1 and 5 must follow the '#' option",
+                                    i);
+                        }
+                    } else if (c == 'L') {
+                        lastDayOfWeek = true;
+                        i++;
+                    }
+                }
+
+            } else {
+                throw new ParseException(
+                        "Illegal characters for this position: '" + sub + "'",
+                        i);
+            }
+            if (eval != -1) {
+                incr = 1;
+            }
+            addToSet(sval, eval, incr, type);
+            return (i + 3);
+        }
+
+        if (c == '?') {
+            i++;
+            if ((i + 1) < s.length()
+                    && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
+                throw new ParseException("Illegal character after '?': "
+                        + s.charAt(i), i);
+            }
+            if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
+                throw new ParseException(
+                        "'?' can only be specified for Day-of-Month or Day-of-Week.",
+                        i);
+            }
+            if (type == DAY_OF_WEEK) {
+                if (!daysOfMonth.isEmpty() && daysOfMonth.last() == NO_SPEC_INT) {
+                    throw new ParseException(
+                            "'?' can only be specified for Day-of-Month -OR- Day-of-Week.",
+                            i);
+                }
+            }
+
+            addToSet(NO_SPEC_INT, -1, 0, type);
+            return i;
+        }
+
+        if (c == '*' || c == '/') {
+            if (c == '*' && (i + 1) >= s.length()) {
+                addToSet(ALL_SPEC_INT, -1, incr, type);
+                return i + 1;
+            } else if (c == '/'
+                    && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
+                    .charAt(i + 1) == '\t')) {
+                throw new ParseException("'/' must be followed by an integer.", i);
+            } else if (c == '*') {
+                i++;
+            }
+            c = s.charAt(i);
+            if (c == '/') { // is an increment specified?
+                i++;
+                if (i >= s.length()) {
+                    throw new ParseException("Unexpected end of string.", i);
+                }
+
+                incr = getNumericValue(s, i);
+
+                i++;
+                if (incr > 10) {
+                    i++;
+                }
+                checkIncrementRange(incr, type, i);
+            } else {
+                incr = 1;
+            }
+
+            addToSet(ALL_SPEC_INT, -1, incr, type);
+            return i;
+        } else if (c == 'L') {
+            i++;
+            if (type == DAY_OF_WEEK) {
+                addToSet(7, 7, 0, type);
+            }
+            if (type == DAY_OF_MONTH) {
+                int dom = LAST_DAY_OFFSET_END;
+                boolean nearestWeekday = false;
+                if (s.length() > i) {
+                    c = s.charAt(i);
+                    if (c == '-') {
+                        ValueSet vs = getValue(0, s, i + 1);
+                        int offset = vs.value;
+                        if (offset > MAX_LAST_DAY_OFFSET)
+                            throw new ParseException("Offset from last day must be <= " + MAX_LAST_DAY_OFFSET, i + 1);
+                        dom -= offset;
+                        i = vs.pos;
+                    }
+                    if (s.length() > i) {
+                        c = s.charAt(i);
+                        if (c == 'W') {
+                            nearestWeekday = true;
+                            i++;
+                        }
+                    }
+                }
+                if (nearestWeekday) {
+                    nearestWeekdays.add(dom);
+                } else {
+                    daysOfMonth.add(dom);
+                }
+            }
+            return i;
+        } else if (c >= '0' && c <= '9') {
+            int val = Integer.parseInt(String.valueOf(c));
+            i++;
+            if (i >= s.length()) {
+                addToSet(val, -1, -1, type);
+            } else {
+                c = s.charAt(i);
+                if (c >= '0' && c <= '9') {
+                    ValueSet vs = getValue(val, s, i);
+                    val = vs.value;
+                    i = vs.pos;
+                }
+                i = checkNext(i, s, val, type);
+                return i;
+            }
+        } else {
+            throw new ParseException("Unexpected character: " + c, i);
+        }
+
+        return i;
+    }
+
+    private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException {
+        if (incr > 59 && (type == SECOND || type == MINUTE)) {
+            throw new ParseException("Increment > 60 : " + incr, idxPos);
+        } else if (incr > 23 && (type == HOUR)) {
+            throw new ParseException("Increment > 24 : " + incr, idxPos);
+        } else if (incr > 31 && (type == DAY_OF_MONTH)) {
+            throw new ParseException("Increment > 31 : " + incr, idxPos);
+        } else if (incr > 7 && (type == DAY_OF_WEEK)) {
+            throw new ParseException("Increment > 7 : " + incr, idxPos);
+        } else if (incr > 12 && (type == MONTH)) {
+            throw new ParseException("Increment > 12 : " + incr, idxPos);
+        }
+    }
+
+    protected int checkNext(int pos, String s, int val, int type)
+            throws ParseException {
+
+        int end = -1;
+        int i = pos;
+
+        if (i >= s.length()) {
+            addToSet(val, end, -1, type);
+            return i;
+        }
+
+        char c = s.charAt(pos);
+
+        if (c == 'L') {
+            if (type == DAY_OF_WEEK) {
+                if(val < 1 || val > 7)
+                    throw new ParseException("Day-of-Week values must be between 1 and 7", -1);
+                lastDayOfWeek = true;
+            } else {
+                throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
+            }
+            TreeSet<Integer> set = getSet(type);
+            set.add(val);
+            i++;
+            return i;
+        }
+
+        if (c == 'W') {
+            if (type != DAY_OF_MONTH) {
+                throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
+            }
+            if(val > 31)
+                throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
+            nearestWeekdays.add(val);
+            i++;
+            return i;
+        }
+
+        if (c == '#') {
+            if (type != DAY_OF_WEEK) {
+                throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i);
+            }
+            i++;
+            try {
+                nthDayOfWeek = Integer.parseInt(s.substring(i));
+                if (nthDayOfWeek < 1 || nthDayOfWeek > 5) {
+                    throw new Exception();
+                }
+            } catch (Exception e) {
+                throw new ParseException(
+                        "A numeric value between 1 and 5 must follow the '#' option",
+                        i);
+            }
+
+            TreeSet<Integer> set = getSet(type);
+            set.add(val);
+            i++;
+            return i;
+        }
+
+        if (c == '-') {
+            i++;
+            c = s.charAt(i);
+            int v = Integer.parseInt(String.valueOf(c));
+            end = v;
+            i++;
+            if (i >= s.length()) {
+                addToSet(val, end, 1, type);
+                return i;
+            }
+            c = s.charAt(i);
+            if (c >= '0' && c <= '9') {
+                ValueSet vs = getValue(v, s, i);
+                end = vs.value;
+                i = vs.pos;
+            }
+            if (i < s.length() && ((c = s.charAt(i)) == '/')) {
+                i++;
+                c = s.charAt(i);
+                int v2 = Integer.parseInt(String.valueOf(c));
+                i++;
+                if (i >= s.length()) {
+                    addToSet(val, end, v2, type);
+                    return i;
+                }
+                c = s.charAt(i);
+                if (c >= '0' && c <= '9') {
+                    ValueSet vs = getValue(v2, s, i);
+                    int v3 = vs.value;
+                    addToSet(val, end, v3, type);
+                    i = vs.pos;
+                    return i;
+                } else {
+                    addToSet(val, end, v2, type);
+                    return i;
+                }
+            } else {
+                addToSet(val, end, 1, type);
+                return i;
+            }
+        }
+
+        if (c == '/') {
+            if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') {
+                throw new ParseException("'/' must be followed by an integer.", i);
+            }
+
+            i++;
+            c = s.charAt(i);
+            int v2 = Integer.parseInt(String.valueOf(c));
+            i++;
+            if (i >= s.length()) {
+                checkIncrementRange(v2, type, i);
+                addToSet(val, end, v2, type);
+                return i;
+            }
+            c = s.charAt(i);
+            if (c >= '0' && c <= '9') {
+                ValueSet vs = getValue(v2, s, i);
+                int v3 = vs.value;
+                checkIncrementRange(v3, type, i);
+                addToSet(val, end, v3, type);
+                i = vs.pos;
+                return i;
+            } else {
+                throw new ParseException("Unexpected character '" + c + "' after '/'", i);
+            }
+        }
+
+        addToSet(val, end, 0, type);
+        i++;
+        return i;
+    }
+
+    public String getCronExpression() {
+        return cronExpression;
+    }
+
+    public String getExpressionSummary() {
+        StringBuilder buf = new StringBuilder();
+
+        buf.append("seconds: ");
+        buf.append(getExpressionSetSummary(seconds));
+        buf.append("\n");
+        buf.append("minutes: ");
+        buf.append(getExpressionSetSummary(minutes));
+        buf.append("\n");
+        buf.append("hours: ");
+        buf.append(getExpressionSetSummary(hours));
+        buf.append("\n");
+        buf.append("daysOfMonth: ");
+        buf.append(getExpressionSetSummary(daysOfMonth));
+        buf.append("\n");
+        buf.append("nearestWeekdays: ");
+        buf.append(getExpressionSetSummary(nearestWeekdays));
+        buf.append("\n");
+        buf.append("months: ");
+        buf.append(getExpressionSetSummary(months));
+        buf.append("\n");
+        buf.append("daysOfWeek: ");
+        buf.append(getExpressionSetSummary(daysOfWeek));
+        buf.append("\n");
+        buf.append("lastDayOfWeek: ");
+        buf.append(lastDayOfWeek);
+        buf.append("\n");
+        buf.append("NthDayOfWeek: ");
+        buf.append(nthDayOfWeek);
+        buf.append("\n");
+        buf.append("years: ");
+        buf.append(getExpressionSetSummary(years));
+        buf.append("\n");
+
+        return buf.toString();
+    }
+
+    protected String getExpressionSetSummary(java.util.Set<Integer> set) {
+
+        if (set.contains(NO_SPEC)) {
+            return "?";
+        }
+        if (set.contains(ALL_SPEC)) {
+            return "*";
+        }
+
+        StringBuilder buf = new StringBuilder();
+
+        Iterator<Integer> itr = set.iterator();
+        boolean first = true;
+        while (itr.hasNext()) {
+            Integer iVal = itr.next();
+            String val = iVal.toString();
+            if (!first) {
+                buf.append(",");
+            }
+            buf.append(val);
+            first = false;
+        }
+
+        return buf.toString();
+    }
+
+    protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) {
+
+        if (list.contains(NO_SPEC)) {
+            return "?";
+        }
+        if (list.contains(ALL_SPEC)) {
+            return "*";
+        }
+
+        StringBuilder buf = new StringBuilder();
+
+        Iterator<Integer> itr = list.iterator();
+        boolean first = true;
+        while (itr.hasNext()) {
+            Integer iVal = itr.next();
+            String val = iVal.toString();
+            if (!first) {
+                buf.append(",");
+            }
+            buf.append(val);
+            first = false;
+        }
+
+        return buf.toString();
+    }
+
+    protected int skipWhiteSpace(int i, String s) {
+        for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) {
+        }
+
+        return i;
+    }
+
+    protected int findNextWhiteSpace(int i, String s) {
+        for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {
+        }
+
+        return i;
+    }
+
+    protected void addToSet(int val, int end, int incr, int type)
+            throws ParseException {
+
+        TreeSet<Integer> set = getSet(type);
+
+        if (type == SECOND || type == MINUTE) {
+            if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
+                throw new ParseException(
+                        "Minute and Second values must be between 0 and 59",
+                        -1);
+            }
+        } else if (type == HOUR) {
+            if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
+                throw new ParseException(
+                        "Hour values must be between 0 and 23", -1);
+            }
+        } else if (type == DAY_OF_MONTH) {
+            if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
+                    && (val != NO_SPEC_INT)) {
+                throw new ParseException(
+                        "Day of month values must be between 1 and 31", -1);
+            }
+        } else if (type == MONTH) {
+            if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
+                throw new ParseException(
+                        "Month values must be between 1 and 12", -1);
+            }
+        } else if (type == DAY_OF_WEEK) {
+            if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
+                    && (val != NO_SPEC_INT)) {
+                throw new ParseException(
+                        "Day-of-Week values must be between 1 and 7", -1);
+            }
+        }
+
+        if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
+            if (val != -1) {
+                set.add(val);
+            } else {
+                set.add(NO_SPEC);
+            }
+
+            return;
+        }
+
+        int startAt = val;
+        int stopAt = end;
+
+        if (val == ALL_SPEC_INT && incr <= 0) {
+            incr = 1;
+            set.add(ALL_SPEC); // put in a marker, but also fill values
+        }
+
+        if (type == SECOND || type == MINUTE) {
+            if (stopAt == -1) {
+                stopAt = 59;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 0;
+            }
+        } else if (type == HOUR) {
+            if (stopAt == -1) {
+                stopAt = 23;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 0;
+            }
+        } else if (type == DAY_OF_MONTH) {
+            if (stopAt == -1) {
+                stopAt = 31;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1;
+            }
+        } else if (type == MONTH) {
+            if (stopAt == -1) {
+                stopAt = 12;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1;
+            }
+        } else if (type == DAY_OF_WEEK) {
+            if (stopAt == -1) {
+                stopAt = 7;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1;
+            }
+        } else if (type == YEAR) {
+            if (stopAt == -1) {
+                stopAt = MAX_YEAR;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1970;
+            }
+        }
+
+        // if the end of the range is before the start, then we need to overflow into
+        // the next day, month etc. This is done by adding the maximum amount for that
+        // type, and using modulus max to determine the value being added.
+        int max = -1;
+        if (stopAt < startAt) {
+            switch (type) {
+                case       SECOND : max = 60; break;
+                case       MINUTE : max = 60; break;
+                case         HOUR : max = 24; break;
+                case        MONTH : max = 12; break;
+                case  DAY_OF_WEEK : max = 7;  break;
+                case DAY_OF_MONTH : max = 31; break;
+                case         YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
+                default           : throw new IllegalArgumentException("Unexpected type encountered");
+            }
+            stopAt += max;
+        }
+
+        for (int i = startAt; i <= stopAt; i += incr) {
+            if (max == -1) {
+                // ie: there's no max to overflow over
+                set.add(i);
+            } else {
+                // take the modulus to get the real value
+                int i2 = i % max;
+
+                // 1-indexed ranges should not include 0, and should include their max
+                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) {
+                    i2 = max;
+                }
+
+                set.add(i2);
+            }
+        }
+    }
+
+    TreeSet<Integer> getSet(int type) {
+        switch (type) {
+            case SECOND:
+                return seconds;
+            case MINUTE:
+                return minutes;
+            case HOUR:
+                return hours;
+            case DAY_OF_MONTH:
+                return daysOfMonth;
+            case MONTH:
+                return months;
+            case DAY_OF_WEEK:
+                return daysOfWeek;
+            case YEAR:
+                return years;
+            default:
+                return null;
+        }
+    }
+
+    protected ValueSet getValue(int v, String s, int i) {
+        char c = s.charAt(i);
+        StringBuilder s1 = new StringBuilder(String.valueOf(v));
+        while (c >= '0' && c <= '9') {
+            s1.append(c);
+            i++;
+            if (i >= s.length()) {
+                break;
+            }
+            c = s.charAt(i);
+        }
+        ValueSet val = new ValueSet();
+
+        val.pos = (i < s.length()) ? i : i + 1;
+        val.value = Integer.parseInt(s1.toString());
+        return val;
+    }
+
+    protected int getNumericValue(String s, int i) {
+        int endOfVal = findNextWhiteSpace(i, s);
+        String val = s.substring(i, endOfVal);
+        return Integer.parseInt(val);
+    }
+
+    protected int getMonthNumber(String s) {
+        Integer integer = monthMap.get(s);
+
+        if (integer == null) {
+            return -1;
+        }
+
+        return integer;
+    }
+
+    protected int getDayOfWeekNumber(String s) {
+        Integer integer = dayMap.get(s);
+
+        if (integer == null) {
+            return -1;
+        }
+
+        return integer;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //
+    // Computation Functions
+    //
+    ////////////////////////////////////////////////////////////////////////////
+
+    public Date getTimeAfter(Date afterTime) {
+
+        // Computation is based on Gregorian year only.
+        Calendar cl = new java.util.GregorianCalendar(getTimeZone());
+
+        // move ahead one second, since we're computing the time *after* the
+        // given time
+        afterTime = new Date(afterTime.getTime() + 1000);
+        // CronTrigger does not deal with milliseconds
+        cl.setTime(afterTime);
+        cl.set(Calendar.MILLISECOND, 0);
+
+        boolean gotOne = false;
+        // loop until we've computed the next time, or we've past the endTime
+        while (!gotOne) {
+
+            //if (endTime != null && cl.getTime().after(endTime)) return null;
+            if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop...
+                return null;
+            }
+
+            SortedSet<Integer> st = null;
+            int t = 0;
+
+            int sec = cl.get(Calendar.SECOND);
+            int min = cl.get(Calendar.MINUTE);
+
+            // get second.................................................
+            st = seconds.tailSet(sec);
+            if (st != null && !st.isEmpty()) {
+                sec = st.first();
+            } else {
+                sec = seconds.first();
+                min++;
+                cl.set(Calendar.MINUTE, min);
+            }
+            cl.set(Calendar.SECOND, sec);
+
+            min = cl.get(Calendar.MINUTE);
+            int hr = cl.get(Calendar.HOUR_OF_DAY);
+            t = -1;
+
+            // get minute.................................................
+            st = minutes.tailSet(min);
+            if (st != null && !st.isEmpty()) {
+                t = min;
+                min = st.first();
+            } else {
+                min = minutes.first();
+                hr++;
+            }
+            if (min != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, min);
+                setCalendarHour(cl, hr);
+                continue;
+            }
+            cl.set(Calendar.MINUTE, min);
+
+            hr = cl.get(Calendar.HOUR_OF_DAY);
+            int day = cl.get(Calendar.DAY_OF_MONTH);
+            t = -1;
+
+            // get hour...................................................
+            st = hours.tailSet(hr);
+            if (st != null && !st.isEmpty()) {
+                t = hr;
+                hr = st.first();
+            } else {
+                hr = hours.first();
+                day++;
+            }
+            if (hr != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, 0);
+                cl.set(Calendar.DAY_OF_MONTH, day);
+                setCalendarHour(cl, hr);
+                continue;
+            }
+            cl.set(Calendar.HOUR_OF_DAY, hr);
+
+            day = cl.get(Calendar.DAY_OF_MONTH);
+            int mon = cl.get(Calendar.MONTH) + 1;
+            // '+ 1' because calendar is 0-based for this field, and we are
+            // 1-based
+            t = -1;
+            int tmon = mon;
+
+            // get day...................................................
+            boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
+            boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
+            if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
+                Optional<Integer> smallestDay = findSmallestDay(day, mon, cl.get(Calendar.YEAR), daysOfMonth);
+                Optional<Integer> smallestDayForWeekday = findSmallestDay(day, mon, cl.get(Calendar.YEAR), nearestWeekdays);
+                t = day;
+                day = -1;
+                if (smallestDayForWeekday.isPresent()) {
+                    day = smallestDayForWeekday.get();
+
+                    java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
+                    tcal.set(Calendar.SECOND, 0);
+                    tcal.set(Calendar.MINUTE, 0);
+                    tcal.set(Calendar.HOUR_OF_DAY, 0);
+                    tcal.set(Calendar.DAY_OF_MONTH, day);
+                    tcal.set(Calendar.MONTH, mon - 1);
+                    tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
+
+                    int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+                    int dow = tcal.get(Calendar.DAY_OF_WEEK);
+
+                    if(dow == Calendar.SATURDAY && day == 1) {
+                        day += 2;
+                    } else if(dow == Calendar.SATURDAY) {
+                        day -= 1;
+                    } else if(dow == Calendar.SUNDAY && day == ldom) {
+                        day -= 2;
+                    } else if(dow == Calendar.SUNDAY) {
+                        day += 1;
+                    }
+
+
+                    tcal.set(Calendar.SECOND, sec);
+                    tcal.set(Calendar.MINUTE, min);
+                    tcal.set(Calendar.HOUR_OF_DAY, hr);
+                    tcal.set(Calendar.DAY_OF_MONTH, day);
+                    tcal.set(Calendar.MONTH, mon - 1);
+                    Date nTime = tcal.getTime();
+                    if(nTime.before(afterTime)) {
+                        day = -1;
+                    }
+                }
+                if (smallestDay.isPresent()) {
+                    if (day == -1 || smallestDay.get() < day) {
+                        day = smallestDay.get();
+                    }
+                } else if (day == -1) {
+                    day = 1;
+                    mon++;
+                }
+                if (day != t || mon != tmon) {
+                    cl.set(Calendar.SECOND, 0);
+                    cl.set(Calendar.MINUTE, 0);
+                    cl.set(Calendar.HOUR_OF_DAY, 0);
+                    cl.set(Calendar.DAY_OF_MONTH, day);
+                    cl.set(Calendar.MONTH, mon - 1);
+                    // '- 1' because calendar is 0-based for this field, and we
+                    // are 1-based
+                    continue;
+                }
+            } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
+                if (lastDayOfWeek) { // are we looking for the last XXX day of
+                    // the month?
+                    int dow = daysOfWeek.first(); // desired
+                    // d-o-w
+                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
+                    int daysToAdd = 0;
+                    if (cDow < dow) {
+                        daysToAdd = dow - cDow;
+                    }
+                    if (cDow > dow) {
+                        daysToAdd = dow + (7 - cDow);
+                    }
+
+                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+
+                    if (day + daysToAdd > lDay) { // did we already miss the
+                        // last one?
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, 1);
+                        cl.set(Calendar.MONTH, mon);
+                        // no '- 1' here because we are promoting the month
+                        continue;
+                    }
+
+                    // find date of last occurrence of this day in this month...
+                    while ((day + daysToAdd + 7) <= lDay) {
+                        daysToAdd += 7;
+                    }
+
+                    day += daysToAdd;
+
+                    if (daysToAdd > 0) {
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, day);
+                        cl.set(Calendar.MONTH, mon - 1);
+                        // '- 1' here because we are not promoting the month
+                        continue;
+                    }
+
+                } else if (nthDayOfWeek != 0) {
+                    // are we looking for the Nth XXX day in the month?
+                    int dow = daysOfWeek.first(); // desired
+                    // d-o-w
+                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
+                    int daysToAdd = 0;
+                    if (cDow < dow) {
+                        daysToAdd = dow - cDow;
+                    } else if (cDow > dow) {
+                        daysToAdd = dow + (7 - cDow);
+                    }
+
+                    boolean dayShifted = daysToAdd > 0;
+
+                    day += daysToAdd;
+                    int weekOfMonth = day / 7;
+                    if (day % 7 > 0) {
+                        weekOfMonth++;
+                    }
+
+                    daysToAdd = (nthDayOfWeek - weekOfMonth) * 7;
+                    day += daysToAdd;
+                    if (daysToAdd < 0
+                            || day > getLastDayOfMonth(mon, cl
+                            .get(Calendar.YEAR))) {
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, 1);
+                        cl.set(Calendar.MONTH, mon);
+                        // no '- 1' here because we are promoting the month
+                        continue;
+                    } else if (daysToAdd > 0 || dayShifted) {
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, day);
+                        cl.set(Calendar.MONTH, mon - 1);
+                        // '- 1' here because we are NOT promoting the month
+                        continue;
+                    }
+                } else {
+                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
+                    int dow = daysOfWeek.first(); // desired
+                    // d-o-w
+                    st = daysOfWeek.tailSet(cDow);
+                    if (st != null && !st.isEmpty()) {
+                        dow = st.first();
+                    }
+
+                    int daysToAdd = 0;
+                    if (cDow < dow) {
+                        daysToAdd = dow - cDow;
+                    }
+                    if (cDow > dow) {
+                        daysToAdd = dow + (7 - cDow);
+                    }
+
+                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+
+                    if (day + daysToAdd > lDay) { // will we pass the end of
+                        // the month?
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, 1);
+                        cl.set(Calendar.MONTH, mon);
+                        // no '- 1' here because we are promoting the month
+                        continue;
+                    } else if (daysToAdd > 0) { // are we switching days?
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd);
+                        cl.set(Calendar.MONTH, mon - 1);
+                        // '- 1' because calendar is 0-based for this field,
+                        // and we are 1-based
+                        continue;
+                    }
+                }
+            } else { // dayOfWSpec && !dayOfMSpec
+                throw new UnsupportedOperationException(
+                        "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
+            }
+            cl.set(Calendar.DAY_OF_MONTH, day);
+
+            mon = cl.get(Calendar.MONTH) + 1;
+            // '+ 1' because calendar is 0-based for this field, and we are
+            // 1-based
+            int year = cl.get(Calendar.YEAR);
+            t = -1;
+
+            // test for expressions that never generate a valid fire date,
+            // but keep looping...
+            if (year > MAX_YEAR) {
+                return null;
+            }
+
+            // get month...................................................
+            st = months.tailSet(mon);
+            if (st != null && !st.isEmpty()) {
+                t = mon;
+                mon = st.first();
+            } else {
+                mon = months.first();
+                year++;
+            }
+            if (mon != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, 0);
+                cl.set(Calendar.HOUR_OF_DAY, 0);
+                cl.set(Calendar.DAY_OF_MONTH, 1);
+                cl.set(Calendar.MONTH, mon - 1);
+                // '- 1' because calendar is 0-based for this field, and we are
+                // 1-based
+                cl.set(Calendar.YEAR, year);
+                continue;
+            }
+            cl.set(Calendar.MONTH, mon - 1);
+            // '- 1' because calendar is 0-based for this field, and we are
+            // 1-based
+
+            year = cl.get(Calendar.YEAR);
+            t = -1;
+
+            // get year...................................................
+            st = years.tailSet(year);
+            if (st != null && !st.isEmpty()) {
+                t = year;
+                year = st.first();
+            } else {
+                return null; // ran out of years...
+            }
+
+            if (year != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, 0);
+                cl.set(Calendar.HOUR_OF_DAY, 0);
+                cl.set(Calendar.DAY_OF_MONTH, 1);
+                cl.set(Calendar.MONTH, 0);
+                // '- 1' because calendar is 0-based for this field, and we are
+                // 1-based
+                cl.set(Calendar.YEAR, year);
+                continue;
+            }
+            cl.set(Calendar.YEAR, year);
+
+            gotOne = true;
+        } // while( !done )
+
+        return cl.getTime();
+    }
+
+    /**
+     * Advance the calendar to the particular hour paying particular attention
+     * to daylight saving problems.
+     *
+     * @param cal the calendar to operate on
+     * @param hour the hour to set
+     */
+    protected void setCalendarHour(Calendar cal, int hour) {
+        cal.set(java.util.Calendar.HOUR_OF_DAY, hour);
+        if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) {
+            cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1);
+        }
+    }
+
+    /**
+     * NOT YET IMPLEMENTED: Returns the time before the given time
+     * that the <code>CronExpression</code> matches.
+     */
+    public Date getTimeBefore(Date endTime) {
+        // FUTURE_TODO: implement QUARTZ-423
+        return null;
+    }
+
+    /**
+     * NOT YET IMPLEMENTED: Returns the final time that the
+     * <code>CronExpression</code> will match.
+     */
+    public Date getFinalFireTime() {
+        // FUTURE_TODO: implement QUARTZ-423
+        return null;
+    }
+
+    protected boolean isLeapYear(int year) {
+        return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
+    }
+
+    protected int getLastDayOfMonth(int monthNum, int year) {
+
+        switch (monthNum) {
+            case 1:
+                return 31;
+            case 2:
+                return (isLeapYear(year)) ? 29 : 28;
+            case 3:
+                return 31;
+            case 4:
+                return 30;
+            case 5:
+                return 31;
+            case 6:
+                return 30;
+            case 7:
+                return 31;
+            case 8:
+                return 31;
+            case 9:
+                return 30;
+            case 10:
+                return 31;
+            case 11:
+                return 30;
+            case 12:
+                return 31;
+            default:
+                throw new IllegalArgumentException("Illegal month number: "
+                        + monthNum);
+        }
+    }
+
+
+    private Optional<Integer> findSmallestDay(int day, int mon, int year, TreeSet<Integer> set) {
+        if (set.isEmpty()) {
+            return Optional.empty();
+        }
+
+        final int lastDay = getLastDayOfMonth(mon, year);
+        // For "L", "L-1", etc.
+        int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day)))
+                .map(d -> d - LAST_DAY_OFFSET_START + 1)
+                .orElse(Integer.MAX_VALUE);
+
+        // For "1", "2", etc.
+        SortedSet<Integer> st = set.subSet(day, LAST_DAY_OFFSET_START);
+        // make sure we don't over-run a short month, such as february
+        if (!st.isEmpty() && st.first() < smallestDay && st.first() <= lastDay) {
+            smallestDay = st.first();
+        }
+
+        return smallestDay == Integer.MAX_VALUE ? Optional.empty() : Optional.of(smallestDay);
+    }
+
+    private void readObject(java.io.ObjectInputStream stream)
+            throws java.io.IOException, ClassNotFoundException {
+
+        stream.defaultReadObject();
+        try {
+            buildExpression(cronExpression);
+        } catch (Exception ignore) {
+        } // never happens
+    }
+
+    @Override
+    @Deprecated
+    public Object clone() {
+        return new CronExpression(this);
+    }
+}
+
+class ValueSet {
+    public int value;
+
+    public int pos;
+}

+ 14 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java

@@ -0,0 +1,14 @@
+package com.xxl.job.admin.core.exception;
+
+/**
+ * @author xuxueli 2019-05-04 23:19:29
+ */
+public class XxlJobException extends RuntimeException {
+
+    public XxlJobException() {
+    }
+    public XxlJobException(String message) {
+        super(message);
+    }
+
+}

+ 33 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java

@@ -0,0 +1,33 @@
+package com.xxl.job.admin.core.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * xxl-job log for glue, used to track job code process
+ * @author xuxueli 2016-5-19 17:57:46
+ */
+@Data
+@TableName("xxl_job_logglue")
+public class XxlJobLogGlue {
+	@TableId(type = IdType.ASSIGN_ID)
+	private String id;
+	@TableField("JOB_ID")
+	private String jobId;				// 任务主键ID
+	@TableField("GLUE_TYPE")
+	private String glueType;		// GLUE类型	#com.xxl.job.core.glue.GlueTypeEnum
+	@TableField("GLUE_SOURCE")
+	private String glueSource;
+	@TableField("GLUE_REMARK")
+	private String glueRemark;
+	@TableField("ADD_TIME")
+	private Date addTime;
+	@TableField("UPDATE_TIME")
+	private Date updateTime;
+
+}

+ 34 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java

@@ -0,0 +1,34 @@
+package com.xxl.job.admin.core.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("xxl_job_log_report")
+public class XxlJobLogReport {
+
+    @TableId(type = IdType.ASSIGN_ID)
+	private String id;
+
+    @TableField(value = "TRIGGER_DAY")
+    private Date triggerDay;
+
+
+    @TableField(value = "RUNNING_COUNT")
+    private int runningCount;
+
+    @TableField(value = "SUC_COUNT")
+    private int sucCount;
+
+    @TableField(value = "FAIL_COUNT")
+    private int failCount;
+
+    @TableField(value = "UPDATE_TIME")
+    private Date updateTime;
+
+}

+ 29 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java

@@ -0,0 +1,29 @@
+package com.xxl.job.admin.core.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * Created by xuxueli on 16/9/30.
+ */
+@Data
+@TableName("xxl_job_registry")
+public class XxlJobRegistry {
+
+    @TableId(type = IdType.ASSIGN_ID)
+	private String id;
+    @TableField("REGISTRY_GROUP")
+    private String registryGroup;
+    @TableField("REGISTRY_KEY")
+    private String registryKey;
+    @TableField("REGISTRY_VALUE")
+    private String registryValue;
+    @TableField("UPDATE_TIME")
+    private Date updateTime;
+    
+}

+ 45 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java

@@ -0,0 +1,45 @@
+package com.xxl.job.admin.core.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author xuxueli 2019-05-04 16:43:12
+ */
+@Data
+@TableName("xxl_job_user")
+public class XxlJobUser {
+
+	@TableId(type = IdType.ASSIGN_ID)
+	private String id;
+	@TableField("USERNAME")
+	private String username;		// 账号
+	@TableField("PASSWORD")
+	private String password;		// 密码
+	@TableField("ROLE")
+	private int role;				// 角色:0-普通用户、1-管理员
+	@TableField("PERMISSION")
+	private String permission;	// 权限:执行器ID列表,多个逗号分割
+
+	// plugin
+	public boolean validPermission(String jobGroup){
+		if (this.role == 1) {
+			return true;
+		} else {
+			if (StringUtils.hasText(this.permission)) {
+				for (String permissionItem : this.permission.split(",")) {
+					if (jobGroup.equals(permissionItem)) {
+						return true;
+					}
+				}
+			}
+			return false;
+		}
+
+	}
+
+}

+ 32 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java

@@ -0,0 +1,32 @@
+//package com.xxl.job.admin.core.jobbean;
+//
+//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
+//import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
+//import org.quartz.JobExecutionContext;
+//import org.quartz.JobExecutionException;
+//import org.quartz.JobKey;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//import org.springframework.scheduling.quartz.QuartzJobBean;
+//
+///**
+// * http job bean
+// * “@DisallowConcurrentExecution” disable concurrent, thread size can not be only one, better given more
+// * @author xuxueli 2015-12-17 18:20:34
+// */
+////@DisallowConcurrentExecution
+//public class RemoteHttpJobBean extends QuartzJobBean {
+//	private static Logger logger = LoggerFactory.getLogger(RemoteHttpJobBean.class);
+//
+//	@Override
+//	protected void executeInternal(JobExecutionContext context)
+//			throws JobExecutionException {
+//
+//		// load jobId
+//		JobKey jobKey = context.getTrigger().getJobKey();
+//		Integer jobId = Integer.valueOf(jobKey.getName());
+//
+//
+//	}
+//
+//}

+ 413 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java

@@ -0,0 +1,413 @@
+//package com.xxl.job.admin.core.schedule;
+//
+//import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
+//import com.xxl.job.admin.core.jobbean.RemoteHttpJobBean;
+//import jnpf.scheduletask.entity.XxlJobInfo;
+//import com.xxl.job.admin.core.thread.JobFailMonitorHelper;
+//import com.xxl.job.admin.core.thread.JobRegistryMonitorHelper;
+//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
+//import com.xxl.job.admin.core.util.I18nUtil;
+//import com.xxl.job.core.biz.AdminBiz;
+//import com.xxl.job.core.biz.ExecutorBiz;
+//import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
+//import com.xxl.rpc.remoting.invoker.XxlRpcInvokerFactory;
+//import com.xxl.rpc.remoting.invoker.call.CallType;
+//import com.xxl.rpc.remoting.invoker.reference.XxlRpcReferenceBean;
+//import com.xxl.rpc.remoting.invoker.route.LoadBalance;
+//import com.xxl.rpc.remoting.net.NetEnum;
+//import com.xxl.rpc.remoting.net.impl.servlet.server.ServletServerHandler;
+//import com.xxl.rpc.remoting.provider.XxlRpcProviderFactory;
+//import com.xxl.rpc.serialize.Serializer;
+//import org.quartz.*;
+//import org.quartz.Trigger.TriggerState;
+//import org.quartz.impl.triggers.CronTriggerImpl;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//import org.springframework.util.Assert;
+//
+//import jakarta.servlet.ServletException;
+//import jakarta.servlet.http.HttpServletRequest;
+//import jakarta.servlet.http.HttpServletResponse;
+//import java.io.IOException;
+//import java.util.Date;
+//import java.util.concurrent.ConcurrentHashMap;
+//
+///**
+// * base quartz scheduler util
+// * @author xuxueli 2015-12-19 16:13:53
+// */
+//public final class XxlJobDynamicScheduler {
+//    private static final Logger logger = LoggerFactory.getLogger(XxlJobDynamicScheduler_old.class);
+//
+//    // ---------------------- param ----------------------
+//
+//    // scheduler
+//    private static Scheduler scheduler;
+//    public void setScheduler(Scheduler scheduler) {
+//		XxlJobDynamicScheduler_old.scheduler = scheduler;
+//	}
+//
+//
+//    // ---------------------- init + destroy ----------------------
+//    public void start() throws Exception {
+//        // valid
+//        Assert.notNull(scheduler, "quartz scheduler is null");
+//
+//        // init i18n
+//        initI18n();
+//
+//        // admin registry monitor run
+//        JobRegistryMonitorHelper.getInstance().start();
+//
+//        // admin monitor run
+//        JobFailMonitorHelper.getInstance().start();
+//
+//        // admin-server
+//        initRpcProvider();
+//
+//        logger.info(">>>>>>>>> init xxl-job admin success.");
+//    }
+//
+//
+//    public void destroy() throws Exception {
+//        // admin trigger pool stop
+//        JobTriggerPoolHelper.toStop();
+//
+//        // admin registry stop
+//        JobRegistryMonitorHelper.getInstance().toStop();
+//
+//        // admin monitor stop
+//        JobFailMonitorHelper.getInstance().toStop();
+//
+//        // admin-server
+//        stopRpcProvider();
+//    }
+//
+//
+//    // ---------------------- I18n ----------------------
+//
+//    private void initI18n(){
+//        for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) {
+//            item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name())));
+//        }
+//    }
+//
+//
+//    // ---------------------- admin rpc provider (no server version) ----------------------
+//    private static ServletServerHandler servletServerHandler;
+//    private void initRpcProvider(){
+//        // init
+//        XxlRpcProviderFactory xxlRpcProviderFactory = new XxlRpcProviderFactory();
+//        xxlRpcProviderFactory.initConfig(
+//                NetEnum.NETTY_HTTP,
+//                Serializer.SerializeEnum.HESSIAN.getSerializer(),
+//                null,
+//                0,
+//                XxlJobAdminConfig.getAdminConfig().getAccessToken(),
+//                null,
+//                null);
+//
+//        // add services
+//        xxlRpcProviderFactory.addService(AdminBiz.class.getName(), null, XxlJobAdminConfig.getAdminConfig().getAdminBiz());
+//
+//        // servlet handler
+//        servletServerHandler = new ServletServerHandler(xxlRpcProviderFactory);
+//    }
+//    private void stopRpcProvider() throws Exception {
+//        XxlRpcInvokerFactory.getInstance().stop();
+//    }
+//    public static void invokeAdminService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+//        servletServerHandler.handle(null, request, response);
+//    }
+//
+//
+//    // ---------------------- executor-client ----------------------
+//    private static ConcurrentHashMap<String, ExecutorBiz> executorBizRepository = new ConcurrentHashMap<String, ExecutorBiz>();
+//    public static ExecutorBiz getExecutorBiz(String address) throws Exception {
+//        // valid
+//        if (address==null || address.trim().length()==0) {
+//            return null;
+//        }
+//
+//        // load-cache
+//        address = address.trim();
+//        ExecutorBiz executorBiz = executorBizRepository.get(address);
+//        if (executorBiz != null) {
+//            return executorBiz;
+//        }
+//
+//        // set-cache
+//        executorBiz = (ExecutorBiz) new XxlRpcReferenceBean(
+//                NetEnum.NETTY_HTTP,
+//                Serializer.SerializeEnum.HESSIAN.getSerializer(),
+//                CallType.SYNC,
+//                LoadBalance.ROUND,
+//                ExecutorBiz.class,
+//                null,
+//                5000,
+//                address,
+//                XxlJobAdminConfig.getAdminConfig().getAccessToken(),
+//                null,
+//                null).getObject();
+//
+//        executorBizRepository.put(address, executorBiz);
+//        return executorBiz;
+//    }
+//
+//
+//    // ---------------------- schedule util ----------------------
+//
+//    /**
+//     * fill job info
+//     *
+//     * @param jobInfo
+//     */
+//	public static void fillJobInfo(XxlJobInfo jobInfo) {
+//
+//        String name = String.valueOf(jobInfo.getId());
+//
+//        // trigger key
+//        TriggerKey triggerKey = TriggerKey.triggerKey(name);
+//        try {
+//
+//            // trigger cron
+//			Trigger trigger = scheduler.getTrigger(triggerKey);
+//			if (trigger!=null && trigger instanceof CronTriggerImpl) {
+//				String cronExpression = ((CronTriggerImpl) trigger).getCronExpression();
+//				jobInfo.setJobCron(cronExpression);
+//			}
+//
+//            // trigger state
+//            TriggerState triggerState = scheduler.getTriggerState(triggerKey);
+//			if (triggerState!=null) {
+//				jobInfo.setJobStatus(triggerState.name());
+//			}
+//
+//            //JobKey jobKey = new JobKey(jobInfo.getJobName(), String.valueOf(jobInfo.getJobGroup()));
+//            //JobDetail jobDetail = scheduler.getJobDetail(jobKey);
+//            //String jobClass = jobDetail.getJobClass().getName();
+//
+//		} catch (SchedulerException e) {
+//			logger.error(e.getMessage(), e);
+//		}
+//	}
+//
+//
+//    /**
+//     * add trigger + job
+//     *
+//     * @param jobName
+//     * @param cronExpression
+//     * @return
+//     * @throws SchedulerException
+//     */
+//	public static boolean addJob(String jobName, String cronExpression) throws SchedulerException {
+//    	// 1、job key
+//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
+//        JobKey jobKey = new JobKey(jobName);
+//
+//        // 2、valid
+//        if (scheduler.checkExists(triggerKey)) {
+//            return true;    // PASS
+//        }
+//
+//        // 3、corn trigger
+//        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();   // withMisfireHandlingInstructionDoNothing 忽略掉调度终止过程中忽略的调度
+//        CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
+//
+//        // 4、job detail
+//		Class<? extends Job> jobClass_ = RemoteHttpJobBean.class;   // Class.forName(jobInfo.getJobClass());
+//		JobDetail jobDetail = JobBuilder.newJob(jobClass_).withIdentity(jobKey).build();
+//
+//        /*if (jobInfo.getJobData()!=null) {
+//        	JobDataMap jobDataMap = jobDetail.getJobDataMap();
+//        	jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class));
+//        	// JobExecutionContext context.getMergedJobDataMap().get("mailGuid");
+//		}*/
+//
+//        // 5、schedule job
+//        Date date = scheduler.scheduleJob(jobDetail, cronTrigger);
+//
+//        logger.info(">>>>>>>>>>> addJob success(quartz), jobDetail:{}, cronTrigger:{}, date:{}", jobDetail, cronTrigger, date);
+//        return true;
+//    }
+//
+//
+//    /**
+//     * remove trigger + job
+//     *
+//     * @param jobName
+//     * @return
+//     * @throws SchedulerException
+//     */
+//    public static boolean removeJob(String jobName) throws SchedulerException {
+//
+//        JobKey jobKey = new JobKey(jobName);
+//        scheduler.deleteJob(jobKey);
+//
+//        /*TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
+//        if (scheduler.checkExists(triggerKey)) {
+//            scheduler.unscheduleJob(triggerKey);    // trigger + job
+//        }*/
+//
+//        logger.info(">>>>>>>>>>> removeJob success(quartz), jobKey:{}", jobKey);
+//        return true;
+//    }
+//
+//
+//    /**
+//     * updateJobCron
+//     *
+//     * @param jobName
+//     * @param cronExpression
+//     * @return
+//     * @throws SchedulerException
+//     */
+//	public static boolean updateJobCron(String jobName, String cronExpression) throws SchedulerException {
+//
+//        // 1、job key
+//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
+//
+//        // 2、valid
+//        if (!scheduler.checkExists(triggerKey)) {
+//            return true;    // PASS
+//        }
+//
+//        CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
+//
+//        // 3、avoid repeat cron
+//        String oldCron = oldTrigger.getCronExpression();
+//        if (oldCron.equals(cronExpression)){
+//            return true;    // PASS
+//        }
+//
+//        // 4、new cron trigger
+//        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
+//        oldTrigger = oldTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
+//
+//        // 5、rescheduleJob
+//        scheduler.rescheduleJob(triggerKey, oldTrigger);
+//
+//        /*
+//        JobKey jobKey = new JobKey(jobName);
+//
+//        // old job detail
+//        JobDetail jobDetail = scheduler.getJobDetail(jobKey);
+//
+//        // new trigger
+//        HashSet<Trigger> triggerSet = new HashSet<Trigger>();
+//        triggerSet.add(cronTrigger);
+//        // cover trigger of job detail
+//        scheduler.scheduleJob(jobDetail, triggerSet, true);*/
+//
+//        logger.info(">>>>>>>>>>> resumeJob success, JobName:{}", jobName);
+//        return true;
+//    }
+//
+//
+//    /**
+//     * pause
+//     *
+//     * @param jobName
+//     * @return
+//     * @throws SchedulerException
+//     */
+//    /*public static boolean pauseJob(String jobName) throws SchedulerException {
+//
+//    	TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
+//
+//        boolean result = false;
+//        if (scheduler.checkExists(triggerKey)) {
+//            scheduler.pauseTrigger(triggerKey);
+//            result =  true;
+//        }
+//
+//        logger.info(">>>>>>>>>>> pauseJob {}, triggerKey:{}", (result?"success":"fail"),triggerKey);
+//        return result;
+//    }*/
+//
+//
+//    /**
+//     * resume
+//     *
+//     * @param jobName
+//     * @return
+//     * @throws SchedulerException
+//     */
+//    /*public static boolean resumeJob(String jobName) throws SchedulerException {
+//
+//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
+//
+//        boolean result = false;
+//        if (scheduler.checkExists(triggerKey)) {
+//            scheduler.resumeTrigger(triggerKey);
+//            result = true;
+//        }
+//
+//        logger.info(">>>>>>>>>>> resumeJob {}, triggerKey:{}", (result?"success":"fail"), triggerKey);
+//        return result;
+//    }*/
+//
+//
+//    /**
+//     * run
+//     *
+//     * @param jobName
+//     * @return
+//     * @throws SchedulerException
+//     */
+//    /*public static boolean triggerJob(String jobName) throws SchedulerException {
+//    	// TriggerKey : name + group
+//    	JobKey jobKey = new JobKey(jobName);
+//        TriggerKey triggerKey = TriggerKey.triggerKey(jobName);
+//
+//        boolean result = false;
+//        if (scheduler.checkExists(triggerKey)) {
+//            scheduler.triggerJob(jobKey);
+//            result = true;
+//            logger.info(">>>>>>>>>>> runJob success, jobKey:{}", jobKey);
+//        } else {
+//        	logger.info(">>>>>>>>>>> runJob fail, jobKey:{}", jobKey);
+//        }
+//        return result;
+//    }*/
+//
+//
+//    /**
+//     * finaAllJobList
+//     *
+//     * @return
+//     *//*
+//    @Deprecated
+//    public static List<Map<String, Object>> finaAllJobList(){
+//        List<Map<String, Object>> jobList = new ArrayList<Map<String,Object>>();
+//
+//        try {
+//            if (scheduler.getJobGroupNames()==null || scheduler.getJobGroupNames().size()==0) {
+//                return null;
+//            }
+//            String groupName = scheduler.getJobGroupNames().get(0);
+//            Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName));
+//            if (jobKeys!=null && jobKeys.size()>0) {
+//                for (JobKey jobKey : jobKeys) {
+//                    TriggerKey triggerKey = TriggerKey.triggerKey(jobKey.getName(), Scheduler.DEFAULT_GROUP);
+//                    Trigger trigger = scheduler.getTrigger(triggerKey);
+//                    JobDetail jobDetail = scheduler.getJobDetail(jobKey);
+//                    TriggerState triggerState = scheduler.getTriggerState(triggerKey);
+//                    Map<String, Object> jobMap = new HashMap<String, Object>();
+//                    jobMap.put("TriggerKey", triggerKey);
+//                    jobMap.put("Trigger", trigger);
+//                    jobMap.put("JobDetail", jobDetail);
+//                    jobMap.put("TriggerState", triggerState);
+//                    jobList.add(jobMap);
+//                }
+//            }
+//
+//        } catch (SchedulerException e) {
+//            logger.error(e.getMessage(), e);
+//            return null;
+//        }
+//        return jobList;
+//    }*/
+//
+//}

+ 58 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java

@@ -0,0 +1,58 @@
+//package com.xxl.job.admin.core.quartz;
+//
+//import org.quartz.SchedulerConfigException;
+//import org.quartz.spi.ThreadPool;
+//
+///**
+// * single thread pool, for async trigger
+// *
+// * @author xuxueli 2019-03-06
+// */
+//public class XxlJobThreadPool implements ThreadPool {
+//
+//    @Override
+//    public boolean runInThread(Runnable runnable) {
+//
+//        // async run
+//        runnable.run();
+//        return true;
+//
+//        //return false;
+//    }
+//
+//    @Override
+//    public int blockForAvailableThreads() {
+//        return 1;
+//    }
+//
+//    @Override
+//    public void initialize() throws SchedulerConfigException {
+//
+//    }
+//
+//    @Override
+//    public void shutdown(boolean waitForJobsToComplete) {
+//
+//    }
+//
+//    @Override
+//    public int getPoolSize() {
+//        return 1;
+//    }
+//
+//    @Override
+//    public void setInstanceId(String schedInstId) {
+//
+//    }
+//
+//    @Override
+//    public void setInstanceName(String schedName) {
+//
+//    }
+//
+//    // support
+//    public void setThreadCount(int count) {
+//        //
+//    }
+//
+//}

+ 48 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java

@@ -0,0 +1,48 @@
+package com.xxl.job.admin.core.route;
+
+import com.xxl.job.admin.core.route.strategy.*;
+import com.xxl.job.admin.core.util.I18nUtil;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public enum ExecutorRouteStrategyEnum {
+
+    FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()),
+    LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()),
+    ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()),
+    RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()),
+    CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()),
+    LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()),
+    LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()),
+    FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()),
+    BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()),
+    SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null);
+
+    ExecutorRouteStrategyEnum(String title, ExecutorRouter router) {
+        this.title = title;
+        this.router = router;
+    }
+
+    private String title;
+    private ExecutorRouter router;
+
+    public String getTitle() {
+        return title;
+    }
+    public ExecutorRouter getRouter() {
+        return router;
+    }
+
+    public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){
+        if (name != null) {
+            for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) {
+                if (item.name().equals(name)) {
+                    return item;
+                }
+            }
+        }
+        return defaultItem;
+    }
+
+}

+ 24 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java

@@ -0,0 +1,24 @@
+package com.xxl.job.admin.core.route;
+
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public abstract class ExecutorRouter {
+    protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class);
+
+    /**
+     * route address
+     *
+     * @param addressList
+     * @return  ReturnT.content=address
+     */
+    public abstract ReturnT<String> route(TriggerParam triggerParam, List<String> addressList);
+
+}

+ 48 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java

@@ -0,0 +1,48 @@
+package com.xxl.job.admin.core.route.strategy;
+
+import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
+import com.xxl.job.admin.core.route.ExecutorRouter;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.ExecutorBiz;
+import com.xxl.job.core.biz.model.IdleBeatParam;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteBusyover extends ExecutorRouter {
+
+    @Override
+    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
+        StringBuffer idleBeatResultSB = new StringBuffer();
+        for (String address : addressList) {
+            // beat
+            ReturnT<String> idleBeatResult = null;
+            try {
+                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
+                idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));
+            } catch (Exception e) {
+                logger.error(e.getMessage(), e);
+                idleBeatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
+            }
+            idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"<br><br>":"")
+                    .append(I18nUtil.getString("jobconf_idleBeat") + ":")
+                    .append("<br>address:").append(address)
+                    .append("<br>code:").append(idleBeatResult.getCode())
+                    .append("<br>msg:").append(idleBeatResult.getMsg());
+
+            // beat success
+            if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {
+                idleBeatResult.setMsg(idleBeatResultSB.toString());
+                idleBeatResult.setContent(address);
+                return idleBeatResult;
+            }
+        }
+
+        return new ReturnT<String>(ReturnT.FAIL_CODE, idleBeatResultSB.toString());
+    }
+
+}

+ 85 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java

@@ -0,0 +1,85 @@
+package com.xxl.job.admin.core.route.strategy;
+
+import com.xxl.job.admin.core.route.ExecutorRouter;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器;
+ *      a、virtual node:解决不均衡问题
+ *      b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteConsistentHash extends ExecutorRouter {
+
+    private static int VIRTUAL_NODE_NUM = 100;
+
+    /**
+     * get hash code on 2^32 ring (md5散列的方式计算hash值)
+     * @param key
+     * @return
+     */
+    private static long hash(String key) {
+
+        // md5 byte
+        MessageDigest md5;
+        try {
+            md5 = MessageDigest.getInstance("MD5");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("MD5 not supported", e);
+        }
+        md5.reset();
+        byte[] keyBytes = null;
+        try {
+            keyBytes = key.getBytes("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Unknown string :" + key, e);
+        }
+
+        md5.update(keyBytes);
+        byte[] digest = md5.digest();
+
+        // hash code, Truncate to 32-bits
+        long hashCode = ((long) (digest[3] & 0xFF) << 24)
+                | ((long) (digest[2] & 0xFF) << 16)
+                | ((long) (digest[1] & 0xFF) << 8)
+                | (digest[0] & 0xFF);
+
+        long truncateHashCode = hashCode & 0xffffffffL;
+        return truncateHashCode;
+    }
+
+    public String hashJob(String jobId, List<String> addressList) {
+
+        // ------A1------A2-------A3------
+        // -----------J1------------------
+        TreeMap<Long, String> addressRing = new TreeMap<Long, String>();
+        for (String address: addressList) {
+            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
+                long addressHash = hash("SHARD-" + address + "-NODE-" + i);
+                addressRing.put(addressHash, address);
+            }
+        }
+
+        long jobHash = hash(String.valueOf(jobId));
+        SortedMap<Long, String> lastRing = addressRing.tailMap(jobHash);
+        if (!lastRing.isEmpty()) {
+            return lastRing.get(lastRing.firstKey());
+        }
+        return addressRing.firstEntry().getValue();
+    }
+
+    @Override
+    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
+        String address = hashJob(triggerParam.getJobId(), addressList);
+        return new ReturnT<String>(address);
+    }
+
+}

+ 48 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java

@@ -0,0 +1,48 @@
+package com.xxl.job.admin.core.route.strategy;
+
+import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
+import com.xxl.job.admin.core.route.ExecutorRouter;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.core.biz.ExecutorBiz;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteFailover extends ExecutorRouter {
+
+    @Override
+    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
+
+        StringBuffer beatResultSB = new StringBuffer();
+        for (String address : addressList) {
+            // beat
+            ReturnT<String> beatResult = null;
+            try {
+                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
+                beatResult = executorBiz.beat();
+            } catch (Exception e) {
+                logger.error(e.getMessage(), e);
+                beatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
+            }
+            beatResultSB.append( (beatResultSB.length()>0)?"<br><br>":"")
+                    .append(I18nUtil.getString("jobconf_beat") + ":")
+                    .append("<br>address:").append(address)
+                    .append("<br>code:").append(beatResult.getCode())
+                    .append("<br>msg:").append(beatResult.getMsg());
+
+            // beat success
+            if (beatResult.getCode() == ReturnT.SUCCESS_CODE) {
+
+                beatResult.setMsg(beatResultSB.toString());
+                beatResult.setContent(address);
+                return beatResult;
+            }
+        }
+        return new ReturnT<String>(ReturnT.FAIL_CODE, beatResultSB.toString());
+
+    }
+}

+ 19 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java

@@ -0,0 +1,19 @@
+package com.xxl.job.admin.core.route.strategy;
+
+import com.xxl.job.admin.core.route.ExecutorRouter;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteFirst extends ExecutorRouter {
+
+    @Override
+    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList){
+        return new ReturnT<String>(addressList.get(0));
+    }
+
+}

+ 79 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java

@@ -0,0 +1,79 @@
+package com.xxl.job.admin.core.route.strategy;
+
+import com.xxl.job.admin.core.route.ExecutorRouter;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * 单个JOB对应的每个执行器,使用频率最低的优先被选举
+ *      a(*)、LFU(Least Frequently Used):最不经常使用,频率/次数
+ *      b、LRU(Least Recently Used):最近最久未使用,时间
+ *
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteLFU extends ExecutorRouter {
+
+    private static ConcurrentMap<String, HashMap<String, Integer>> jobLfuMap = new ConcurrentHashMap<>();
+    private static long CACHE_VALID_TIME = 0;
+
+    public String route(String jobId, List<String> addressList) {
+
+        // cache clear
+        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
+            jobLfuMap.clear();
+            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
+        }
+
+        // lfu item init
+        HashMap<String, Integer> lfuItemMap = jobLfuMap.get(jobId);     // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList;
+        if (lfuItemMap == null) {
+            lfuItemMap = new HashMap<String, Integer>();
+            jobLfuMap.putIfAbsent(jobId, lfuItemMap);   // 避免重复覆盖
+        }
+
+        // put new
+        for (String address: addressList) {
+            if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) {
+                lfuItemMap.put(address, new Random().nextInt(addressList.size()));  // 初始化时主动Random一次,缓解首次压力
+            }
+        }
+        // remove old
+        List<String> delKeys = new ArrayList<>();
+        for (String existKey: lfuItemMap.keySet()) {
+            if (!addressList.contains(existKey)) {
+                delKeys.add(existKey);
+            }
+        }
+        if (delKeys.size() > 0) {
+            for (String delKey: delKeys) {
+                lfuItemMap.remove(delKey);
+            }
+        }
+
+        // load least userd count address
+        List<Map.Entry<String, Integer>> lfuItemList = new ArrayList<Map.Entry<String, Integer>>(lfuItemMap.entrySet());
+        Collections.sort(lfuItemList, new Comparator<Map.Entry<String, Integer>>() {
+            @Override
+            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
+                return o1.getValue().compareTo(o2.getValue());
+            }
+        });
+
+        Map.Entry<String, Integer> addressItem = lfuItemList.get(0);
+        String minAddress = addressItem.getKey();
+        addressItem.setValue(addressItem.getValue() + 1);
+
+        return addressItem.getKey();
+    }
+
+    @Override
+    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
+        String address = route(triggerParam.getJobId(), addressList);
+        return new ReturnT<String>(address);
+    }
+
+}

+ 76 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java

@@ -0,0 +1,76 @@
+package com.xxl.job.admin.core.route.strategy;
+
+import com.xxl.job.admin.core.route.ExecutorRouter;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * 单个JOB对应的每个执行器,最久为使用的优先被选举
+ *      a、LFU(Least Frequently Used):最不经常使用,频率/次数
+ *      b(*)、LRU(Least Recently Used):最近最久未使用,时间
+ *
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteLRU extends ExecutorRouter {
+
+    private static ConcurrentMap<String, LinkedHashMap<String, String>> jobLRUMap = new ConcurrentHashMap<>();
+    private static long CACHE_VALID_TIME = 0;
+
+    public String route(String jobId, List<String> addressList) {
+
+        // cache clear
+        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
+            jobLRUMap.clear();
+            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
+        }
+
+        // init lru
+        LinkedHashMap<String, String> lruItem = jobLRUMap.get(jobId);
+        if (lruItem == null) {
+            /**
+             * LinkedHashMap
+             *      a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;
+             *      b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法;
+             */
+            lruItem = new LinkedHashMap<String, String>(16, 0.75f, true);
+            jobLRUMap.putIfAbsent(jobId, lruItem);
+        }
+
+        // put new
+        for (String address: addressList) {
+            if (!lruItem.containsKey(address)) {
+                lruItem.put(address, address);
+            }
+        }
+        // remove old
+        List<String> delKeys = new ArrayList<>();
+        for (String existKey: lruItem.keySet()) {
+            if (!addressList.contains(existKey)) {
+                delKeys.add(existKey);
+            }
+        }
+        if (delKeys.size() > 0) {
+            for (String delKey: delKeys) {
+                lruItem.remove(delKey);
+            }
+        }
+
+        // load
+        String eldestKey = lruItem.entrySet().iterator().next().getKey();
+        String eldestValue = lruItem.get(eldestKey);
+        return eldestValue;
+    }
+
+    @Override
+    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
+        String address = route(triggerParam.getJobId(), addressList);
+        return new ReturnT<String>(address);
+    }
+
+}

+ 19 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java

@@ -0,0 +1,19 @@
+package com.xxl.job.admin.core.route.strategy;
+
+import com.xxl.job.admin.core.route.ExecutorRouter;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.biz.model.TriggerParam;
+
+import java.util.List;
+
+/**
+ * Created by xuxueli on 17/3/10.
+ */
+public class ExecutorRouteLast extends ExecutorRouter {
+
+    @Override
+    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
+        return new ReturnT<String>(addressList.get(addressList.size()-1));
+    }
+
+}

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