java: Task Scheduler

一、引言

在业务系统中,定时任务与工作流编排是常见的核心需求。本文以一个基于 JDK 21、Swing、DDD 与 MVC 架构的 Java 任务调度系统为例,从领域模型设计、仓储接口抽象到界面交互与调度集成,完整梳理一套可落地的实现思路。项目支持 Quartz 定时调度、Redis 集群节点发现、DAG 工作流编排以及任务执行历史统计,适合作为中大型桌面端调度系统的参考实现。

二、领域模型设计

领域模型是整个系统的地基,它决定了业务规则如何表达、如何被持久化以及如何被上层调度与界面复用。本节围绕任务实体、DAG 工作流与领域事件三个核心部分展开。

2.1 任务实体与状态管理

任务实体(Task)是系统的聚合根,承载名称、Cron 表达式、命令、超时时间、重试策略等关键属性。状态使用枚举 TaskStatus 管理,当前仅区分空闲与运行中两种状态,便于后续扩展。任务还支持一次性语义:当备注中包含「一次性」时,执行成功后会自动禁用,适合初始化数据等场景。

2.2 DAG 工作流与节点编排

DAG 工作流用于表达任务之间的依赖关系。DagDefinition 保存整个工作流的节点列表,DagNode 则描述单个可执行单元,支持绑定已有任务或内联命令,并通过 dependsOn 声明上游依赖。节点还提供失败策略(阻断或跳过)与重试次数,满足并行分支与容错需求。

2.3 领域事件与解耦

领域事件基类 DomainEvent 定义了任务开始、完成、失败以及 DAG 执行完成等事件类型。通过 EventBus 传递事件,可以将任务执行、日志记录与界面刷新解耦,提升系统的可维护性与扩展性。

三、仓储抽象与基础设施

仓储接口遵循 DDD 的依赖倒置原则,将领域模型与具体数据库实现分离。本节介绍任务、历史与 DAG 三类仓储接口的设计,以及它们在基础设施层的落地方式。

3.1 任务仓储接口

TaskRepository 提供任务的全量查询、分页搜索、保存更新、状态切换与启停控制等能力。接口设计面向领域服务与界面控制层,屏蔽底层 JDBC 细节,便于替换实现或引入缓存。

3.2 执行历史仓储接口

TaskHistoryRepository 负责记录每次任务的执行结果,支持按任务查询、最近记录、统计汇总以及按天分布等能力。这些数据是仪表盘与执行日志页面的数据来源,也是评估调度稳定性的重要依据。

3.3 DAG 仓储接口

DagRepository 提供工作流的增删改查能力,支撑 DAG 编排界面的数据读写。与任务仓储类似,接口保持精简,具体持久化逻辑由基础设施层实现。

四、实战示例:从启动到界面交互

本节通过主程序入口与 Swing 界面两个示例,展示系统如何从配置加载、数据库初始化、调度器启动,最终呈现为可操作的桌面应用。

4.1 主程序启动流程

Application 主类按照固定顺序完成系统引导:加载配置与日志、初始化 H2 数据库、启动 Redis 支持、启动 Quartz 调度器、写入种子数据,最后在 EDT 线程中创建主窗口。首次运行空库时,系统会自动插入健康检查、数据库备份与一次性初始化三个示例任务。

4.2 Swing 主窗口与工具栏

MainWindow 采用 BorderLayout 组织界面,顶部为操作工具栏,中部为任务列表、仪表盘、DAG 工作流与执行日志四个标签页,底部为状态栏。工具栏提供新增、编辑、删除、立即执行、暂停、恢复、启停、历史、DAG 编排以及 JSON 导入导出等操作,覆盖日常管理所需的主要功能。

4.3 任务编辑对话框与 Cron 校验

TaskEditDialog 是新增与编辑任务的统一入口,表单包含名称、Cron 表达式、命令、超时、重试等字段。保存前会进行必填校验,并提供 Cron 表达式校验与下次执行时间预览,帮助用户快速确认调度配置是否正确。

五、总结

本文从领域模型、仓储抽象、调度集成到 Swing 界面,完整梳理了一个 Java 任务调度系统的核心实现。整体采用 DDD 分层与 MVC 交互模式,兼顾业务表达、扩展性与界面可操作性。后续可在此基础上继续完善分布式调度协调、任务告警通知以及更丰富的 DAG 执行策略,让系统在真实生产环境中更加健壮。

项目结构:

java 复制代码
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : DomainEvent.java
 * explain   : 学习  类
 **/
package com.jewelry.taskscheduler.domain.event;
import java.time.LocalDateTime;
/**
领域事件基类(EventBus 传递对象)
子类:TaskStartedEvent / TaskCompletedEvent / TaskFailedEvent / DagExecutedEvent
*/
public abstract class DomainEvent {
private final long taskId;
private final String taskName;
private final LocalDateTime occurredAt = LocalDateTime.now();
protected DomainEvent(long taskId, String taskName) {
this.taskId = taskId;
this.taskName = taskName;
}
public long getTaskId() { return taskId; }
public String getTaskName() { return taskName; }
public LocalDateTime getOccurredAt() { return occurredAt; }
/** 任务开始事件 */
public static class TaskStartedEvent extends DomainEvent {
public TaskStartedEvent(long taskId, String taskName) {
super(taskId, taskName);
}
}
/** 任务完成事件(成功) */
public static class TaskCompletedEvent extends DomainEvent {
private final String message;
public TaskCompletedEvent(long taskId, String taskName, String message) {
super(taskId, taskName);
this.message = message;
}
public String getMessage() { return message; }
}
/** 任务失败事件 */
public static class TaskFailedEvent extends DomainEvent {
private final String message;
public TaskFailedEvent(long taskId, String taskName, String message) {
super(taskId, taskName);
this.message = message;
}
public String getMessage() { return message; }
}
/** DAG 执行完成事件 */
public static class DagExecutedEvent extends DomainEvent {
private final String dagName;
public DagExecutedEvent(long dagId, String dagName, boolean success) {
super(dagId, dagName);
this.dagName = dagName;
this.success = success;
}
private final boolean success;
public boolean isSuccess() { return success; }
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : Application.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
DAG 工作流定义:任务依赖编排(A 完成后执行 B,支持并行分支)
nodesJson 保存节点列表(DagNode 的 JSON)
*/
public class DagDefinition {
private long id;
private String name = "";
private String cronExpr = "";   // 可选:定时触发整个 DAG
private boolean enabled = true;
private List<DagNode> nodes = new ArrayList<>();
private LocalDateTime createdAt;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCronExpr() { return cronExpr; }
public void setCronExpr(String cronExpr) { this.cronExpr = cronExpr; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public List<DagNode> getNodes() { return nodes; }
public void setNodes(List<DagNode> nodes) { this.nodes = nodes; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : Application.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.model;
import java.util.ArrayList;
import java.util.List;
/**
DAG 节点:一个可执行单元(命令 或 引用现有任务 key)
字段与 .NET 版 DAG 节点语义对齐:
key       节点唯一标识(DAG 内)
name      显示名
taskKey   可空:绑定现有任务(task-{id})执行其命令
command   可空:内联命令
dependsOn 上游节点 key 列表
onFail    STOP=失败阻断下游 / SKIP=失败跳过下游
retry     失败重试次数
*/
public class DagNode {
private String key = "";
private String name = "";
private String taskKey = "";
private String command = "";
private List<String> dependsOn = new ArrayList<>();
private String onFail = "STOP";
private int retry = 0;
private int timeoutSec = 60;
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getTaskKey() { return taskKey; }
public void setTaskKey(String taskKey) { this.taskKey = taskKey; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public List&lt;String&gt; getDependsOn() { return dependsOn; }
public void setDependsOn(List&lt;String&gt; dependsOn) { this.dependsOn = dependsOn; }
public String getOnFail() { return onFail; }
public void setOnFail(String onFail) { this.onFail = onFail; }
public int getRetry() { return retry; }
public void setRetry(int retry) { this.retry = retry; }
public int getTimeoutSec() { return timeoutSec; }
public void setTimeoutSec(int timeoutSec) { this.timeoutSec = timeoutSec; }
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : Task.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.model;
/**
任务实体(DDD 聚合根)
cron 为标准 5 字段:分 时 日 月 周(如 0 8 * * *)
*/
public class Task {
private long id;
private String name = "";
private String cronExpr = "";
private String command = "";
private int timeoutSec = 30;
private int maxRetry = 0;
private int retryGapSec = 5;
private boolean enabled = true;
private String remark = "";
private TaskStatus status = TaskStatus.IDLE;
/** 一次性任务:备注含"一次性",执行成功后自动禁用 */
public boolean isOneTime() {
return remark != null && remark.contains("一次性");
}
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCronExpr() { return cronExpr; }
public void setCronExpr(String cronExpr) { this.cronExpr = cronExpr; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public int getTimeoutSec() { return timeoutSec; }
public void setTimeoutSec(int timeoutSec) { this.timeoutSec = timeoutSec; }
public int getMaxRetry() { return maxRetry; }
public void setMaxRetry(int maxRetry) { this.maxRetry = maxRetry; }
public int getRetryGapSec() { return retryGapSec; }
public void setRetryGapSec(int retryGapSec) { this.retryGapSec = retryGapSec; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public TaskStatus getStatus() { return status; }
public void setStatus(TaskStatus status) { this.status = status; }
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : TaskHistory.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.model;
import java.time.LocalDateTime;
/** 任务执行历史实体 */
public class TaskHistory {
private long id;
private long taskId;
private LocalDateTime startTime;
private LocalDateTime endTime;
private boolean success;
private String message = "";
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public long getTaskId() { return taskId; }
public void setTaskId(long taskId) { this.taskId = taskId; }
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }

public long costMillis() {
    if (startTime == null || endTime == null) return 0;
    return java.time.Duration.between(startTime, endTime).toMillis();
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : TaskStatus.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.model;
/** 任务状态枚举 /
public enum TaskStatus {
/* 空闲 /
IDLE,
/* 运行中 */
RUNNING;
public static TaskStatus of(String s) {
    if (s == null) return IDLE;
    try {
        return valueOf(s.trim().toUpperCase());
    } catch (IllegalArgumentException e) {
        return IDLE;
    }
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : DagRepository.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.repository;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import java.util.List;
/** DAG 工作流仓储接口 */
public interface DagRepository {
List&lt;DagDefinition&gt; findAll();

DagDefinition findById(long id);

DagDefinition save(DagDefinition dag);

boolean deleteById(long id);
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : TaskHistoryRepository.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.repository;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import java.util.List;
/** 任务历史仓储接口 */
public interface TaskHistoryRepository {
TaskHistory insert(TaskHistory history);

List&lt;TaskHistory&gt; listByTaskId(long taskId, int limit);

List&lt;TaskHistory&gt; listRecent(int limit);

/** 返回 [total, success, fail] */
long[] countStats();

long countToday();

/** 近 days 天每天次数(无记录补 0),返回 [日期yyyy-MM-dd, 次数] */
List&lt;String[]&gt; countByDay(int days);
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : TaskRepository.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.domain.repository;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import java.util.List;
/** 任务仓储接口(DDD 依赖倒置:基础设施层 JDBC 实现) */
public interface TaskRepository {
List&lt;Task&gt; findAll();

List&lt;Task&gt; findEnabled();

long count();

/** 关键字(名称/备注)分页,page 从 1 开始 */
List&lt;Task&gt; searchPage(String keyword, int page, int pageSize);

Task findById(long id);

/** 保存:id&lt;=0 新增,否则更新;返回带 id 的实体 */
Task save(Task task);

boolean deleteById(long id);

boolean updateStatus(long id, TaskStatus status);

boolean updateEnabled(long id, boolean enabled);
}
java 复制代码
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : Db.java
 * explain   : 学习  类
 **/
 
package com.jewelry.taskscheduler.infrastructure.db;
 
import com.jewelry.taskscheduler.config.AppConfig;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
 
/**
 * H2 嵌入式数据库(DDD 基础设施层)
 * 启动时 EnsureCreated 语义:表不存在则建表(与 .NET EF EnsureCreated 对齐,兼容既有库文件)
 * 时间统一存 VARCHAR "yyyy-MM-dd HH:mm:ss"(与 .NET 版 Db.FormatTime 一致)
 */
public final class Db {
 
    private Db() {
    }
 
    /** 初始化:建表(已存在自动跳过) */
    public static void init() {
        try (Connection c = open(); Statement st = c.createStatement()) {
            st.executeUpdate("""
                CREATE TABLE IF NOT EXISTS t_task (
                    id BIGINT AUTO_INCREMENT PRIMARY KEY,
                    task_name VARCHAR(200) NOT NULL,
                    cron_expr VARCHAR(100) NOT NULL,
                    command VARCHAR(1000) NOT NULL,
                    timeout_sec INT DEFAULT 30,
                    max_retry INT DEFAULT 0,
                    retry_gap_sec INT DEFAULT 5,
                    is_enable BOOLEAN DEFAULT TRUE,
                    remark VARCHAR(500) DEFAULT '',
                    task_status VARCHAR(20) DEFAULT 'IDLE'
                )
                """);
            st.executeUpdate("""
                CREATE TABLE IF NOT EXISTS t_task_history (
                    id BIGINT AUTO_INCREMENT PRIMARY KEY,
                    task_id BIGINT NOT NULL,
                    execute_start_time VARCHAR(30) NOT NULL,
                    execute_end_time VARCHAR(30) NOT NULL,
                    is_success BOOLEAN NOT NULL,
                    execute_msg VARCHAR(4000) DEFAULT ''
                )
                """);
            st.executeUpdate("""
                CREATE TABLE IF NOT EXISTS t_dag (
                    id BIGINT AUTO_INCREMENT PRIMARY KEY,
                    dag_name VARCHAR(200) NOT NULL,
                    cron_expr VARCHAR(100) DEFAULT '',
                    is_enable BOOLEAN DEFAULT TRUE,
                    nodes_json CLOB,
                    created_at VARCHAR(30) DEFAULT ''
                )
                """);
            AppConfig.AppLog.info("H2 数据库初始化完成: " + AppConfig.INSTANCE.dbUrl);
        } catch (Exception e) {
            AppConfig.AppLog.error("H2 数据库初始化失败: " + e.getMessage());
            throw new RuntimeException("数据库初始化失败", e);
        }
    }
 
    /** 打开新连接(每次操作独立连接,与 .NET 短生命周期上下文一致) */
    public static Connection open() throws java.sql.SQLException {
        AppConfig c = AppConfig.INSTANCE;
        return DriverManager.getConnection(c.dbUrl, c.dbUser, c.dbPassword);
    }
 
    /** 时间 → 文本(yyyy-MM-dd HH:mm:ss) */
    public static String formatTime(java.time.LocalDateTime t) {
        return t.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
 
    /** 文本 → 时间 */
    public static java.time.LocalDateTime parseTime(String s) {
        if (s == null || s.isBlank()) {
            return java.time.LocalDateTime.now();
        }
        try {
            return java.time.LocalDateTime.parse(s.trim(),
                    java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        } catch (Exception e) {
            return java.time.LocalDateTime.now();
        }
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : EventBusAdapter.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.event;
 
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
 
/**
 * 领域事件总线(Guava EventBus 封装,单例)
 * 发布任务开始/完成/失败/DAG 事件;UI 与日志系统订阅
 */
public final class EventBusAdapter {
 
    private static final EventBusAdapter INSTANCE = new EventBusAdapter();
 
    private final ExecutorService executor = Executors.newFixedThreadPool(2, r -> {
        Thread t = new Thread(r, "eventbus");
        t.setDaemon(true);
        return t;
    });
    private final EventBus syncBus = new EventBus("domain");
    private final AsyncEventBus asyncBus = new AsyncEventBus("domain-async", executor);
 
    private EventBusAdapter() {
    }
 
    public static EventBusAdapter instance() {
        return INSTANCE;
    }
 
    /** 发布领域事件(UI 用同步,后台任务用异步) */
    public void post(DomainEvent event) {
        try {
            syncBus.post(event);
        } catch (Exception e) {
            AppConfig.AppLog.warn("事件发布失败: " + e.getMessage());
        }
    }
 
    public void postAsync(DomainEvent event) {
        asyncBus.post(event);
    }
 
    /** 注册订阅者(对象中 @Subscribe 方法) */
    public void register(Object listener) {
        syncBus.register(listener);
        asyncBus.register(listener);
    }
 
    /**
     * 便捷订阅:某类型事件的消费者
     * 注意:Guava 反射会擦除泛型参数类型,@Subscribe 方法必须用具体类型(DomainEvent 基类),
     * 内部按 type instanceof 分发,避免 ClassCastException
     */
    public <T extends DomainEvent> void on(Class<T> type, Consumer<T> consumer) {
        register(new Object() {
            @Subscribe
            public void handle(DomainEvent event) {
                if (type.isInstance(event)) {
                    consumer.accept(type.cast(event));
                }
            }
        });
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : RedisSupport.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.redis;
 
import com.jewelry.taskscheduler.config.AppConfig;
import redis.clients.jedis.Jedis;
 
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
/**
 * Redis 支持(可选,配置 redis.enabled=false 时全部跳过,纯本地模式)
 * 能力:
 *   1. 分布式锁(SET NX EX)------多实例任务防重复执行
 *   2. 节点心跳上报(HSET + 过期)------集群节点列表展示
 */
public class RedisSupport {
 
    private static final RedisSupport INSTANCE = new RedisSupport();
 
    private volatile Jedis jedis;
    private final String nodeId = UUID.randomUUID().toString().substring(0, 8);
    private final ScheduledExecutorService heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
        Thread t = new Thread(r, "redis-heartbeat");
        t.setDaemon(true);
        return t;
    });
 
    public static RedisSupport instance() {
        return INSTANCE;
    }
 
    public boolean enabled() {
        return AppConfig.INSTANCE.redisEnabled;
    }
 
    /** 启动:连接 + 启动心跳上报 */
    public void start() {
        if (!enabled()) {
            AppConfig.AppLog.info("Redis 未启用,运行于本地模式(分布式锁/心跳已跳过)");
            return;
        }
        try {
            jedis = new Jedis(AppConfig.INSTANCE.redisHost, AppConfig.INSTANCE.redisPort);
            if (!AppConfig.INSTANCE.redisPassword.isBlank()) {
                jedis.auth(AppConfig.INSTANCE.redisPassword);
            }
            jedis.ping();
            AppConfig.AppLog.info("Redis 已连接 " + AppConfig.INSTANCE.redisHost + ":" + AppConfig.INSTANCE.redisPort
                    + ",节点ID=" + nodeId);
            // 心跳:每 5 秒上报一次
            heartbeatExecutor.scheduleAtFixedRate(this::heartbeat, 1, 5, TimeUnit.SECONDS);
        } catch (Exception e) {
            AppConfig.AppLog.error("Redis 连接失败: " + e.getMessage() + "(本次运行跳过 Redis 能力)");
            jedis = null;
        }
    }
 
    public void shutdown() {
        heartbeatExecutor.shutdownNow();
        if (jedis != null) {
            try {
                jedis.close();
            } catch (Exception ignored) {
            }
        }
    }
 
    /** 节点心跳上报:HSET 节点 → 时间戳 + 过期 */
    private void heartbeat() {
        try {
            String key = AppConfig.INSTANCE.redisHeartbeatKey;
            jedis.hset(key, nodeId, String.valueOf(System.currentTimeMillis()));
            jedis.expire(key, AppConfig.INSTANCE.redisHeartbeatTimeoutSec + 15);
        } catch (Exception e) {
            AppConfig.AppLog.warn("心跳上报失败: " + e.getMessage());
        }
    }
 
    /** 获取在线节点列表(nodeId → 最后心跳时间) */
    public Map<String, String> listNodes() {
        if (jedis == null || !enabled()) {
            return Map.of();
        }
        try {
            String key = AppConfig.INSTANCE.redisHeartbeatKey;
            long timeout = AppConfig.INSTANCE.redisHeartbeatTimeoutSec * 1000L;
            long now = System.currentTimeMillis();
            Map<String, String> all = jedis.hgetAll(key);
            // 过滤超时节点
            all.entrySet().removeIf(e -> {
                try {
                    return now - Long.parseLong(e.getValue()) > timeout;
                } catch (Exception ex) {
                    return true;
                }
            });
            return all;
        } catch (Exception e) {
            return Map.of();
        }
    }
 
    /** 分布式锁:SET NX EX,成功返回 true */
    public boolean tryLock(String key, int ttlSec) {
        if (jedis == null || !enabled()) {
            return true; // 本地模式视为已获锁(Quartz 已防同实例并发)
        }
        try {
            String r = jedis.set(key, nodeId, new redis.clients.jedis.params.SetParams().nx().ex(ttlSec));
            return "OK".equals(r);
        } catch (Exception e) {
            AppConfig.AppLog.warn("获取分布式锁失败 " + key + ": " + e.getMessage());
            return true;
        }
    }
 
    public void unlock(String key) {
        if (jedis == null || !enabled()) {
            return;
        }
        try {
            jedis.del(key);
        } catch (Exception ignored) {
        }
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : DagRepositoryJdbc.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.repository;
 
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import com.jewelry.taskscheduler.domain.model.DagNode;
import com.jewelry.taskscheduler.domain.repository.DagRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
 
/** DAG 工作流仓储 H2/JDBC 实现(nodes_json 存节点 JSON) */
public class DagRepositoryJdbc implements DagRepository {
 
    private static final ObjectMapper JSON = new ObjectMapper();
 
    @Override
    public List<DagDefinition> findAll() {
        List<DagDefinition> list = new ArrayList<>();
        try (Connection c = Db.open();
             Statement st = c.createStatement();
             ResultSet rs = st.executeQuery("SELECT * FROM t_dag ORDER BY id")) {
            while (rs.next()) {
                list.add(map(rs));
            }
        } catch (Exception e) {
            throw new RuntimeException("查询 DAG 失败", e);
        }
        return list;
    }
 
    @Override
    public DagDefinition findById(long id) {
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("SELECT * FROM t_dag WHERE id = ?")) {
            ps.setLong(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? map(rs) : null;
            }
        } catch (Exception e) {
            throw new RuntimeException("查询 DAG 失败 id=" + id, e);
        }
    }
 
    @Override
    public DagDefinition save(DagDefinition dag) {
        if (dag.getId() <= 0) {
            try (Connection c = Db.open();
                 PreparedStatement ps = c.prepareStatement("""
                     INSERT INTO t_dag(dag_name, cron_expr, is_enable, nodes_json, created_at)
                     VALUES (?,?,?,?,?)
                     """, Statement.RETURN_GENERATED_KEYS)) {
                ps.setString(1, dag.getName());
                ps.setString(2, dag.getCronExpr());
                ps.setBoolean(3, dag.isEnabled());
                ps.setString(4, nodesJson(dag));
                ps.setString(5, Db.formatTime(dag.getCreatedAt() != null ? dag.getCreatedAt() : java.time.LocalDateTime.now()));
                ps.executeUpdate();
                try (ResultSet keys = ps.getGeneratedKeys()) {
                    if (keys.next()) {
                        dag.setId(keys.getLong(1));
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException("新增 DAG 失败", e);
            }
        } else {
            try (Connection c = Db.open();
                 PreparedStatement ps = c.prepareStatement(
                         "UPDATE t_dag SET dag_name=?, cron_expr=?, is_enable=?, nodes_json=? WHERE id=?")) {
                ps.setString(1, dag.getName());
                ps.setString(2, dag.getCronExpr());
                ps.setBoolean(3, dag.isEnabled());
                ps.setString(4, nodesJson(dag));
                ps.setLong(5, dag.getId());
                ps.executeUpdate();
            } catch (Exception e) {
                throw new RuntimeException("更新 DAG 失败 id=" + dag.getId(), e);
            }
        }
        return dag;
    }
 
    @Override
    public boolean deleteById(long id) {
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("DELETE FROM t_dag WHERE id = ?")) {
            ps.setLong(1, id);
            return ps.executeUpdate() > 0;
        } catch (Exception e) {
            throw new RuntimeException("删除 DAG 失败 id=" + id, e);
        }
    }
 
    private String nodesJson(DagDefinition dag) {
        try {
            return JSON.writeValueAsString(dag.getNodes());
        } catch (Exception e) {
            throw new RuntimeException("DAG 节点序列化失败", e);
        }
    }
 
    private DagDefinition map(ResultSet rs) throws java.sql.SQLException {
        DagDefinition d = new DagDefinition();
        d.setId(rs.getLong("id"));
        d.setName(rs.getString("dag_name"));
        d.setCronExpr(rs.getString("cron_expr"));
        d.setEnabled(rs.getBoolean("is_enable"));
        d.setCreatedAt(Db.parseTime(rs.getString("created_at")));
        String json = rs.getString("nodes_json");
        if (json != null && !json.isBlank()) {
            try {
                d.setNodes(JSON.readValue(json, new TypeReference<List<DagNode>>() {
                }));
            } catch (Exception e) {
                throw new RuntimeException("DAG 节点反序列化失败 id=" + d.getId(), e);
            }
        }
        return d;
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : TaskHistoryRepositoryJdbc.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.repository;
 
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.repository.TaskHistoryRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
/** 任务历史仓储 H2/JDBC 实现 */
public class TaskHistoryRepositoryJdbc implements TaskHistoryRepository {
 
    @Override
    public TaskHistory insert(TaskHistory h) {
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("""
                 INSERT INTO t_task_history(task_id, execute_start_time, execute_end_time, is_success, execute_msg)
                 VALUES (?,?,?,?,?)
                 """, Statement.RETURN_GENERATED_KEYS)) {
            ps.setLong(1, h.getTaskId());
            ps.setString(2, Db.formatTime(h.getStartTime()));
            ps.setString(3, Db.formatTime(h.getEndTime()));
            ps.setBoolean(4, h.isSuccess());
            ps.setString(5, truncate(h.getMessage(), 4000));
            ps.executeUpdate();
            try (ResultSet keys = ps.getGeneratedKeys()) {
                if (keys.next()) {
                    h.setId(keys.getLong(1));
                }
            }
            return h;
        } catch (Exception e) {
            throw new RuntimeException("写入执行历史失败", e);
        }
    }
 
    @Override
    public List<TaskHistory> listByTaskId(long taskId, int limit) {
        List<TaskHistory> list = new ArrayList<>();
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement(
                     "SELECT * FROM t_task_history WHERE task_id=? ORDER BY id DESC LIMIT ?")) {
            ps.setLong(1, taskId);
            ps.setInt(2, limit);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    list.add(map(rs));
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("查询任务历史失败 taskId=" + taskId, e);
        }
        return list;
    }
 
    @Override
    public List<TaskHistory> listRecent(int limit) {
        List<TaskHistory> list = new ArrayList<>();
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement(
                     "SELECT * FROM t_task_history ORDER BY id DESC LIMIT ?")) {
            ps.setInt(1, limit);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    list.add(map(rs));
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("查询最近历史失败", e);
        }
        return list;
    }
 
    @Override
    public long[] countStats() {
        try (Connection c = Db.open();
             Statement st = c.createStatement();
             ResultSet rs = st.executeQuery(
                     "SELECT COUNT(*), SUM(CASE WHEN is_success THEN 1 ELSE 0 END) FROM t_task_history")) {
            if (rs.next()) {
                long total = rs.getLong(1);
                long ok = rs.getLong(2);
                return new long[]{total, ok, total - ok};
            }
        } catch (Exception e) {
            throw new RuntimeException("统计历史失败", e);
        }
        return new long[]{0, 0, 0};
    }
 
    @Override
    public long countToday() {
        String today = LocalDate.now().toString();
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement(
                     "SELECT COUNT(*) FROM t_task_history WHERE execute_start_time LIKE ?")) {
            ps.setString(1, today + "%");
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? rs.getLong(1) : 0;
            }
        } catch (Exception e) {
            throw new RuntimeException("统计今日失败", e);
        }
    }
 
    @Override
    public List<String[]> countByDay(int days) {
        Map<String, Long> count = new HashMap<>();
        try (Connection c = Db.open();
             Statement st = c.createStatement();
             ResultSet rs = st.executeQuery("SELECT execute_start_time FROM t_task_history")) {
            while (rs.next()) {
                String time = rs.getString(1);
                if (time != null && time.length() >= 10) {
                    count.merge(time.substring(0, 10), 1L, Long::sum);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("统计按天失败", e);
        }
        // 近 days 天补 0
        List<String[]> result = new ArrayList<>(days);
        DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDate today = LocalDate.now();
        for (int i = 0; i < days; i++) {
            String day = today.minusDays(days - 1 - i).format(f);
            result.add(new String[]{day, String.valueOf(count.getOrDefault(day, 0L))});
        }
        return result;
    }
 
    private TaskHistory map(ResultSet rs) throws java.sql.SQLException {
        TaskHistory h = new TaskHistory();
        h.setId(rs.getLong("id"));
        h.setTaskId(rs.getLong("task_id"));
        h.setStartTime(Db.parseTime(rs.getString("execute_start_time")));
        h.setEndTime(Db.parseTime(rs.getString("execute_end_time")));
        h.setSuccess(rs.getBoolean("is_success"));
        h.setMessage(rs.getString("execute_msg"));
        return h;
    }
 
    private static String truncate(String s, int max) {
        if (s == null) {
            return "";
        }
        return s.length() <= max ? s : s.substring(0, max);
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : TaskRepositoryJdbc.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.repository;
 
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
 
/** 任务仓储 H2/JDBC 实现(公共方法与接口语义对齐 .NET TaskRepo) */
public class TaskRepositoryJdbc implements TaskRepository {
 
    @Override
    public List<Task> findAll() {
        List<Task> list = new ArrayList<>();
        try (Connection c = Db.open();
             Statement st = c.createStatement();
             ResultSet rs = st.executeQuery("SELECT * FROM t_task ORDER BY id")) {
            while (rs.next()) {
                list.add(map(rs));
            }
        } catch (Exception e) {
            throw new RuntimeException("查询任务失败", e);
        }
        return list;
    }
 
    @Override
    public List<Task> findEnabled() {
        List<Task> list = new ArrayList<>();
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("SELECT * FROM t_task WHERE is_enable = TRUE ORDER BY id")) {
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    list.add(map(rs));
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("查询启用任务失败", e);
        }
        return list;
    }
 
    @Override
    public long count() {
        try (Connection c = Db.open();
             Statement st = c.createStatement();
             ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM t_task")) {
            return rs.next() ? rs.getLong(1) : 0;
        } catch (Exception e) {
            throw new RuntimeException("统计任务失败", e);
        }
    }
 
    @Override
    public List<Task> searchPage(String keyword, int page, int pageSize) {
        if (page < 1) {
            page = 1;
        }
        if (pageSize < 1) {
            pageSize = 10;
        }
        List<Task> list = new ArrayList<>();
        String sql = "SELECT * FROM t_task";
        if (keyword != null && !keyword.isBlank()) {
            sql += " WHERE task_name LIKE ? OR remark LIKE ?";
        }
        sql += " ORDER BY id LIMIT ? OFFSET ?";
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement(sql)) {
            int i = 1;
            if (keyword != null && !keyword.isBlank()) {
                String kw = "%" + keyword.trim() + "%";
                ps.setString(i++, kw);
                ps.setString(i++, kw);
            }
            ps.setInt(i++, pageSize);
            ps.setInt(i, (page - 1) * pageSize);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    list.add(map(rs));
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("分页查询任务失败", e);
        }
        return list;
    }
 
    @Override
    public Task findById(long id) {
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("SELECT * FROM t_task WHERE id = ?")) {
            ps.setLong(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? map(rs) : null;
            }
        } catch (Exception e) {
            throw new RuntimeException("查询任务失败 id=" + id, e);
        }
    }
 
    @Override
    public Task save(Task task) {
        if (task.getId() <= 0) {
            try (Connection c = Db.open();
                 PreparedStatement ps = c.prepareStatement("""
                     INSERT INTO t_task(task_name, cron_expr, command, timeout_sec, max_retry, retry_gap_sec, is_enable, remark, task_status)
                     VALUES (?,?,?,?,?,?,?,?,?)
                     """, Statement.RETURN_GENERATED_KEYS)) {
                fill(ps, task);
                ps.executeUpdate();
                try (ResultSet keys = ps.getGeneratedKeys()) {
                    if (keys.next()) {
                        task.setId(keys.getLong(1));
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException("新增任务失败", e);
            }
        } else {
            try (Connection c = Db.open();
                 PreparedStatement ps = c.prepareStatement("""
                     UPDATE t_task SET task_name=?, cron_expr=?, command=?, timeout_sec=?, max_retry=?,
                     retry_gap_sec=?, is_enable=?, remark=?, task_status=? WHERE id=?
                     """)) {
                fill(ps, task);
                ps.setLong(10, task.getId());
                ps.executeUpdate();
            } catch (Exception e) {
                throw new RuntimeException("更新任务失败 id=" + task.getId(), e);
            }
        }
        return task;
    }
 
    private void fill(PreparedStatement ps, Task t) throws java.sql.SQLException {
        ps.setString(1, t.getName());
        ps.setString(2, t.getCronExpr());
        ps.setString(3, t.getCommand());
        ps.setInt(4, t.getTimeoutSec());
        ps.setInt(5, t.getMaxRetry());
        ps.setInt(6, t.getRetryGapSec());
        ps.setBoolean(7, t.isEnabled());
        ps.setString(8, t.getRemark());
        ps.setString(9, t.getStatus().name());
    }
 
    @Override
    public boolean deleteById(long id) {
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("DELETE FROM t_task WHERE id = ?")) {
            ps.setLong(1, id);
            return ps.executeUpdate() > 0;
        } catch (Exception e) {
            throw new RuntimeException("删除任务失败 id=" + id, e);
        }
    }
 
    @Override
    public boolean updateStatus(long id, TaskStatus status) {
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("UPDATE t_task SET task_status=? WHERE id=?")) {
            ps.setString(1, status.name());
            ps.setLong(2, id);
            return ps.executeUpdate() > 0;
        } catch (Exception e) {
            throw new RuntimeException("更新任务状态失败 id=" + id, e);
        }
    }
 
    @Override
    public boolean updateEnabled(long id, boolean enabled) {
        try (Connection c = Db.open();
             PreparedStatement ps = c.prepareStatement("UPDATE t_task SET is_enable=? WHERE id=?")) {
            ps.setBoolean(1, enabled);
            ps.setLong(2, id);
            return ps.executeUpdate() > 0;
        } catch (Exception e) {
            throw new RuntimeException("更新任务启用状态失败 id=" + id, e);
        }
    }
 
    private Task map(ResultSet rs) throws java.sql.SQLException {
        Task t = new Task();
        t.setId(rs.getLong("id"));
        t.setName(rs.getString("task_name"));
        t.setCronExpr(rs.getString("cron_expr"));
        t.setCommand(rs.getString("command"));
        t.setTimeoutSec(rs.getInt("timeout_sec"));
        t.setMaxRetry(rs.getInt("max_retry"));
        t.setRetryGapSec(rs.getInt("retry_gap_sec"));
        t.setEnabled(rs.getBoolean("is_enable"));
        t.setRemark(rs.getString("remark"));
        t.setStatus(TaskStatus.of(rs.getString("task_status")));
        return t;
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : CronCompat.java
 * explain   : 学习  类
 **/
 
package com.jewelry.taskscheduler.infrastructure.scheduler;
 
/**
 * Cron 兼容转换:标准 5 字段 → Quartz 6 字段
 *  5字段: 分 时 日 月 周(周日 0/7,支持 * , - /)
 *  6字段: 秒 分 时 日 月 周(Quartz 周字段 1-7,日/周互斥用 ?)
 */
public final class CronCompat {
 
    private CronCompat() {
    }
 
    /** 5 字段 → Quartz 6 字段 */
    public static String toQuartz(String cron) {
        if (cron == null) {
            throw new IllegalArgumentException("cron 不能为空");
        }
        String[] parts = cron.trim().split("\\s+");
        if (parts.length != 5) {
            throw new IllegalArgumentException("cron 必须是 5 字段(分 时 日 月 周): " + cron);
        }
        String minute = parts[0];
        String hour = parts[1];
        String dom = parts[2];
        String month = parts[3];
        String dow = parts[4];
 
        // 日与周互斥:指定了日期(非 *)→ 周用 ?;否则日用 ?
        if (dom.equals("*")) {
            dom = "?";
        } else {
            dow = "?";
        }
        return "0 " + minute + " " + hour + " " + dom + " " + month + " " + normalizeDow(dow);
    }
 
    /** 周字段 0-6 → 1-7(Quartz 周 1=周日):0→7,其余 +1;* / ? 原样返回 */
    private static String normalizeDow(String dow) {
        if (dow == null || dow.equals("?") || dow.equals("*")) {
            return dow;
        }
        StringBuilder sb = new StringBuilder();
        for (String part : dow.split(",")) {
            if (sb.length() > 0) {
                sb.append(',');
            }
            String p = part.trim();
            if (p.matches("\\d+")) {
                int v = Integer.parseInt(p);
                if (v == 0) {
                    v = 7;
                } else if (v > 0 && v <= 6) {
                    v++;
                } else {
                    throw new IllegalArgumentException("周字段取值 0-6 或 7 之外: " + dow);
                }
                sb.append(v);
            } else if (p.contains("-")) {
                // 范围 0-6 / 1-5
                String[] r = p.split("-");
                int lo = Integer.parseInt(r[0].trim());
                int hi = Integer.parseInt(r[1].trim());
                lo = lo == 0 ? 7 : lo + 1;
                hi = hi == 0 ? 7 : hi + 1;
                sb.append(lo).append('-').append(hi);
            } else if (p.contains("/")) {
                // 步长 0/2 → 7/2? 语义近似:保留起始映射
                String[] r = p.split("/");
                int base = Integer.parseInt(r[0].trim());
                base = base == 0 ? 7 : base + 1;
                sb.append(base).append('/').append(r[1].trim());
            } else {
                throw new IllegalArgumentException("不支持的周字段: " + dow);
            }
        }
        return sb.toString();
    }
 
    /** 校验 5 字段 cron 是否合法(Quartz 可解析) */
    public static void validate(String cron) {
        String q = toQuartz(cron);
        try {
            new org.quartz.CronExpression(q);
        } catch (Exception e) {
            throw new IllegalArgumentException("cron 无效(" + cron + " → " + q + "): " + e.getMessage(), e);
        }
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : DagTriggerJob.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.scheduler;
 
import com.jewelry.taskscheduler.application.DagService;
import com.jewelry.taskscheduler.config.AppConfig;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
 
/** Quartz DAG 触发 Job:到点执行整个 DAG 工作流 */
@DisallowConcurrentExecution
public class DagTriggerJob implements Job {
 
    @Override
    public void execute(JobExecutionContext context) {
        long dagId = context.getMergedJobDataMap().getLong("dagId");
        AppConfig.AppLog.info("Quartz 触发 DAG dag_id=" + dagId);
        DagService.instance().runAsync(dagId);
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : QuartzSchedulerService.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.scheduler;
 
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
 
import java.util.Date;
import java.util.List;
 
/**
 * Quartz 调度服务(基础设施层,单例)
 * 启动注册全部启用任务;注册/注销/暂停/恢复/手动触发/下次执行时间
 * 对应 .NET QuartzSchedulerService
 */
public class QuartzSchedulerService {
 
    private static final QuartzSchedulerService INSTANCE = new QuartzSchedulerService();
    private static final String GROUP = "tasks";
 
    private final TaskRepository taskRepo = new TaskRepositoryJdbc();
    private Scheduler scheduler;
 
    public static QuartzSchedulerService instance() {
        return INSTANCE;
    }
 
    /** 启动调度器并注册全部启用任务 */
    public synchronized void start() {
        if (scheduler != null) {
            return;
        }
        try {
            scheduler = new StdSchedulerFactory().getScheduler();
            scheduler.start();
            List<Task> enabled = taskRepo.findEnabled();
            for (Task t : enabled) {
                try {
                    register(t);
                } catch (Exception e) {
                    AppConfig.AppLog.error("Quartz 注册任务失败 task_id=" + t.getId() + ": " + e.getMessage());
                }
            }
            AppConfig.AppLog.info("Quartz 调度器已启动,共注册 " + enabled.size() + " 个启用任务");
        } catch (SchedulerException e) {
            throw new RuntimeException("Quartz 启动失败", e);
        }
    }
 
    public synchronized void shutdown() {
        if (scheduler != null) {
            try {
                scheduler.shutdown(true);
                AppConfig.AppLog.info("Quartz 调度器已停止");
            } catch (SchedulerException e) {
                AppConfig.AppLog.warn("Quartz 停止失败: " + e.getMessage());
            }
            scheduler = null;
        }
    }
 
    public boolean isRunning() {
        if (scheduler == null) {
            return false;
        }
        try {
            return scheduler.isStarted();
        } catch (SchedulerException e) {
            return false;
        }
    }
 
    public String stateText() {
        if (scheduler == null) {
            return "未启动";
        }
        try {
            return scheduler.isStarted() ? "运行中" : "已停止";
        } catch (SchedulerException e) {
            return "异常";
        }
    }
 
    /** 注册任务(cron 触发,重复注册覆盖) */
    public void register(Task task) throws SchedulerException {
        ensure();
        String cron6 = CronCompat.toQuartz(task.getCronExpr());
        JobDetail job = JobBuilder.newJob(TaskExecutionJob.class)
                .withIdentity("task-" + task.getId(), GROUP)
                .usingJobData(new JobDataMap(java.util.Map.of("taskId", task.getId())))
                .build();
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger-" + task.getId(), GROUP)
                .withSchedule(CronScheduleBuilder.cronSchedule(cron6))
                .forJob(job)
                .build();
        // Java 版无 scheduleJob(job, trigger, replace) 重载:先删旧注册实现覆盖语义
        scheduler.deleteJob(job.getKey());
        scheduler.scheduleJob(job, trigger);
        AppConfig.AppLog.info("Quartz 注册任务 task_id=" + task.getId() + " cron=" + task.getCronExpr() + " → " + cron6);
    }
 
    /** 注销任务 */
    public void unregister(long taskId) throws SchedulerException {
        if (scheduler == null) {
            return;
        }
        scheduler.unscheduleJob(TriggerKey.triggerKey("trigger-" + taskId, GROUP));
        scheduler.deleteJob(JobKey.jobKey("task-" + taskId, GROUP));
        AppConfig.AppLog.info("Quartz 注销任务 task_id=" + taskId);
    }
 
    /** 暂停任务 */
    public void pause(long taskId) throws SchedulerException {
        if (scheduler != null) {
            scheduler.pauseJob(JobKey.jobKey("task-" + taskId, GROUP));
            AppConfig.AppLog.info("Quartz 暂停任务 task_id=" + taskId);
        }
    }
 
    /** 恢复任务 */
    public void resume(long taskId) throws SchedulerException {
        if (scheduler != null) {
            scheduler.resumeJob(JobKey.jobKey("task-" + taskId, GROUP));
            AppConfig.AppLog.info("Quartz 恢复任务 task_id=" + taskId);
        }
    }
 
    /** 立即执行(手动触发,不影响 cron 计划):直接触发已注册 Job */
    public void runOnce(Task task) throws SchedulerException {
        ensure();
        JobKey key = JobKey.jobKey("task-" + task.getId(), GROUP);
        if (!scheduler.checkExists(key)) {
            // 任务未注册到 Quartz(如禁用状态)时先注册再触发
            register(task);
        }
        scheduler.triggerJob(key);
        AppConfig.AppLog.info("Quartz 手动触发任务 task_id=" + task.getId());
    }
 
    /** 注册 DAG(cron 触发整个工作流) */
    public void registerDag(DagDefinition dag) throws SchedulerException {
        ensure();
        if (dag.getCronExpr() == null || dag.getCronExpr().isBlank()) {
            return;
        }
        String cron6 = CronCompat.toQuartz(dag.getCronExpr());
        JobDetail job = JobBuilder.newJob(DagTriggerJob.class)
                .withIdentity("dag-" + dag.getId(), GROUP)
                .usingJobData(new JobDataMap(java.util.Map.of("dagId", dag.getId())))
                .build();
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("dagtrigger-" + dag.getId(), GROUP)
                .withSchedule(CronScheduleBuilder.cronSchedule(cron6))
                .forJob(job)
                .build();
        scheduler.deleteJob(job.getKey());
        scheduler.scheduleJob(job, trigger);
        AppConfig.AppLog.info("Quartz 注册 DAG 任务 dag_id=" + dag.getId() + " cron=" + dag.getCronExpr() + " → " + cron6);
    }
 
    public void unregisterDag(long dagId) throws SchedulerException {
        if (scheduler == null) {
            return;
        }
        scheduler.unscheduleJob(TriggerKey.triggerKey("dagtrigger-" + dagId, GROUP));
        scheduler.deleteJob(JobKey.jobKey("dag-" + dagId, GROUP));
    }
 
    /** 任务下次触发时间 */
    public Date nextFireTime(long taskId) {
        if (scheduler == null) {
            return null;
        }
        try {
            Trigger t = scheduler.getTrigger(TriggerKey.triggerKey("trigger-" + taskId, GROUP));
            return t == null ? null : t.getNextFireTime();
        } catch (SchedulerException e) {
            return null;
        }
    }
 
    private void ensure() throws SchedulerException {
        if (scheduler == null) {
            throw new SchedulerException("Quartz 调度器尚未启动");
        }
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : TaskExecutionJob.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.infrastructure.scheduler;
 
import com.jewelry.taskscheduler.application.TaskExecutionService;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.event.EventBusAdapter;
import com.jewelry.taskscheduler.infrastructure.redis.RedisSupport;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
 
/**
 * Quartz 任务执行 Job
 * @DisallowConcurrentExecution 防止同一任务并发(替代旧运行锁)
 * 流程:置 RUNNING → 分布式锁(可选)→ 执行(子进程+重试+写历史+事件)→ 一次性自禁用 → 置 IDLE
 */
@DisallowConcurrentExecution
public class TaskExecutionJob implements Job {
 
    private final TaskRepository taskRepo = new TaskRepositoryJdbc();
    private final TaskExecutionService executionService = new TaskExecutionService();
 
    @Override
    public void execute(JobExecutionContext context) {
        long taskId = context.getMergedJobDataMap().getLong("taskId");
        Task task = taskRepo.findById(taskId);
        if (task == null) {
            AppConfig.AppLog.warn("Quartz Job 找不到任务 task_id=" + taskId + ",已忽略执行");
            return;
        }
 
        // 分布式锁(Redis 开启时):防多实例重复执行同一任务
        String lockKey = "taskscheduler:lock:task:" + taskId;
        boolean locked = false;
        if (RedisSupport.instance().enabled()) {
            locked = RedisSupport.instance().tryLock(lockKey, 300);
            if (!locked) {
                AppConfig.AppLog.warn("任务 task_id=" + taskId + " 已被其他节点执行,本次跳过");
                return;
            }
        }
        try {
            taskRepo.updateStatus(taskId, TaskStatus.RUNNING);
            TaskHistory history = executionService.executeSingle(task);
            if (history != null) {
                AppConfig.AppLog.info("Quartz 执行完成 task_id=" + taskId
                        + " 结果=" + (history.isSuccess() ? "成功" : "失败"));
            }
            // 一次性任务(备注含"一次性")执行成功后自动禁用,真正"只执行一次"
            if (task.isOneTime() && history != null && history.isSuccess()) {
                taskRepo.updateEnabled(taskId, false);
                AppConfig.AppLog.info("一次性任务 task_id=" + taskId + "【" + task.getName() + "】执行完成,已自动禁用");
                QuartzSchedulerService.instance().unregister(taskId);
            }
        } catch (Exception e) {
            AppConfig.AppLog.error("Quartz 任务执行异常 task_id=" + taskId + ": " + e.getMessage());
            EventBusAdapter.instance().post(new DomainEvent.TaskFailedEvent(taskId, task.getName(), e.getMessage()));
        } finally {
            taskRepo.updateStatus(taskId, TaskStatus.IDLE);
            if (locked) {
                RedisSupport.instance().unlock(lockKey);
            }
        }
    }
}
java 复制代码
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : DagService.java
 * explain   : 学习  类
 **/
 
package com.jewelry.taskscheduler.application;
 
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import com.jewelry.taskscheduler.domain.model.DagNode;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.DagRepository;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.event.EventBusAdapter;
import com.jewelry.taskscheduler.infrastructure.repository.DagRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import org.quartz.SchedulerException;
 
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * DAG 工作流服务(应用层):
 * 任务依赖编排(A 完成后执行 B,支持并行分支),依赖失败按 onFail 策略 STOP/SKIP
 * 支持 cron 定时触发整个 DAG(经 Quartz DagTriggerJob)
 */
public class DagService {
 
    private static final DagService INSTANCE = new DagService();
 
    private final DagRepository dagRepo = new DagRepositoryJdbc();
    private final TaskRepository taskRepo = new TaskRepositoryJdbc();
    private final TaskExecutionService executionService = new TaskExecutionService();
    private final ExecutorService executor = Executors.newCachedThreadPool(r -> {
        Thread t = new Thread(r, "dag-exec");
        t.setDaemon(true);
        return t;
    });
 
    public static DagService instance() {
        return INSTANCE;
    }
 
    public List<DagDefinition> findAll() {
        return dagRepo.findAll();
    }
 
    public DagDefinition findById(long id) {
        return dagRepo.findById(id);
    }
 
    /** 保存 DAG 并同步 Quartz(有 cron 则注册触发) */
    public DagDefinition save(DagDefinition dag) {
        if (dag.getCreatedAt() == null) {
            dag.setCreatedAt(LocalDateTime.now());
        }
        DagDefinition saved = dagRepo.save(dag);
        syncScheduler(saved);
        return saved;
    }
 
    public boolean delete(long id) {
        boolean ok = dagRepo.deleteById(id);
        if (ok) {
            try {
                QuartzSchedulerService.instance().unregisterDag(id);
            } catch (SchedulerException e) {
                AppConfig.AppLog.warn("删除 DAG 后注销 Quartz 失败 id=" + id);
            }
        }
        return ok;
    }
 
    private void syncScheduler(DagDefinition dag) {
        try {
            if (dag.isEnabled() && dag.getCronExpr() != null && !dag.getCronExpr().isBlank()) {
                QuartzSchedulerService.instance().registerDag(dag);
            } else {
                QuartzSchedulerService.instance().unregisterDag(dag.getId());
            }
        } catch (SchedulerException e) {
            AppConfig.AppLog.error("同步 DAG 调度失败 dag_id=" + dag.getId() + ": " + e.getMessage());
        }
    }
 
    /** 后台异步执行 DAG */
    public void runAsync(long dagId) {
        executor.submit(() -> run(dagId));
    }
 
    /** 执行 DAG:Kahn 拓扑排序,按依赖串行/并行执行节点 */
    public boolean run(long dagId) {
        DagDefinition dag = dagRepo.findById(dagId);
        if (dag == null) {
            AppConfig.AppLog.error("DAG 不存在 id=" + dagId);
            return false;
        }
        AppConfig.AppLog.info("DAG 开始执行 dag_id=" + dagId + " 名称=" + dag.getName()
                + " 节点数=" + dag.getNodes().size());
 
        // 节点 key → 节点
        Map<String, DagNode> byKey = new HashMap<>();
        for (DagNode n : dag.getNodes()) {
            byKey.put(n.getKey(), n);
        }
        // 入度
        Map<String, Integer> indegree = new HashMap<>();
        Map<String, List<String>> dependents = new HashMap<>();
        for (DagNode n : dag.getNodes()) {
            indegree.put(n.getKey(), 0);
            dependents.put(n.getKey(), new ArrayList<>());
        }
        for (DagNode n : dag.getNodes()) {
            for (String dep : n.getDependsOn()) {
                if (byKey.containsKey(dep)) {
                    indegree.merge(n.getKey(), 1, Integer::sum);
                    dependents.get(dep).add(n.getKey());
                }
            }
        }
 
        Queue<String> ready = new LinkedList<>();
        for (Map.Entry<String, Integer> e : indegree.entrySet()) {
            if (e.getValue() == 0) {
                ready.add(e.getKey());
            }
        }
 
        Set<String> executed = new HashSet<>();
        Set<String> failed = new HashSet<>();
        boolean stopped = false;
        int executedCount = 0;
 
        while (!ready.isEmpty() && !stopped) {
            String key = ready.poll();
            if (executed.contains(key)) {
                continue;
            }
            DagNode node = byKey.get(key);
 
            // 依赖中是否有失败节点
            boolean depFailed = node.getDependsOn().stream().anyMatch(failed::contains);
            if (depFailed) {
                // 上游失败:SKIP 节点按跳过处理;STOP 节点阻断整个 DAG
                String failPolicy = node.getOnFail() == null ? "STOP" : node.getOnFail();
                if ("SKIP".equalsIgnoreCase(failPolicy)) {
                    AppConfig.AppLog.warn("DAG 节点 " + node.getKey() + " 因上游失败已跳过(SKIP)");
                    executed.add(key);
                    executedCount++;
                } else {
                    AppConfig.AppLog.warn("DAG 节点 " + node.getKey() + " 上游失败,按 STOP 策略阻断下游");
                    stopped = true;
                    break;
                }
            }
 
            if (!depFailed) {
                boolean ok = executeNode(dag, node);
                executed.add(key);
                executedCount++;
                if (ok) {
                    AppConfig.AppLog.info("DAG 节点执行成功: " + node.getKey());
                } else {
                    failed.add(key);
                    AppConfig.AppLog.error("DAG 节点执行失败: " + node.getKey() + "(onFail=" + node.getOnFail() + ")");
                    // 节点失败:本节点无下游则继续,有下游则按下游策略处理
                }
            }
 
            // 解锁下游
            for (String d : dependents.getOrDefault(key, List.of())) {
                indegree.merge(d, -1, Integer::sum);
                if (indegree.get(d) == 0) {
                    ready.add(d);
                }
            }
        }
 
        boolean success = failed.isEmpty() && !stopped;
        AppConfig.AppLog.info("DAG 执行完成 dag_id=" + dagId + " 结果=" + (success ? "成功" : "失败")
                + " 执行节点=" + executedCount + "/" + dag.getNodes().size());
        EventBusAdapter.instance().post(new DomainEvent.DagExecutedEvent(dagId, dag.getName(), success));
        return success;
    }
 
    /** 执行单个 DAG 节点:优先绑定任务(taskKey),否则内联命令 */
    private boolean executeNode(DagDefinition dag, DagNode node) {
        String command = null;
        int timeout = node.getTimeoutSec() > 0 ? node.getTimeoutSec() : 60;
 
        if (node.getTaskKey() != null && !node.getTaskKey().isBlank()) {
            // taskKey 格式:task-{id}
            try {
                long tid = Long.parseLong(node.getTaskKey().replace("task-", "").trim());
                Task t = taskRepo.findById(tid);
                if (t != null) {
                    command = t.getCommand();
                    timeout = t.getTimeoutSec();
                }
            } catch (NumberFormatException ignored) {
            }
        }
        if (command == null && node.getCommand() != null && !node.getCommand().isBlank()) {
            command = node.getCommand();
        }
        if (command == null) {
            AppConfig.AppLog.error("DAG 节点 " + node.getKey() + " 无命令且未绑定任务,执行失败");
            return false;
        }
 
        // 重试
        int attempts = 1 + Math.max(0, node.getRetry());
        for (int i = 0; i < attempts; i++) {
            TaskExecutionService.RunOutcome outcome = executionService.executeCommand(command, timeout);
            if (outcome.success) {
                return true;
            }
            if (i < attempts - 1) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            }
        }
        return false;
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : DashboardService.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.application;
 
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import com.jewelry.taskscheduler.domain.repository.TaskHistoryRepository;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.repository.TaskHistoryRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
 
import java.util.List;
 
/**
 * 仪表盘服务(应用层):统计面板数据 + 近7天柱状图 + 最近执行记录
 */
public class DashboardService {
 
    private final TaskRepository taskRepo = new TaskRepositoryJdbc();
    private final TaskHistoryRepository historyRepo = new TaskHistoryRepositoryJdbc();
 
    public long taskTotal() {
        return taskRepo.count();
    }
 
    public long taskEnabled() {
        return taskRepo.findEnabled().size();
    }
 
    public long taskRunning() {
        return taskRepo.findAll().stream().filter(t -> t.getStatus() == TaskStatus.RUNNING).count();
    }
 
    /** 今日成功/失败(按最近 200 条统计) */
    public long[] todayOkFail() {
        long ok = 0;
        long fail = 0;
        String today = java.time.LocalDate.now().toString();
        for (TaskHistory h : historyRepo.listRecent(200)) {
            if (h.getStartTime() != null && h.getStartTime().toLocalDate().toString().equals(today)) {
                if (h.isSuccess()) {
                    ok++;
                } else {
                    fail++;
                }
            }
        }
        return new long[]{ok, fail};
    }
 
    public long[] countStats() {
        return historyRepo.countStats();
    }
 
    public List<TaskHistory> listRecent(int limit) {
        return historyRepo.listRecent(limit);
    }
 
    /** 近7天执行次数(含补0),返回 [日期, 次数] */
    public List<String[]> countByDay(int days) {
        return historyRepo.countByDay(days);
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : TaskExecutionService.java
 * explain   : 学习  类
 **/
 
 
package com.jewelry.taskscheduler.application;
 
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.repository.TaskHistoryRepository;
import com.jewelry.taskscheduler.infrastructure.event.EventBusAdapter;
import com.jewelry.taskscheduler.infrastructure.repository.TaskHistoryRepositoryJdbc;
 
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * 任务执行服务(应用层):子进程执行 + 超时 + 失败重试 + 写历史 + 领域事件
 * 对应 .NET TaskSchedulerService.ExecuteSingleTask
 */
public class TaskExecutionService {
 
    private final TaskHistoryRepository historyRepo = new TaskHistoryRepositoryJdbc();
 
    /** 执行单任务(含重试),返回最后一条历史 */
    public TaskHistory executeSingle(Task task) {
        int attempts = 1 + Math.max(0, task.getMaxRetry());
        TaskHistory last = null;
        for (int i = 0; i < attempts; i++) {
            last = executeOnce(task);
            if (last.isSuccess()) {
                return last;
            }
            if (i < attempts - 1) {
                AppConfig.AppLog.warn(String.format("task_id=%d 执行失败,第%d次重试,间隔%d秒",
                        task.getId(), i + 1, task.getRetryGapSec()));
                try {
                    Thread.sleep(Math.max(1, task.getRetryGapSec()) * 1000L);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
        return last;
    }
 
    /** 执行一次任务并写历史 */
    private TaskHistory executeOnce(Task task) {
        LocalDateTime start = LocalDateTime.now();
        AppConfig.AppLog.info("开始执行任务 task_id=" + task.getId() + " 任务名称=" + task.getName());
        EventBusAdapter.instance().post(new DomainEvent.TaskStartedEvent(task.getId(), task.getName()));
 
        RunOutcome outcome = executeCommand(task.getCommand(), task.getTimeoutSec());
        LocalDateTime end = LocalDateTime.now();
 
        TaskHistory h = new TaskHistory();
        h.setTaskId(task.getId());
        h.setStartTime(start);
        h.setEndTime(end);
        h.setSuccess(outcome.success);
        h.setMessage(outcome.output);
        historyRepo.insert(h);
 
        if (outcome.success) {
            AppConfig.AppLog.info("任务执行成功 task_id=" + task.getId());
            EventBusAdapter.instance().post(new DomainEvent.TaskCompletedEvent(task.getId(), task.getName(), outcome.output));
        } else {
            AppConfig.AppLog.error("任务执行失败 task_id=" + task.getId() + ",输出=" + outcome.output);
            EventBusAdapter.instance().post(new DomainEvent.TaskFailedEvent(task.getId(), task.getName(), outcome.output));
        }
        return h;
    }
 
    /** 执行命令(无历史记录,DAG 节点使用) */
    public RunOutcome executeCommand(String command, int timeoutSec) {
        // 执行前自动创建输出目标目录(echo >> logs\xx.log、copy ... db_backup\xx.db)
        prepareOutputDirs(command);
        try {
            String os = System.getProperty("os.name", "").toLowerCase();
            ProcessBuilder pb = os.contains("win")
                    ? new ProcessBuilder("cmd.exe", "/C", command)
                    : new ProcessBuilder("/bin/sh", "-c", command);
            pb.redirectErrorStream(true);
            Process p = pb.start();
            byte[] out = readAll(p.getInputStream());
            boolean finished = p.waitFor(Math.max(1, timeoutSec), TimeUnit.SECONDS);
            if (!finished) {
                p.destroyForcibly();
                return new RunOutcome(false, "【超时】超过" + timeoutSec + "秒被强制终止");
            }
            String output = decodeText(out);
            boolean ok = p.exitValue() == 0;
            if (!ok && output.isBlank()) {
                output = "退出码: " + p.exitValue();
            }
            return new RunOutcome(ok, output);
        } catch (Exception e) {
            return new RunOutcome(false, "启动进程失败: " + e.getMessage());
        }
    }
 
    /** 执行结果 */
    public static class RunOutcome {
        public final boolean success;
        public final String output;
 
        public RunOutcome(boolean success, String output) {
            this.success = success;
            this.output = output;
        }
    }
 
    /** 读取全部字节 */
    private static byte[] readAll(InputStream in) throws java.io.IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buf = new byte[8192];
        int n;
        while ((n = in.read(buf)) != -1) {
            bos.write(buf, 0, n);
        }
        return bos.toByteArray();
    }
 
    /**
     * 智能解码:UTF-8 严格解码 + round-trip 校验,失败回退系统本地编码(Windows=GBK)
     * 修复 cmd 中文输出乱码
     */
    private static String decodeText(byte[] bytes) {
        if (bytes.length == 0) {
            return "";
        }
        // 1) UTF-8 严格解码 + round-trip 校验(重编码字节与原文一致才认定为 UTF-8 文本)
        try {
            String s = StandardCharsets.UTF_8.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT)
                    .decode(java.nio.ByteBuffer.wrap(bytes)).toString();
            java.nio.ByteBuffer re = StandardCharsets.UTF_8.encode(s);
            byte[] reBytes = new byte[re.remaining()];
            re.get(reBytes);
            if (java.util.Arrays.equals(reBytes, bytes)) {
                return s;
            }
        } catch (CharacterCodingException | RuntimeException ignored) {
            // 非 UTF-8,走回退
        }
        // 2) 回退系统本地编码(中文 Windows 为 GBK)
        try {
            return new String(bytes, Charset.forName(System.getProperty("sun.jnu.encoding", "GBK")));
        } catch (Exception e) {
            return new String(bytes, StandardCharsets.UTF_8);
        }
    }
 
    /** 执行前自动创建命令输出目标目录(重定向目标 / copy 目标) */
    private static void prepareOutputDirs(String command) {
        if (command == null || command.isBlank()) {
            return;
        }
        List<String> targets = new java.util.ArrayList<>();
        // 重定向:>> / > 后路径
        Pattern redirect = Pattern.compile("(?:>>|>)\\s*(?:\"([^\"]+)\"|([^\\s&|;<>]+))");
        Matcher m = redirect.matcher(command);
        while (m.find()) {
            String t = m.group(1) != null ? m.group(1) : m.group(2);
            if (t != null && !t.isBlank()) {
                targets.add(t.trim());
            }
        }
        // copy 命令目标
        Matcher cm = Pattern.compile("\\bcopy\\b[^\\r\\n]*", Pattern.CASE_INSENSITIVE).matcher(command);
        if (cm.find()) {
            String[] parts = cm.group().split("[ \\t]+");
            List<String> nonSwitch = new java.util.ArrayList<>();
            for (String p : parts) {
                if (!p.startsWith("/")) {
                    nonSwitch.add(p);
                }
            }
            if (nonSwitch.size() >= 3) {
                targets.add(nonSwitch.get(nonSwitch.size() - 1));
            }
        }
        String workDir = System.getProperty("user.dir");
        for (String t : targets) {
            try {
                Path p = Path.of(t);
                Path full = p.isAbsolute() ? p : Path.of(workDir).resolve(p).normalize();
                Path parent = full.getParent();
                if (parent != null) {
                    Files.createDirectories(parent);
                }
            } catch (Exception ignored) {
                // 解析失败不阻塞
            }
        }
    }
}
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : TaskService.java
 * explain   : 学习  类
 **/
 
 
 
package com.jewelry.taskscheduler.application;
 
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import org.quartz.SchedulerException;
 
import java.util.ArrayList;
import java.util.List;
 
/**
 * 任务服务(应用层,DDD 用例层):
 * CRUD + Quartz 同步注册/注销 + 暂停/恢复/立即执行 + JSON 导入导出
 */
public class TaskService {
 
    private final TaskRepository taskRepo = new TaskRepositoryJdbc();
 
    public List<Task> findAll() {
        return taskRepo.findAll();
    }
 
    public List<Task> findEnabled() {
        return taskRepo.findEnabled();
    }
 
    public long count() {
        return taskRepo.count();
    }
 
    public List<Task> searchPage(String keyword, int page, int pageSize) {
        return taskRepo.searchPage(keyword, page, pageSize);
    }
 
    public Task findById(long id) {
        return taskRepo.findById(id);
    }
 
    /** 保存(新增/更新)并同步 Quartz 注册/注销 */
    public Task save(Task task) {
        CronCompatValidate(task.getCronExpr());
        Task saved = taskRepo.save(task);
        syncScheduler(saved);
        return saved;
    }
 
    public boolean delete(long id) {
        boolean ok = taskRepo.deleteById(id);
        if (ok) {
            try {
                QuartzSchedulerService.instance().unregister(id);
            } catch (SchedulerException e) {
                AppConfig.AppLog.warn("删除任务后注销 Quartz 失败 id=" + id + ": " + e.getMessage());
            }
        }
        return ok;
    }
 
    /** 启用任务 → 注册;禁用任务 → 注销 */
    private void syncScheduler(Task task) {
        try {
            if (task.isEnabled()) {
                QuartzSchedulerService.instance().register(task);
            } else {
                QuartzSchedulerService.instance().unregister(task.getId());
            }
        } catch (SchedulerException e) {
            AppConfig.AppLog.error("同步 Quartz 失败 task_id=" + task.getId() + ": " + e.getMessage());
        }
    }
 
    public void pause(long id) {
        try {
            QuartzSchedulerService.instance().pause(id);
        } catch (SchedulerException e) {
            AppConfig.AppLog.error("暂停任务失败 id=" + id + ": " + e.getMessage());
        }
    }
 
    public void resume(long id) {
        try {
            QuartzSchedulerService.instance().resume(id);
        } catch (SchedulerException e) {
            AppConfig.AppLog.error("恢复任务失败 id=" + id + ": " + e.getMessage());
        }
    }
 
    public void runNow(Task task) {
        try {
            QuartzSchedulerService.instance().runOnce(task);
        } catch (SchedulerException e) {
            AppConfig.AppLog.error("立即执行失败 id=" + task.getId() + ": " + e.getMessage());
        }
    }
 
    public java.util.Date nextFireTime(long id) {
        return QuartzSchedulerService.instance().nextFireTime(id);
    }
 
    /** 启用/禁用并同步调度器 */
    public void setEnabled(long id, boolean enabled) {
        taskRepo.updateEnabled(id, enabled);
        Task t = taskRepo.findById(id);
        if (t != null) {
            t.setEnabled(enabled);
            syncScheduler(t);
        }
    }
 
    /** cron 校验(5字段) */
    private void CronCompatValidate(String cron) {
        com.jewelry.taskscheduler.infrastructure.scheduler.CronCompat.validate(cron);
    }
 
    /** 导出全部任务为 JSON(与 examples/task_examples.json 的 tasks 结构一致) */
    public void exportTasksToJson(String filePath) throws Exception {
        List<Task> tasks = taskRepo.findAll();
        var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
        var root = new java.util.LinkedHashMap<String, Object>();
        root.put("tasks", tasks);
        root.put("dags", new ArrayList<>());
        try (var out = new java.io.BufferedWriter(new java.io.OutputStreamWriter(
                new java.io.FileOutputStream(filePath), java.nio.charset.StandardCharsets.UTF_8))) {
            out.write(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(root));
        }
    }
 
    /** 从 JSON 导入任务,兼容三种结构:
     * 1) {"tasks":[...]} 对象;2) 纯任务数组;3) 字段兼容 camelCase(name/cronExpr...)与 snake_case(task_name/cron_expr/is_enable...) */
    public int importTasksFromJson(String filePath) throws Exception {
        var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
        String text;
        try (var in = new java.io.BufferedReader(new java.io.InputStreamReader(
                new java.io.FileInputStream(filePath), java.nio.charset.StandardCharsets.UTF_8))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = in.readLine()) != null) {
                sb.append(line).append('\n');
            }
            text = sb.toString();
        }
        com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(text);
        com.fasterxml.jackson.databind.JsonNode arr;
        if (root.isArray()) {
            arr = root;
        } else if (root.has("tasks") && root.get("tasks").isArray()) {
            arr = root.get("tasks");
        } else {
            throw new IllegalArgumentException("JSON 需为任务数组,或包含 tasks 数组的对象");
        }
        if (!arr.iterator().hasNext()) {
            throw new IllegalArgumentException("没有可导入的任务");
        }
        int imported = 0;
        for (com.fasterxml.jackson.databind.JsonNode n : arr) {
            if (!n.isObject()) {
                continue;
            }
            Task t = parseTaskNode(n);
            if (t == null) {
                continue;
            }
            save(t);
            imported++;
        }
        return imported;
    }
 
    /** 按节点解析任务:camelCase/snake_case 字段名都兼容,缺 name/cron 或两者皆空则跳过 */
    private Task parseTaskNode(com.fasterxml.jackson.databind.JsonNode n) {
        String name = nodeStr(n, "name", "task_name");
        String cron = nodeStr(n, "cronExpr", "cron_expr");
        if (name == null || name.isBlank() || cron == null || cron.isBlank()) {
            return null;
        }
        Task t = new Task();
        t.setName(name.trim());
        t.setCronExpr(cron.trim());
        String command = nodeStr(n, "command", "command");
        t.setCommand(command == null ? "" : command.trim());
        t.setTimeoutSec(nodeInt(n, 30, "timeoutSec", "timeout_sec"));
        t.setMaxRetry(nodeInt(n, 0, "maxRetry", "max_retry"));
        t.setRetryGapSec(nodeInt(n, 5, "retryGapSec", "retry_gap_sec"));
        t.setEnabled(nodeBool(n, true, "enabled", "is_enable"));
        String remark = nodeStr(n, "remark", "remark");
        t.setRemark(remark == null ? "" : remark.trim());
        return t;
    }
 
    private String nodeStr(com.fasterxml.jackson.databind.JsonNode n, String camel, String snake) {
        return n.hasNonNull(camel) ? n.get(camel).asText() : (n.hasNonNull(snake) ? n.get(snake).asText() : null);
    }
 
    private int nodeInt(com.fasterxml.jackson.databind.JsonNode n, int def, String camel, String snake) {
        com.fasterxml.jackson.databind.JsonNode v = n.has(camel) ? n.get(camel) : (n.has(snake) ? n.get(snake) : null);
        return v == null || v.isNull() || !v.canConvertToInt() ? def : v.asInt();
    }
 
    private boolean nodeBool(com.fasterxml.jackson.databind.JsonNode n, boolean def, String camel, String snake) {
        if (n.has(camel)) {
            return n.get(camel).asBoolean(def);
        }
        if (n.has(snake)) {
            return n.get(snake).asBoolean(def);
        }
        return def;
    }
}
java 复制代码
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : MainController.java
 * explain   : 学习  类
 **/
package com.jewelry.taskscheduler.ui;
import com.jewelry.taskscheduler.application.DagService;
import com.jewelry.taskscheduler.application.DashboardService;
import com.jewelry.taskscheduler.application.TaskService;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
import java.text.SimpleDateFormat;
import java.util.List;
/**
主控制器(MVC Controller):协调 View 与 应用层服务
*/
public class MainController {
private final TaskService taskService = new TaskService();
private final DashboardService dashboardService = new DashboardService();
private final DagService dagService = DagService.instance();
private MainWindow window;
public TaskService taskService() {
return taskService;
}
public DashboardService dashboardService() {
return dashboardService;
}
public DagService dagService() {
return dagService;
}
public void setWindow(MainWindow window) {
this.window = window;
}
public MainWindow window() {
return window;
}
// ===== 任务操作 =====
public void newTask(JFrame owner) {
new TaskEditDialog(owner, new Task(), taskService::save).setVisible(true);
window.refreshAll();
}
public void editTask(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
new TaskEditDialog(owner, task, taskService::save).setVisible(true);
window.refreshAll();
}
public void deleteTask(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
if (JOptionPane.showConfirmDialog(owner, "确定删除任务【" + task.getName() + "】?",
"确认删除", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
taskService.delete(task.getId());
window.refreshAll();
}
}
public void runNow(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
taskService.runNow(task);
}
public void pause(JFrame owner, Task task) {
if (task == null) {
return;
}
taskService.pause(task.getId());
window.refreshAll();
}
public void resume(JFrame owner, Task task) {
if (task == null) {
return;
}
taskService.resume(task.getId());
window.refreshAll();
}
public void toggleEnabled(JFrame owner, Task task) {
if (task == null) {
return;
}
taskService.setEnabled(task.getId(), !task.isEnabled());
window.refreshAll();
}
/** 查看任务执行历史弹窗 */
public void showHistory(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
List<TaskHistory> list = dashboardService().listRecent(50).stream()
.filter(h -> h.getTaskId() == task.getId())
.toList();
DefaultTableModel m = new DefaultTableModel(new String[]{"ID", "开始时间", "结束时间", "结果", "耗时(秒)", "信息"}, 0) {
@Override
public boolean isCellEditable(int r, int c) {
return false;
}
};
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (TaskHistory h : list) {
m.addRow(new Object[]{
h.getId(),
h.getStartTime() == null ? "" : h.getStartTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
h.getEndTime() == null ? "" : h.getEndTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
h.isSuccess() ? "成功" : "失败",
String.format("%.1f", h.costMillis() / 1000.0),
h.getMessage() == null ? "" : h.getMessage()});
}
JTable t = new JTable(m);
t.setRowHeight(24);
JOptionPane.showMessageDialog(owner, new JScrollPane(t),
"任务【" + task.getName() + "】执行历史(最近50条)", JOptionPane.PLAIN_MESSAGE);
}
/** 全局刷新入口(Swing Timer 5 秒调用) */
public void refreshAll() {
if (window == null) {
return;
}
window.refreshAll();
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : MainWindow.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.ui;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.infrastructure.redis.RedisSupport;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import com.jewelry.taskscheduler.ui.panel.DagPanel;
import com.jewelry.taskscheduler.ui.panel.DashboardPanel;
import com.jewelry.taskscheduler.ui.panel.LogPanel;
import com.jewelry.taskscheduler.ui.panel.TaskListPanel;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
主窗口(MVC View):工具栏 + 四标签页 + 状态栏
Swing Timer 自动刷新:仪表盘、任务下次执行时间、集群节点、统计面板
*/
public class MainWindow extends JFrame {
private final MainController controller;
private final TaskListPanel taskPanel;
private final DashboardPanel dashboardPanel;
private final DagPanel dagPanel;
private final LogPanel logPanel;
private JTabbedPane tabs;
private final JLabel lblQuartz = new JLabel();
private final JLabel lblRedis = new JLabel();
private final JLabel lblNodes = new JLabel();
private final JLabel lblTasks = new JLabel();
private final JLabel lblTime = new JLabel();
public MainWindow(MainController controller) {
super("Java 任务调度系统(Swing + DDD + MVC + Quartz + Redis + DAG)");
this.controller = controller;
 taskPanel = new TaskListPanel(controller);
 dashboardPanel = new DashboardPanel();
 dagPanel = new DagPanel();
 logPanel = new LogPanel();

 buildToolBar();
 buildTabs();
 buildStatusBar();

 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 setSize(1120, 760);
 setMinimumSize(new Dimension(960, 640));
 setLocationRelativeTo(null);

 addWindowListener(new WindowAdapter() {
     @Override
     public void windowClosing(WindowEvent e) {
         shutdown();
     }
 });

 // 全局异常捕获
 Thread.setDefaultUncaughtExceptionHandler((t, ex) -&gt; {
     AppConfig.AppLog.error("未捕获异常 [" + t.getName() + "]: " + ex);
     ex.printStackTrace();
 });

 // 自动刷新定时器(Swing Timer,按配置间隔)
 int intervalMs = Math.max(1000, AppConfig.INSTANCE.refreshIntervalSec * 1000);
 Timer timer = new Timer(intervalMs, e -&gt; SwingUtilities.invokeLater(this::refreshAll));
 timer.start();

 refreshAll();
}
private void buildToolBar() {
JToolBar bar = new JToolBar();
bar.setFloatable(false);
 JButton btnAdd = new JButton("新增任务");
 JButton btnEdit = new JButton("编辑");
 JButton btnDel = new JButton("删除");
 JButton btnRun = new JButton("立即执行");
 JButton btnPause = new JButton("暂停");
 JButton btnResume = new JButton("恢复");
 JButton btnEnable = new JButton("启用/禁用");
 JButton btnHistory = new JButton("执行历史");
 JButton btnDag = new JButton("DAG编排");
 JButton btnImport = new JButton("导入JSON");
 JButton btnExport = new JButton("导出JSON");

 btnAdd.addActionListener(e -&gt; controller.newTask(this));
 btnEdit.addActionListener(e -&gt; controller.editTask(this, taskPanel.selectedTask()));
 btnDel.addActionListener(e -&gt; controller.deleteTask(this, taskPanel.selectedTask()));
 btnRun.addActionListener(e -&gt; controller.runNow(this, taskPanel.selectedTask()));
 btnPause.addActionListener(e -&gt; controller.pause(this, taskPanel.selectedTask()));
 btnResume.addActionListener(e -&gt; controller.resume(this, taskPanel.selectedTask()));
 btnEnable.addActionListener(e -&gt; controller.toggleEnabled(this, taskPanel.selectedTask()));
 btnHistory.addActionListener(e -&gt; controller.showHistory(this, taskPanel.selectedTask()));
 btnDag.addActionListener(e -&gt; tabs.setSelectedIndex(2));
 btnImport.addActionListener(e -&gt; taskPanel.importJson());
 btnExport.addActionListener(e -&gt; taskPanel.exportJson());

 bar.add(btnAdd);
 bar.add(btnEdit);
 bar.add(btnDel);
 bar.addSeparator();
 bar.add(btnRun);
 bar.add(btnPause);
 bar.add(btnResume);
 bar.add(btnEnable);
 bar.addSeparator();
 bar.add(btnHistory);
 bar.add(btnDag);
 bar.addSeparator();
 bar.add(btnImport);
 bar.add(btnExport);

 add(bar, BorderLayout.NORTH);
}
private void buildTabs() {
tabs = new JTabbedPane();
tabs.addTab("任务列表", taskPanel);
tabs.addTab("仪表盘", dashboardPanel);
tabs.addTab("DAG 工作流", dagPanel);
tabs.addTab("执行日志", logPanel);
add(tabs, BorderLayout.CENTER);
}
private void buildStatusBar() {
JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT, 14, 3));
bar.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new java.awt.Color(210, 210, 218)));
bar.add(lblQuartz);
bar.add(lblRedis);
bar.add(lblNodes);
bar.add(lblTasks);
bar.add(lblTime);
add(bar, BorderLayout.SOUTH);
}
/** 全局刷新(定时器与操作后调用) */
public void refreshAll() {
taskPanel.reload();
dashboardPanel.refresh();
dagPanel.reload();
 lblQuartz.setText("Quartz: " + QuartzSchedulerService.instance().stateText());
 lblRedis.setText("Redis: " + (AppConfig.INSTANCE.redisEnabled ? "已启用" : "本地模式"));
 int nodes = RedisSupport.instance().listNodes().size();
 lblNodes.setText("集群节点: " + nodes);
 lblTasks.setText("任务总数: " + controller.taskService().count());
 lblTime.setText("时间: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
private void shutdown() {
try {
QuartzSchedulerService.instance().shutdown();
} catch (Exception ignored) {
}
RedisSupport.instance().shutdown();
AppConfig.AppLog.info("应用退出");
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author    : geovindu,Geovin Du 涂聚文.
IDE       : IntelliJ IDEA 2024.3.6 Java 21
database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS        : window10
Datetime  : 2026 - 2026/9/5 - 21:08
User      : geovindu
Product   : IntelliJ IDEA
Project   : Taskscheduler
File      : TaskEditDialog.java
explain   : 学习  类
**/
package com.jewelry.taskscheduler.ui;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.infrastructure.scheduler.CronCompat;
import org.quartz.CronExpression;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.text.SimpleDateFormat;
/**
任务编辑对话框(MVC View):新增/编辑任务,带 cron 校验与下次执行时间预览
*/
public class TaskEditDialog extends JDialog {
private final Task task;
private final java.util.function.Consumer<Task> onSave;
private final JTextField txtName = new JTextField(24);
private final JTextField txtCron = new JTextField(24);
private final JTextField txtCommand = new JTextField(24);
private final JTextField txtTimeout = new JTextField("30", 6);
private final JTextField txtMaxRetry = new JTextField("0", 6);
private final JTextField txtRetryGap = new JTextField("5", 6);
private final JTextArea txtRemark = new JTextArea(2, 22);
private final JCheckBox chkEnabled = new JCheckBox("启用", true);
private final JLabel lblNext = new JLabel("");
public TaskEditDialog(Window owner, Task task, java.util.function.Consumer<Task> onSave) {
super(owner instanceof java.awt.Frame ? (java.awt.Frame) owner : null,
task == null || task.getId() <= 0 ? "新增任务" : "编辑任务", true);
this.task = task;
this.onSave = onSave;
setSize(560, 520);
setLocationRelativeTo(owner);
 buildForm();
 if (task != null &amp;&amp; task.getId() &gt; 0) {
     txtName.setText(task.getName());
     txtCron.setText(task.getCronExpr());
     txtCommand.setText(task.getCommand());
     txtTimeout.setText(String.valueOf(task.getTimeoutSec()));
     txtMaxRetry.setText(String.valueOf(task.getMaxRetry()));
     txtRetryGap.setText(String.valueOf(task.getRetryGapSec()));
     txtRemark.setText(task.getRemark());
     chkEnabled.setSelected(task.isEnabled());
 }
 previewCron();
}
private void buildForm() {
JPanel form = new JPanel(new GridBagLayout());
GridBagConstraints g = new GridBagConstraints();
g.insets = new Insets(6, 10, 6, 10);
g.anchor = GridBagConstraints.WEST;
 int row = 0;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("任务名称:*"), g);
 g.gridx = 1;
 form.add(txtName, g);

 row++;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("Cron表达式(5字段):*"), g);
 g.gridx = 1;
 form.add(txtCron, g);

 row++;
 g.gridx = 0; g.gridy = row;
 JButton btnPreview = new JButton("校验并预览下次执行");
 btnPreview.addActionListener(e -&gt; previewCron());
 form.add(btnPreview, g);
 g.gridx = 1;
 form.add(lblNext, g);

 row++;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("命令(cmd):*"), g);
 g.gridx = 1;
 form.add(txtCommand, g);

 row++;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("超时(秒):"), g);
 g.gridx = 1;
 form.add(txtTimeout, g);

 row++;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("失败重试次数:"), g);
 g.gridx = 1;
 form.add(txtMaxRetry, g);

 row++;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("重试间隔(秒):"), g);
 g.gridx = 1;
 form.add(txtRetryGap, g);

 row++;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("备注(含\"一次性\"=仅执行一次后自动禁用):"), g);
 g.gridx = 1;
 form.add(new JScrollPane(txtRemark), g);

 row++;
 g.gridx = 0; g.gridy = row;
 form.add(new JLabel("启用:"), g);
 g.gridx = 1;
 form.add(chkEnabled, g);

 JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
 JButton ok = new JButton("保存");
 JButton cancel = new JButton("取消");
 buttons.add(ok);
 buttons.add(cancel);

 ok.addActionListener(e -&gt; {
     try {
         if (txtName.getText().trim().isEmpty() || txtCron.getText().trim().isEmpty()
                 || txtCommand.getText().trim().isEmpty()) {
             throw new IllegalArgumentException("名称、Cron、命令均为必填");
         }
         Task t = task != null &amp;&amp; task.getId() &gt; 0 ? task : new Task();
         t.setName(txtName.getText().trim());
         t.setCronExpr(txtCron.getText().trim());
         t.setCommand(txtCommand.getText().trim());
         t.setTimeoutSec(Integer.parseInt(txtTimeout.getText().trim()));
         t.setMaxRetry(Integer.parseInt(txtMaxRetry.getText().trim()));
         t.setRetryGapSec(Integer.parseInt(txtRetryGap.getText().trim()));
         t.setRemark(txtRemark.getText());
         t.setEnabled(chkEnabled.isSelected());
         t.setStatus(com.jewelry.taskscheduler.domain.model.TaskStatus.IDLE);
         onSave.accept(t);
         dispose();
     } catch (Exception ex) {
         JOptionPane.showMessageDialog(this, "保存失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
     }
 });
 cancel.addActionListener(e -&gt; dispose());

 setLayout(new BorderLayout());
 add(form, BorderLayout.CENTER);
 add(buttons, BorderLayout.SOUTH);
}
/** cron 校验 + 显示下一次执行时间 */
private void previewCron() {
try {
String cron = txtCron.getText().trim();
if (cron.isEmpty()) {
lblNext.setText("请输入 cron 后再预览");
return;
}
CronCompat.validate(cron);
String q = CronCompat.toQuartz(cron);
CronExpression expr = new CronExpression(q);
java.util.Date next = expr.getNextValidTimeAfter(new java.util.Date());
lblNext.setText("有效,下次执行: " + (next == null ? "无" : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(next)));
lblNext.setForeground(new java.awt.Color(46, 125, 50));
} catch (Exception e) {
lblNext.setText("无效: " + e.getMessage());
lblNext.setForeground(new java.awt.Color(198, 40, 40));
}
}
}

调用:

java 复制代码
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Tasks cheduler  mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 21
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/9/5 - 21:08
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : Taskscheduler
 * File      : Application.java
 * explain   : 学习  类
 **/
package com.jewelry.taskscheduler;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
import com.jewelry.taskscheduler.infrastructure.redis.RedisSupport;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import com.jewelry.taskscheduler.ui.MainController;
import com.jewelry.taskscheduler.ui.MainWindow;
import javax.swing.SwingUtilities;
import java.util.List;
/**
Java 任务调度系统主入口
启动顺序:加载配置 → 初始化日志 → H2 初始化 → Redis 启动 → Quartz 启动 → 种子数据 → Swing 主窗口
JDK 21 + Swing + DDD + MVC + Quartz + Redis(可选) + EventBus + DAG
*/
public class Application {
public static void main(String[] args) {
// 1. 配置与日志
AppConfig.load("app.properties");
AppConfig.AppLog.init(AppConfig.INSTANCE.logDir, AppConfig.INSTANCE.logMaxSizeMb);
AppConfig.AppLog.info("========== Java 任务调度系统启动 ==========");
AppConfig.AppLog.info("运行环境: JDK " + System.getProperty("java.version") + " / OS " + System.getProperty("os.name"));
 // 2. H2 数据库
 Db.init();

 // 3. Redis(可选)
 RedisSupport.instance().start();

 // 4. Quartz 调度器
 QuartzSchedulerService.instance().start();

 // 5. 种子数据(首次运行空库时插入示例任务)
 seedIfEmpty();

 // 6. Swing 主窗口(EDT)
 SwingUtilities.invokeLater(() -&gt; {
     try {
         MainController controller = new MainController();
         MainWindow window = new MainWindow(controller);
         controller.setWindow(window);
         window.setVisible(true);
     } catch (Exception e) {
         AppConfig.AppLog.error("Swing 界面启动失败: " + e);
         e.printStackTrace();
     }
 });
}
/** 首次运行(任务表为空)时插入示例任务 */
private static void seedIfEmpty() {
TaskRepository repo = new TaskRepositoryJdbc();
if (repo.count() > 0) {
return;
}
AppConfig.AppLog.info("检测到空库,插入示例任务...");
 // 示例1:每分钟健康检查(cron 5字段:分 时 日 月 周)
 seed(repo, "每分钟健康检查", "* * * * *",
         "echo [%date% %time%] health check ok &gt;&gt; logs\\health.log",
         "定时健康检查示例");

 // 示例2:每天 8 点执行数据库备份
 seed(repo, "每日8点备份数据库", "0 8 * * *",
         "mkdir db_backup 2&gt;nul &amp; copy data\\task_scheduler.mv.db db_backup\\task_scheduler_%date:~0,4%%date:~5,2%%date:~8,2%.mv.db",
         "备份 H2 数据库文件");

 // 示例3:一次性任务(备注含"一次性",执行成功自动禁用)
 seed(repo, "一次性初始化数据", "0 0 * * *",
         "echo init done &gt;&gt; logs\\init.log",
         "一次性:执行成功后自动禁用");
}
private static void seed(TaskRepository repo, String name, String cron, String command, String remark) {
Task t = new Task();
t.setName(name);
t.setCronExpr(cron);
t.setCommand(command);
t.setTimeoutSec(60);
t.setMaxRetry(0);
t.setRetryGapSec(5);
t.setEnabled(true);
t.setRemark(remark);
repo.save(t);
try {
QuartzSchedulerService.instance().register(t);
} catch (Exception e) {
AppConfig.AppLog.warn("种子任务注册失败: " + name + ": " + e.getMessage());
}
}
}

输出:

相关推荐
神奇小梵1 小时前
java的特殊语法点,与常规语言相异的点
java·开发语言
代码不停1 小时前
Spring Boot 配置文件
spring boot·后端
无巧不成书02181 小时前
Tomcat 下载后使用全指南:解压、环境配置、启动验证与常见问题排查
java·tomcat
bnmoel1 小时前
C++ 基础入门篇(二):引用
开发语言·c++·引用·底层
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之多因子认证缓存服务
java·spring·缓存
wuminyu2 小时前
Kafka配置TLS/SSL加密传输时零拷贝失效分析
java·linux·c语言·jvm·c++
边境悍匪2 小时前
蜗牛学苑 Java 智能体学习 Day41|ElementPlus 布局、Vue‑Router 登录、Axios 思维导图复盘
java·vue.js·学习
影视飓风TIM2 小时前
C++ 智能指针:auto_ptr / unique_ptr / shared_ptr / weak_ptr 原理与使用
开发语言·c++
IT_陈寒2 小时前
为什么我的Java Stream操作总是默默吃掉异常?
前端·人工智能·后端