【智能排班系统】Quartz结合Cron-Utils自定义时间发送上班、休息提醒

文章目录

  • Quartz:强大的Java作业调度引擎
    • Quartz概述
    • 核心概念与架构
    • 配置文件
      • 主配置(配置主要调度器设置、事务)
      • 线程池配置(调整作业执行资源)
        • SimpleThreadPool特定属性
        • 自定义线程池
      • RAMJobStore配置(在内存中存储作业和触发器)
      • JDBC-JobStore
        • JDBC-JobStoreTX配置(通过JDBC在数据库中存储作业和触发器)
        • JDBC-JobStoreCMT配置(JDBC与JTA容器管理事务)
      • 数据源配置(供JDBC-JobStores使用)
        • 自定义org.quartz.utils.ConnectionProvider实现
      • 配置数据库集群(使用JDBC-JobStore实现故障转移和负载均衡)
  • 智能排班系统实现
    • 依赖
    • Quartz所需表
    • 数据库连接池
    • 配置文件`myQuartz.properties`
    • Quartz配置类
    • 自定义工作类
    • Quartz相关表增删改查
      • 实体类
      • Mapper
      • Dto
      • 作业管理Service
        • impl
    • 定时通知管理
      • 定时通知实体类
      • 定时通知Service
      • 定时通知实现类
      • Controller
    • 通知发送实现

Quartz:强大的Java作业调度引擎

在现代软件开发中,自动化任务调度是一项关键需求,它允许应用程序按照预定的时间规则或条件执行特定操作。在Java生态系统中,Quartz作为一款久经考验的开源作业调度框架,凭借其灵活性、可靠性与丰富的功能集,赢得了广泛的认可与应用。

Quartz概述

Quartz由OpenSymphony开源组织开发,是一个完全基于Java构建的作业调度系统。它专为满足各种规模和复杂度的应用程序需求而设计,无论是简单的定时任务,还是涉及成千上万作业的复杂调度场景,Quartz都能游刃有余地进行管理

核心概念与架构

Quartz围绕如下关键概念展开:

  1. Job(作业):作业是需要调度执行的实际业务逻辑的封装,通常表现为实现org.quartz.Job接口的Java类。execute(JobExecutionContext context)方法是作业执行的入口点,开发者在此编写具体的任务处理代码。

  2. JobDetail(作业详细信息):JobDetail对象详细描述了一个Job实例,包括作业的唯一标识、所属的Job类以及任何与作业关联的静态数据(JobDataMap,可以使用该字典来存储一些信息供Job使用)。

  3. Trigger(触发器)触发器定义了何时以及如何触发作业执行。常见的触发器类型包括:

    • SimpleTrigger:用于基于固定延迟或重复次数的简单调度。
    • CronTrigger基于cron表达式实现复杂的周期性调度,如每天特定时间、每周特定日期等。
  4. Quartz调度器(Scheduler):作为整个系统的中枢,负责管理和协调作业与触发器的关系,根据触发器设定的时间规则触发作业执行,并提供了诸如持久化、集群支持、事务管理、错误处理和监听器等功能。

应用场景

Quartz的应用领域非常广泛,包括但不限于:

  • 定时任务:如定期清理系统日志、数据库备份、发送电子邮件报告等。
  • 业务流程自动化:在特定时间点触发业务流程中的某个步骤,如订单过期自动取消、会员资格到期提醒等。
  • 数据分析与报表生成:定时提取、处理数据并生成报表,如每日销售统计、网站流量分析等。
  • 系统监控与警报:定期检查系统状态,当检测到异常时触发警报或执行恢复操作。

企业级特性与扩展

Quartz具备诸多企业级特性,使其在大型分布式环境中表现出色:

  • 持久化与集群支持:通过与数据库集成(如JDBCJobStore),Quartz可以将作业和触发器的状态持久化,支持在集群环境中实现高可用性与负载均衡,确保即使在节点故障时也能保证任务的正确调度。
  • 事务管理:与JTA(Java Transaction API)集成,确保作业执行过程中的事务一致性。
  • 插件与定制:通过插件机制,Quartz可以轻松扩展功能,如日志记录、历史追踪、自定义锁机制等。用户还可以根据需要实现自定义的Job、Trigger或Delegate,以适应特定数据库或业务需求。

总之,Quartz作为一款强大且灵活的Java作业调度框架,以其易用性、健壮性与高度可定制性,成为了众多开发团队实现任务自动化、提升系统效率的首选工具。无论是在独立应用还是企业级分布式系统中,Quartz都能够有效地满足多样化的定时任务调度需求。

配置文件

该部分内容主要参考官网的说明文档:configuration.adoc

通过配置文件,Quartz可以实现非常灵活地自定义,通过使用配置文件,可以配置如下内容:

  • 主配置(配置主要调度器设置、事务)
  • 线程池配置(调整作业执行资源)
    • SimpleThreadPool特定属性
    • 自定义线程池
  • 监听器配置(您的应用程序可以接收计划事件的通知)
  • 插件配置(向调度器添加功能)
    • 日志触发历史记录插件示例配置
    • XML调度数据处理器插件示例配置
    • 关闭钩子插件示例配置
  • RMI服务器与客户端配置(从远程进程使用Quartz实例)
  • RAMJobStore配置(在内存中存储作业和触发器)
  • JDBC-JobStoreTX配置(通过JDBC在数据库中存储作业和触发器)
    • 自定义StdRowLockSemaphore
  • JDBC-JobStoreCMT配置(JDBC与JTA容器管理事务)
  • 数据源配置(供JDBC-JobStores使用)
    • 由Quartz创建的数据源定义具有以下属性:
    • 引用应用服务器数据源定义具有以下属性:
    • 自定义ConnectionProvider实现
  • 数据库集群配置(通过JDBC-JobStore实现故障转移和负载均衡)

通常通过使用属性文件,结合使用StdSchedulerFactory(该工厂消费配置文件并实例化调度器)来配置Quartz。默认情况下,StdSchedulerFactory会加载名为quartz.properties的属性文件,位置为“当前工作目录”。如果加载失败,则加载位于org/quartz包中的quartz.properties文件。如果您希望使用非默认文件,必须将系统属性org.quartz.properties设置为您希望使用的文件的路径。或者,您可以在调用StdSchedulerFactory.getScheduler()之前显式初始化工厂,方法是调用initialize(xx)方法之一。

指定的JobStore、ThreadPool和其他SPI类的实例将按名称创建,然后配置文件中为它们指定的所有附加属性将通过调用等效的set方法设置到实例上(将配置文件里面的属性加载到Bean中)。例如,如果属性文件包含属性org.quartz.jobStore.myProp = 10,则在JobStore类实例化后,将调用其上的setMyProp()方法。在调用属性的setter方法前,会进行到基本Java类型的类型转换(int、long、float、double、boolean和String)。

一个属性可以通过使用$@other.property.name的形式引用另一个属性的值,例如,要引用调度器的实例名作为某个其他属性的值,您可以使用$@org.quartz.scheduler.instanceName

本文只对排班系统使用到的配置进行展开,其他配置建议查看官网的配置文件介绍

主配置(配置主要调度器设置、事务)

如下属性用于配置调度器的身份识别以及其他一些“顶层”设置。
在这里插入图片描述
org.quartz.scheduler.instanceName(实例名):该值对调度器本身无实际意义,而是类似于客户端代码,在同一个程序中区分多个调度器实例。如果需要使用集群功能,集群中每个逻辑上相同的调度器实例都必须使用相同的名称。

org.quartz.scheduler.instanceId(实例ID):值可以是任何字符串,在集群中所有调度器ID必须唯一。

  • 将该值设为"AUTO"可以自动生成实例ID。
  • 如果希望该值来自系统属性"org.quartz.scheduler.instanceId",可将其设为"SYS_PROP"。

org.quartz.scheduler.instanceIdGenerator.class(实例ID生成器)仅在instanceId设置为"AUTO"时使用,包括如下实现方式:

  • 值设置为org.quartz.simpl.SimpleInstanceIdGenerator(默认值),根据主机名和时间戳生成实例ID
  • 值设置为SystemPropertyInstanceIdGenerator从系统属性"org.quartz.scheduler.instanceId"获取实例ID
  • 值设置为HostnameInstanceIdGenerator,使用本地主机名InetAddress.getLocalHost().getHostName()
  • 自行实现InstanceIdGenerator接口并指定路径实现类路径

org.quartz.scheduler.threadName(线程名称):可以是任何作为Java线程有效名称的字符串。如果不指定此属性,线程将接收调度器的名称(即"org.quartz.scheduler.instanceName")并追加字符串’_QuartzSchedulerThread’。

org.quartz.scheduler.makeSchedulerThreadDaemon:值为true或false。指定调度器主线程是否应为守护线程。如果您使用的是SimpleThreadPool(很可能是这种情况),请参阅org.quartz.scheduler.makeSchedulerThreadDaemon属性以进行相应调整。

org.quartz.scheduler.threadsInheritContextClassLoaderOfInitializer:值为true或false。指定Quartz启动的线程是否应继承初始化线程(即初始化Quartz实例的线程)的上下文类加载器。这将影响Quartz主调度线程、JDBCJobStore的错过触发处理线程(如果使用JDBCJobStore)、集群恢复线程(如果使用集群)以及SimpleThreadPool中的线程(如果使用SimpleThreadPool)。将此值设为true可能有助于类加载、JNDI查找以及在应用服务器内部使用Quartz的相关问题

org.quartz.scheduler.idleWaitTime当调度器空闲时,重新查询可用触发器之前等待的时间(毫秒)。通常无需调整此参数,除非您使用XA事务且遇到立即触发的触发器延迟触发的问题。建议不设置小于5000 ms的值,因为会导致过多的数据库查询。小于1000的值是非法的

org.quartz.scheduler.dbFailureRetryInterval当检测到JobStore(如与数据库的连接)内失去连接时,调度器重试之间等待的时间(毫秒)。显然,当使用RamJobStore时,此参数并无太大意义。

org.quartz.scheduler.classLoadHelper.class:默认使用"org.quartz.simpl.CascadingClassLoadHelper"类——该类依次使用所有其他ClassLoadHelper类,直到找到一个能正常工作的(健壮性比较高)。一般不需要为此属性指定任何其他类,尽管在应用服务器内部似乎会发生奇怪的事情。当前所有可能的ClassLoadHelper实现都可以在org.quartz.simpl包中找到。

org.quartz.scheduler.jobFactory.class:JobFactory负责生产JobClasses的实例。默认为’org.quartz.simpl.PropertySettingJobFactory’,它每次即将执行时都会简单地调用newInstance()方法在类上生成一个新的实例。PropertySettingJobFactory还反射性地使用SchedulerContext和Job及Trigger JobDataMaps的内容设置作业的bean属性。

org.quartz.context.key.SOME_KEY:表示一对键值对,将作为字符串放入“调度器上下文”中(见Scheduler.getContext())。例如,设置"org.quartz.context.key.MyKey = MyValue"相当于执行scheduler.getContext().put(“MyKey”, “MyValue”)

注意:除非您使用JTA事务,否则应将与事务相关的属性从配置文件中移除。

org.quartz.scheduler.userTransactionURL:应设置为Quartz可以定位应用服务器的UserTransaction管理器的JNDI URL。默认值为"java:comp/UserTransaction"——几乎适用于所有应用服务器。Websphere用户可能需要将此属性设置为"jta/usertransaction"。只有在配置Quartz使用JobStoreCMT且org.quartz.scheduler.wrapJobExecutionInUserTransaction设为true时才会使用此属性。

org.quartz.scheduler.wrapJobExecutionInUserTransaction

  • 将值设为true,则Quartz在调用作业的execute方法之前开始一个UserTransaction。Tx将在作业的execute方法完成且JobDataMap更新后(如果是StatefulJob)提交。
  • 将值设为false(默认值),如果在作业类上使用@ExecuteInJTATransaction注解,该注解允许您控制单个作业是否应由Quartz启动JTA事务——而此属性会导致所有作业都发生这种情况。

org.quartz.scheduler.skipUpdateCheck:是否跳过运行快速网页请求以确定是否有可供下载的Quartz更新版本。如果执行检查且发现有更新,会在Quartz的日志中报告更新可用。可以通过设置属性"org.terracotta.quartz.skipUpdateCheck=true"(可在系统环境或java命令行的-D选项中设置)来禁用更新检查,建议在生产部署中禁用更新检查。

org.quartz.scheduler.batchTriggerAcquisitionMaxCount:调度器节点一次允许获取(以便触发)的最大触发器数量。默认值为1。数值越大,在需要同时触发大量触发器的情况下效率越高,代价是可能导致集群节点之间的负载不平衡。如果此属性的值设为大于1且使用JDBC JobStore,则必须将属性"org.quartz.jobStore.acquireTriggersWithinLock"设为"true"以避免数据损坏。

org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow允许触发器被获取并提前触发其计划触发时间的窗口时间(毫秒)。默认为0。数值越大,批量获取待触发的触发器就越有可能一次性选择并触发多个触发器——但代价是触发器调度可能无法精确遵守(触发器可能会提前这个量的时间触发)。在调度器有大量触发器需要在同一时间或接近同一时间触发的情况下,出于性能考虑,这可能很有用。

线程池配置(调整作业执行资源)

在这里插入图片描述
org.quartz.threadPool.class:希望使用的ThreadPool实现的名称。随Quartz一起提供的线程池为"org.quartz.simpl.SimpleThreadPool",能满足几乎所有用户的需求。它具有非常简单的行为,且经过了充分测试。它提供一个固定大小的线程池,这些线程在整个Scheduler生命周期内“存活”。

org.quartz.threadPool.threadCount可用于并发执行作业的线程数量,可以是任何正整数,只有1到100之间的数字才非常实用。

  • 如果只有少数几个作业每天触发几次,那么1个线程就足够了!
  • 如果您有成千上万个作业,其中许多每分钟触发一次,那么可能需要50或100(甚至更多)这样的线程数(取决于作业所执行的工作性质以及系统资源)!

org.quartz.threadPool.threadPriority:可以是Thread.MIN_PRIORITY(即1)和Thread.MAX_PRIORITY(即10)之间的任何int值。默认值为Thread.NORM_PRIORITY(即5)

SimpleThreadPool特定属性

在这里插入图片描述
org.quartz.threadPool.makeThreadsDaemons:可以设为"true",使线程池中的线程创建为守护线程。默认为"false"。请参阅ConfigMain的org.quartz.scheduler.makeSchedulerThreadDaemon属性。

org.quartz.threadPool.threadsInheritGroupOfInitializingThread:以设为"true"或"false",默认为true。

org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread:可以设为"true"或"false",默认为false。

org.quartz.threadPool.threadNamePrefix:工作线程池中线程名称的前缀——后面会追加一个数字。

自定义线程池

org.quartz.threadPool.class指向自己的线程池
org.quartz.threadPool.somePropOfFooThreadPool 用来设置线程池的一些属性

org.quartz.threadPool.class = com.mycompany.goo.FooThreadPool
org.quartz.threadPool.somePropOfFooThreadPool = someValue

RAMJobStore配置(在内存中存储作业和触发器)

RAMJobStore用于在内存中存储调度信息(作业、触发器和日历)。RAMJobStore速度快、轻量级,但当进程终止时所有调度信息会丢失

通过将org.quartz.jobStore.class属性设置如下,可以将调度器的JobStore设为RAMJobStore

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

RAMJobStore还可以通过以下属性进行调整:
在这里插入图片描述

org.quartz.jobStore.misfireThreshold:调度器在认为触发器“错过触发”之前,允许其超过下一次触发时间的毫秒数。如果不在此配置中为此属性添加条目,其默认值为60000(60秒)。

JDBC-JobStore

JDBCJobStore用于在关系型数据库中存储调度信息(作业、触发器和日历)。实际上可以根据所需的事务行为在两个独立的JDBCJobStore类之间进行选择。

JDBC-JobStoreTX配置(通过JDBC在数据库中存储作业和触发器)

JobStoreTX通过在每次操作(如添加作业)后对数据库连接调用commit(或rollback)来自行管理所有事务。如果您在独立应用程序中使用Quartz,或者在不使用JTA事务的应用程序中在servlet容器内使用Quartz,那么JDBCJobStore是合适的。

通过将org.quartz.jobStore.class属性设置如下,可以将调度器的JobStore设为JobStoreTX:

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

JobStoreTX还可以进一步设置其他属性

在这里插入图片描述
org.quartz.jobStore.dataSource:此属性的值必须是配置属性文件中定义的一个DataSource的名称。

org.quartz.jobStore.tablePrefix:在数据库中创建的Quartz表所使用的前缀。如果使用不同的表前缀,可以在同一数据库中拥有多个Quartz表集。

org.quartz.jobStore.useProperties:“使用属性”标志指示JDBCJobStore,JobDataMap中的所有值都是字符串,因此可以作为名称-值对存储,而不是将更复杂的对象以序列化形式存储在BLOB列中。这样很方便,因为可以避免将非字符串类序列化到BLOB中时可能出现的类版本问题。

org.quartz.jobStore.misfireThreshold:同上。

org.quartz.jobStore.isClustered:设置为"true"以启用群集功能。如果您有多个Quartz实例使用同一套数据库表,必须将此属性设为"true"……否则将会造成混乱。

org.quartz.jobStore.clusterCheckinInterval:设置此实例与其他群集实例“签入”* 的频率(以毫秒为单位)。影响检测失败实例的速度。

org.quartz.jobStore.maxMisfiresToHandleAtATime:jobstore在给定遍历中处理的错过触发次数的最大值。一次性处理太多(超过几十个)可能会导致数据库表被锁定足够长的时间,以至于尚未错过触发的其他触发器的触发性能受到阻碍。

org.quartz.jobStore.dontSetAutoCommitFalse:将此参数设置为"true"告诉Quartz不要对从DataSource(s)获取的连接调用setAutoCommit(false)。在某些情况下,这可能有所帮助,例如,如果驱动程序在已经关闭时被调用会抱怨。此属性默认为false,因为大多数驱动程序要求调用setAutoCommit(false)。

org.quartz.jobStore.selectWithLockSQL:必须是一个SQL字符串,用于在“LOCKS”表中选择一行并对其加锁。如果不设置,默认值为"SELECT * FROM {0}LOCKS WHERE SCHED_NAME = {1} AND LOCK_NAME = ? FOR UPDATE",对大多数数据库有效。"{0}“在运行时将替换为您上面配置的TABLE_PREFIX。”{1}"将替换为调度器的名称。

org.quartz.jobStore.txIsolationLevelSerializable:将此值设为"true"告诉Quartz(使用JobStoreTX或CMT时)对JDBC连接调用setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE)。这在高负载下防止某些数据库出现锁超时,以及“长时间”事务时可能有所帮助。

org.quartz.jobStore.acquireTriggersWithinLock:是否应在显式数据库锁内获取待触发的下一个触发器。这对以前版本的Quartz来说曾经是必要的(对于特定数据库,以避免死锁),但现在不再被认为是必要的,因此默认值为"false"。如果“org.quartz.scheduler.batchTriggerAcquisitionMaxCount”设置为> 1,并且使用JDBC JobStore,则必须将此属性设置为“true”,以避免数据损坏(自Quartz 2.1.1起,如果batchTriggerAcquisitionMaxCount设置为> 1,“true”现在是默认值)。

org.quartz.jobStore.lockHandler.class:用于生产org.quartz.impl.jdbcjobstore.Semaphore实例的类名,该实例将用于对job store数据进行锁定控制。这是一个高级配置特性,大多数用户不应使用。默认情况下,Quartz会选择使用最合适的(预打包)Semaphore实现。

JDBC-JobStoreCMT配置(JDBC与JTA容器管理事务)

JobStoreCMT依赖于使用Quartz的应用程序来管理事务。在尝试安排(或取消安排)作业/触发器之前,必须有一个JTA事务正在进行中。这使得调度的“工作”成为应用程序“更大”事务的一部分。JobStoreCMT实际上需要使用两个数据源——一个由应用服务器(通过JTA)管理其连接的事务,另一个数据源的连接不参与全局(JTA)事务。当应用程序使用JTA事务(如通过EJB会话Bean)来执行其工作时,JobStoreCMT是合适的。

通过将org.quartz.jobStore.class属性设置如下,可以选择JobStoreCMT:

将调度器的JobStore设为JobStoreCMT

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT 

JobStoreCMT可通过以下属性进行调整:
在这里插入图片描述
org.quartz.jobStore.driverDelegateClass:驱动程序委托可以理解为各种数据库系统的特定“方言”。可能的选择包括:

  • org.quartz.impl.jdbcjobstore.StdJDBCDelegate(适用于完全符合JDBC的驱动程序)

  • org.quartz.impl.jdbcjobstore.MSSQLDelegate(适用于Microsoft SQL Server和Sybase)

  • org.quartz.impl.jdbcjobstore.PostgreSQLDelegate

  • org.quartz.impl.jdbcjobstore.WebLogicDelegate(适用于WebLogic驱动程序)

  • org.quartz.impl.jdbcjobstore.oracle.OracleDelegate

  • org.quartz.impl.jdbcjobstore.oracle.WebLogicOracleDelegate(适用于WebLogic中使用的Oracle驱动程序)

  • org.quartz.impl.jdbcjobstore.oracle.weblogic.WebLogicOracleDelegate(适用于WebLogic中使用的Oracle驱动程序)

  • org.quartz.impl.jdbcjobstore.CloudscapeDelegate

  • org.quartz.impl.jdbcjobstore.DB2v6Delegate

  • org.quartz.impl.jdbcjobstore.DB2v7Delegate

  • org.quartz.impl.jdbcjobstore.DB2v8Delegate

  • org.quartz.impl.jdbcjobstore.HSQLDBDelegate

  • org.quartz.impl.jdbcjobstore.PointbaseDelegate

  • org.quartz.impl.jdbcjobstore.SybaseDelegate

请注意,许多数据库已知可与StdJDBCDelegate配合使用,而其他数据库则已知可与针对其他数据库的委托类配合使用,例如Derby与Cloudscape委托类配合良好。

org.quartz.jobStore.dataSource:此属性的值必须是配置属性文件中定义的一个DataSource的名称。对于JobStoreCMT,要求这个DataSource包含能够参与JTA(例如容器管理)事务的连接。通常这意味着DataSource将在应用服务器内部和由应用服务器维护,并且Quartz将通过JNDI获取对其的引用。有关更多信息,请参阅关于DataSources的ConfigDataSources配置文档。

org.quartz.jobStore.nonManagedTXDataSource:JobStoreCMT需要的另一个数据源,其中包含不会成为容器管理事务一部分的连接。此属性的值必须是配置属性文件中定义的一个DataSource的名称。这个数据源必须包含非CMT连接,换句话说,就是对于Quartz可以直接在其上调用commit()和rollback()的连接。

org.quartz.jobStore.tablePrefix:同上

org.quartz.jobStore.useProperties:同上

org.quartz.jobStore.misfireThreshold:同上

org.quartz.jobStore.isClustered:同上

org.quartz.jobStore.clusterCheckinInterval:同上

org.quartz.jobStore.maxMisfiresToHandleAtATime:同上

org.quartz.jobStore.dontSetAutoCommitFalse:同上

org.quartz.jobStore.dontSetNonManagedTXConnectionAutoCommitFalse:与org.quartz.jobStore.dontSetAutoCommitFalse属性相同,只不过它应用于nonManagedTXDataSource。

org.quartz.jobStore.selectWithLockSQL:同上

org.quartz.jobStore.txIsolationLevelSerializable:同上

org.quartz.jobStore.txIsolationLevelReadCommitted:当设置为"true"时,此属性告诉Quartz对非托管JDBC连接调用setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED)。这在高负载下防止某些数据库(如DB2)出现锁超时,以及“长时间”事务时可能有所帮助。

org.quartz.jobStore.acquireTriggersWithinLock:同上

org.quartz.jobStore.lockHandler.class:同上

org.quartz.jobStore.driverDelegateInitString:这是一个管道分隔的属性(及其值)列表,可以在初始化期间传递给DriverDelegate

字符串的格式如下:

settingName=settingValue|otherSettingName=otherSettingValue|...

StdJDBCDelegate及其所有后代(随Quartz一起提供的所有委托)支持一个名为’triggerPersistenceDelegateClasses’的属性,该属性可以设置为实现TriggerPersistenceDelegate接口以存储自定义触发器类型的类的逗号分隔列表。请参阅Java类SimplePropertiesTriggerPersistenceDelegateSupport和SimplePropertiesTriggerPersistenceDelegateSupport,了解为自定义触发器编写持久化委托的示例。

数据源配置(供JDBC-JobStores使用)

如果需要使用JDBC-Jobstore,必须提供一个供其使用的DataSource(如果使用JobStoreCMT,则需要两个DataSource)。

DataSources可以通过以下三种方式配置:

  • 在quartz.properties文件中指定所有池属性,以便Quartz可以自己创建DataSource

  • 可以指定应用服务器管理的DataSource的JNDI位置,以便Quartz可以使用它

  • 自定义org.quartz.utils.ConnectionProvider实现

建议将DataSource最大连接大小配置为至少等于线程池中的工作线程数量加上三个。如果您的应用程序还频繁调用调度器API,您可能需要额外的连接。如果您使用JobStoreCMT,那么“非托管”数据源的最大连接大小应至少为四个。

您定义的每个DataSource(通常是1个或2个)都必须赋予一个名称,并且为每个DataSource定义的属性必须包含该名称,如下所示。DataSource的“NAME”可以是您想要的任何内容,除了在将其分配给JDBCJobStore时能够识别它之外,没有其他意义。

由Quartz创建的DataSources通过以下属性定义:
在这里插入图片描述
org.quartz.dataSource.NAME.driver:数据库的JDBC驱动程序的Java类名

org.quartz.dataSource.NAME.URL:连接数据库的连接URL(主机、端口等)

org.quartz.dataSource.NAME.user:连接到数据库时的用户名

org.quartz.dataSource.NAME.password:连接到数据库时的密码

org.quartz.dataSource.NAME.maxConnections:DataSource在其连接池中可以创建的最大连接数

org.quartz.dataSource.NAME.validationQuery:是DataSource可以用来检测和替换失败/损坏连接的可选SQL查询字符串。例如,Oracle用户可能会选择"select table_name from user_tables"——这是一个除非连接实际出现问题,否则永远不会失败的查询。

org.quartz.dataSource.NAME.idleConnectionValidationSeconds:对空闲连接进行测试之间的秒数——只有在设置了验证查询属性时才启用。默认值为50秒。

org.quartz.dataSource.NAME.validateOnCheckout:是否应每次从池中检索连接时执行数据库SQL查询以确保其仍然有效。如果为false,则会在检查入库时进行验证。默认值为false。

org.quartz.dataSource.NAME.discardIdleConnectionsSeconds:空闲连接闲置指定秒数后将其丢弃。0表示禁用此功能,默认值为0。

由Quartz定义的DataSource示例

org.quartz.dataSource.myDS.driver = oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@10.0.1.23:1521:demodb
org.quartz.dataSource.myDS.user = myUser
org.quartz.dataSource.myDS.password = myPassword
org.quartz.dataSource.myDS.maxConnections = 30
自定义org.quartz.utils.ConnectionProvider实现

在这里插入图片描述
org.quartz.dataSource.NAME.connectionProvider.class用来设置要使用的ConnectionProvider类的类名。在实例化此类后,Quartz可以自动以bean风格在实例上设置配置属性。

使用自定义ConnectionProvider实现的示例

org.quartz.dataSource.myCustomDS.connectionProvider.class = com.foo.FooConnectionProvider
org.quartz.dataSource.myCustomDS.someStringProperty = someValue
org.quartz.dataSource.myCustomDS.someIntProperty = 5

配置数据库集群(使用JDBC-JobStore实现故障转移和负载均衡)

Quartz的集群功能通过故障转移和负载均衡功能为您的调度器带来高可用性和可扩展性。

quartz_cluster 当前集群功能仅适用于JDBC-Jobstore(JobStoreTX或JobStoreCMT),本质上是通过让集群中的每个节点共享同一数据库来实现的

  • 负载均衡负载均衡会自动发生,集群中的每个节点尽可能快地触发作业。当触发器的触发时间到来时,第一个通过对其加锁获取它的节点将是触发它的节点对于每次触发,只有一个节点会执行作业。我的意思是,如果作业有一个重复触发器,告诉它每10秒触发一次,那么在12:00:00时恰好有一个节点会运行作业,在12:00:10时恰好有一个节点会运行作业,依此类推。不一定是每次都是同一个节点运行——哪个节点运行它或多或少是随机的。对于繁忙的调度器(有很多触发器),负载均衡机制几乎是随机的,但对于非繁忙的调度器(例如很少触发器),它倾向于使用同一个节点。

  • 故障迁移当集群中的一个节点在执行一个或多个作业时发生故障时,会发生故障转移。**当一个节点发生故障时,其他节点会检测到这种情况,并识别出在故障节点中处于进行中的数据库中的作业。任何标记为可恢复(在JobDetail上的"requests recovery"属性)的作业将由剩余节点重新执行。**未标记为可恢复的作业将简单地在下次相关触发器触发时释放以供执行。

集群功能最适合于扩展长期运行和/或CPU密集型作业(在多个节点上分布工作负载)。如果您需要扩展以支持数千个短运行(例如1秒)作业,可以考虑通过使用多个独立调度器(包括多个集群调度器以实现高可用性)对作业集合进行分区。调度器使用的是集群范围的锁,这种模式随着添加更多节点而降低性能(当超过大约三个节点时——取决于您的数据库的能力等)。

通过将"org.quartz.jobStore.isClustered"属性设置为"true"来启用集群。集群中的每个实例应使用quartz.properties文件的同一副本。例外情况是使用相同的属性文件,但允许有以下例外:线程池大小不同,instanceId不同(集群中的每个节点必须具有唯一的instanceId)

注意:除非使用某种形式的时间同步服务,如定期(时钟必须相差不超过1秒)同步它们的时钟,否则决不在不同机器上运行集群。如果您不了解如何做到这一点,请参阅https://www.nist.gov/pml/time-and-frequency-division/services/internet-time-service-its。

注意:绝不要对任何其他实例(已start())正在针对的同一组数据库表启动(scheduler.start())非集群实例。否则可能会遇到严重数据损坏,并且肯定会遇到不稳定的行为。

集群调度器的示例属性

#============================================================================
# Configure Main Scheduler Properties
#============================================================================

org.quartz.scheduler.instanceName = MyClusteredScheduler
org.quartz.scheduler.instanceId = AUTO

#============================================================================
# Configure ThreadPool
#============================================================================

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 25
org.quartz.threadPool.threadPriority = 5

#============================================================================
# Configure JobStore
#============================================================================

org.quartz.jobStore.misfireThreshold = 60000

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.useProperties = <span class="code-keyword">false</span>
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_

org.quartz.jobStore.isClustered = <span class="code-keyword">true</span>
org.quartz.jobStore.clusterCheckinInterval = 20000

#============================================================================
# Configure Datasources
#============================================================================

org.quartz.dataSource.myDS.driver = oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@polarbear:1521:dev
org.quartz.dataSource.myDS.user = quartz
org.quartz.dataSource.myDS.password = quartz
org.quartz.dataSource.myDS.maxConnections = 5
org.quartz.dataSource.myDS.validationQuery=select 0 from dual

智能排班系统实现

依赖

需要引入两个工具,Quartz用来做定时任务,Cron-Utils用来生成cron表达式

<!--定时任务-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--cron表达式生成器-->
<dependency>
    <groupId>com.cronutils</groupId>
    <artifactId>cron-utils</artifactId>
    <version>${cronUtils.version}</version>
</dependency>

Quartz所需表

使用Quartz需要使用数据库来存储一些作业、触发器信息,因此需要导入一些默认的表

DROP TABLE IF EXISTS `qrtz_blob_triggers`;
CREATE TABLE `qrtz_blob_triggers` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `TRIGGER_NAME` varchar(200)  NOT NULL,
  `TRIGGER_GROUP` varchar(200)  NOT NULL,
  `BLOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE,
  CONSTRAINT `QRTZ_BLOB_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_calendars`;
CREATE TABLE `qrtz_calendars` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `CALENDAR_NAME` varchar(200)  NOT NULL,
  `CALENDAR` blob NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`CALENDAR_NAME`) USING BTREE
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_cron_triggers`;
CREATE TABLE `qrtz_cron_triggers` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `TRIGGER_NAME` varchar(200)  NOT NULL,
  `TRIGGER_GROUP` varchar(200)  NOT NULL,
  `CRON_EXPRESSION` varchar(200)  NOT NULL,
  `TIME_ZONE_ID` varchar(80)  DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE,
  CONSTRAINT `QRTZ_CRON_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_fired_triggers`;
CREATE TABLE `qrtz_fired_triggers` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `ENTRY_ID` varchar(95)  NOT NULL,
  `TRIGGER_NAME` varchar(200)  NOT NULL,
  `TRIGGER_GROUP` varchar(200)  NOT NULL,
  `INSTANCE_NAME` varchar(200)  NOT NULL,
  `FIRED_TIME` bigint NOT NULL,
  `SCHED_TIME` bigint NOT NULL,
  `PRIORITY` int NOT NULL,
  `STATE` varchar(16)  NOT NULL,
  `JOB_NAME` varchar(200)  DEFAULT NULL,
  `JOB_GROUP` varchar(200)  DEFAULT NULL,
  `IS_NONCONCURRENT` varchar(1)  DEFAULT NULL,
  `REQUESTS_RECOVERY` varchar(1)  DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`) USING BTREE
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_job_details`;
CREATE TABLE `qrtz_job_details` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `JOB_NAME` varchar(200)  NOT NULL,
  `JOB_GROUP` varchar(200)  NOT NULL,
  `DESCRIPTION` varchar(250)  DEFAULT NULL,
  `JOB_CLASS_NAME` varchar(250)  NOT NULL,
  `IS_DURABLE` varchar(1)  NOT NULL,
  `IS_NONCONCURRENT` varchar(1)  NOT NULL,
  `IS_UPDATE_DATA` varchar(1)  NOT NULL,
  `REQUESTS_RECOVERY` varchar(1)  NOT NULL,
  `JOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`) USING BTREE
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_locks`;
CREATE TABLE `qrtz_locks` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `LOCK_NAME` varchar(40)  NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`) USING BTREE
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_paused_trigger_grps`;
CREATE TABLE `qrtz_paused_trigger_grps` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `TRIGGER_GROUP` varchar(200)  NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`) USING BTREE
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_scheduler_state`;
CREATE TABLE `qrtz_scheduler_state` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `INSTANCE_NAME` varchar(200)  NOT NULL,
  `LAST_CHECKIN_TIME` bigint NOT NULL,
  `CHECKIN_INTERVAL` bigint NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`) USING BTREE
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_simple_triggers`;
CREATE TABLE `qrtz_simple_triggers` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `TRIGGER_NAME` varchar(200)  NOT NULL,
  `TRIGGER_GROUP` varchar(200)  NOT NULL,
  `REPEAT_COUNT` bigint NOT NULL,
  `REPEAT_INTERVAL` bigint NOT NULL,
  `TIMES_TRIGGERED` bigint NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE,
  CONSTRAINT `QRTZ_SIMPLE_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_simprop_triggers`;
CREATE TABLE `qrtz_simprop_triggers` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `TRIGGER_NAME` varchar(200)  NOT NULL,
  `TRIGGER_GROUP` varchar(200)  NOT NULL,
  `STR_PROP_1` varchar(512)  DEFAULT NULL,
  `STR_PROP_2` varchar(512)  DEFAULT NULL,
  `STR_PROP_3` varchar(512)  DEFAULT NULL,
  `INT_PROP_1` int DEFAULT NULL,
  `INT_PROP_2` int DEFAULT NULL,
  `LONG_PROP_1` bigint DEFAULT NULL,
  `LONG_PROP_2` bigint DEFAULT NULL,
  `DEC_PROP_1` decimal(13,4) DEFAULT NULL,
  `DEC_PROP_2` decimal(13,4) DEFAULT NULL,
  `BOOL_PROP_1` varchar(1)  DEFAULT NULL,
  `BOOL_PROP_2` varchar(1)  DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE,
  CONSTRAINT `QRTZ_SIMPROP_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB ;

DROP TABLE IF EXISTS `qrtz_triggers`;
CREATE TABLE `qrtz_triggers` (
  `SCHED_NAME` varchar(120)  NOT NULL,
  `TRIGGER_NAME` varchar(200)  NOT NULL,
  `TRIGGER_GROUP` varchar(200)  NOT NULL,
  `JOB_NAME` varchar(200)  NOT NULL,
  `JOB_GROUP` varchar(200)  NOT NULL,
  `DESCRIPTION` varchar(250)  DEFAULT NULL,
  `NEXT_FIRE_TIME` bigint DEFAULT NULL,
  `PREV_FIRE_TIME` bigint DEFAULT NULL,
  `PRIORITY` int DEFAULT NULL,
  `TRIGGER_STATE` varchar(16)  NOT NULL,
  `TRIGGER_TYPE` varchar(8)  NOT NULL,
  `START_TIME` bigint NOT NULL,
  `END_TIME` bigint DEFAULT NULL,
  `CALENDAR_NAME` varchar(200)  DEFAULT NULL,
  `MISFIRE_INSTR` smallint DEFAULT NULL,
  `JOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE,
  KEY `SCHED_NAME` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`) USING BTREE,
  CONSTRAINT `QRTZ_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `qrtz_job_details` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB ;

数据库连接池

配置一个数据库连接池供Quartz配置文件使用

import com.zaxxer.hikari.HikariDataSource;
import lombok.Data;
import org.quartz.utils.ConnectionProvider;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * 使用Hikari作为数据库连接池
 */
@Data
public class HikariConnectionProvider implements ConnectionProvider {

    /**
     * JDBC驱动
     */
    public String driver;

    /**
     * JDBC连接字符串
     */
    public String URL;

    /**
     * 数据库用户名
     */
    public String user;

    /**
     * 数据库用户密码
     */
    public String password;

    /**
     * 数据库最大连接数
     */
    public int maxConnections;

    /**
     * SQL查询语句,用于验证数据库连接的有效性。如果设置,将在连接返回到连接池前执行该查询。
     */
    public String validationQuery;

    /**
     * 是否在获取连接时进行有效性验证(即在 {@link #getConnection()} 时执行验证查询)
     */
    private boolean validateOnCheckout;

    /**
     * 空闲连接的验证间隔(秒)。仅当 {@code validationQuery} 不为 null 时有效。
     */
    private int idleConnectionValidationSeconds;

    /**
     * 默认的最大连接数(仅用于内部计算,实际使用 {@link #maxConnections})。
     */
    public static final int DEFAULT_DB_MAX_CONNECTIONS = 10;

    /**
     * HikariCP 数据源实例。
     */
    private HikariDataSource datasource;


    /**
     * 获取数据库连接。调用此方法时,将从 HikariCP 连接池中获取一个已建立的连接。
     *
     * @return 与数据库建立的连接对象
     * @throws SQLException 如果无法从连接池获取连接或发生其他 SQL 错误
     */
    @Override
    public Connection getConnection() throws SQLException {
        return datasource.getConnection();
    }

    /**
     * 关闭连接池。调用此方法时,将关闭所有与数据库的连接,并释放相关资源。
     */
    @Override
    public void shutdown() {
        datasource.close();
    }

    /**
     * 初始化连接池。此方法负责创建并配置 HikariCP 数据源,设置必要的连接参数。
     *
     * @throws SQLException 如果在创建或配置数据源过程中发生错误
     */
    @Override
    public void initialize() throws SQLException {
        if (this.URL == null) {
            throw new SQLException("URL不能为空");
        }
        if (this.driver == null) {
            throw new SQLException("driver不能为空");
        }
        if (this.maxConnections < 0) {
            throw new SQLException("maxConnections必须大于0");
        }

        // 创建并配置 HikariDataSource
        datasource = new HikariDataSource();
        try {
            datasource.setDriverClassName(this.driver);
        } catch (Exception e) {
            e.printStackTrace();
        }
        datasource.setJdbcUrl(this.URL);
        datasource.setUsername(this.user);
        datasource.setPassword(this.password);
        datasource.setMaximumPoolSize(this.maxConnections);
        datasource.setMinimumIdle(1);
        // 最大连接等待时间(毫秒)
        datasource.setConnectionTimeout(0);
        // 默认最大连接生命周期,单位为毫秒
        datasource.setMaxLifetime(DEFAULT_DB_MAX_CONNECTIONS * 1000);
        // 配置验证查询
        if (this.validationQuery != null) {
            // 这个查询语句用于在必要时验证数据库连接是否仍然有效。例如,对于 MySQL,常用的验证查询可能是 "SELECT 1"
            datasource.setConnectionTestQuery(this.validationQuery);
            // 单位转换为毫秒,这个属性定义了执行验证查询时允许的最大等待时间。如果在这个时间内查询仍未完成,HikariCP 将认为连接无效并关闭该连接
            datasource.setValidationTimeout(this.idleConnectionValidationSeconds * 1000);
            // 可选,设置连接泄漏检测阈值,单位为毫秒
            // 该属性指定了 HikariCP 检测到连接可能泄漏所需的时间(即连接在使用后未正确关闭的时间)
            // 超过这个阈值后,HikariCP 将记录一条警告消息,帮助开发者识别和修复潜在的连接泄漏问题
            datasource.setLeakDetectionThreshold(this.idleConnectionValidationSeconds * 1000);
            if (!this.validateOnCheckout) {
                // 启用 JMX 监控,以便在外部工具中查看连接状态(包括“空闲”和“检查出错”的连接)
                datasource.setRegisterMbeans(true);
            }
        }

    }
}

配置文件myQuartz.properties

## 主配置
# 给实例自动生成实例ID
org.quartz.scheduler.instanceId=AUTO
# 实例名
org.quartz.scheduler.instanceName=MyClusteredScheduler

## 线程设置
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# 并发个数
org.quartz.threadPool.threadCount=10
# 优先级
org.quartz.threadPool.threadPriority=5

## 集群设置
# 设置为“true”以启用集群功能。如果有多实例Quartz使用同一组数据库表,此属性必须设置为“true”,否则将导致混乱。
org.quartz.jobStore.isClustered=true

### JobStore配置
# 调度器在认为触发器“错过触发”之前可以“容忍”其超过下一次触发时间的毫秒数。如果不在此配置中为该属性输入值,则默认值为60000(60秒)。
org.quartz.jobStore.misfireThreshold=6000
## 数据存储方式
# 方式一:存储在内存中,效率高,但进程终止时,所有调度信息都会丢失
# org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore
# 方式二:持久化存储到硬盘中,设置使用数据库方式
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
# useProperties设置为“true”,指示JDBCJobStore,JobDataMap中的所有值都是字符串,因此可以作为名称-值对存储,而不是将更复杂的对象以序列化形式存储在BLOB列中。
# 这很有用,因为可以避免因将非字符串类序列化到BLOB中而导致的类版本问题
org.quartz.jobStore.useProperties=true
# 表前缀 如果使用不同的表前缀,可以在同一数据库中拥有多组Quartz表
org.quartz.jobStore.tablePrefix=QRTZ_
# 数据源别名,自定义
org.quartz.jobStore.dataSource=QuartzDS

## 数据源配置
org.quartz.dataSource.QuartzDS.connectionProvider.class=com.dam.config.hikari.HikariConnectionProvider
org.quartz.dataSource.QuartzDS.URL=jdbc:mysql://127.0.0.1:3308/smart_scheduling_system?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
org.quartz.dataSource.QuartzDS.user=root
org.quartz.dataSource.QuartzDS.password=12345678
org.quartz.dataSource.QuartzDS.driver=com.mysql.cj.jdbc.Driver
# 建议将DataSource最大连接大小配置为至少等于线程池中的工作线程数量加上三个
org.quartz.dataSource.QuartzDS.maxConnections=15

Quartz配置类

package com.dam.config.quartz;

import org.jetbrains.annotations.NotNull;
import org.quartz.Scheduler;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import java.io.IOException;
import java.util.Properties;

/**
 * 此配置类(QuartzConfig)负责设置并集成Quartz Scheduler到Spring框架中
 * 实现了{@link SchedulerFactoryBeanCustomizer}接口
 * 用于自定义Quartz {@link SchedulerFactoryBean}的默认行为
 *
 */
@Configuration
public class QuartzConfig implements SchedulerFactoryBeanCustomizer {

    /**
     * 此方法创建一个由Spring管理的Quartz属性Bean。
     *
     * @return 加载并初始化后的Quartz属性对象
     * @throws IOException 若在读取配置文件过程中发生IO异常
     */
    @Bean
    public Properties properties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        // 设置quartz配置文件所在路径
        propertiesFactoryBean.setLocation(new ClassPathResource("/myQuartz.properties"));
        // 在配置文件中的属性被读取并注入后 再 初始化对象
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }


    /**
     * 创建并返回一个Spring管理的{@link SchedulerFactoryBean}实例。
     *
     * @return 配置好的{@link SchedulerFactoryBean}实例
     * @throws IOException 若在加载配置文件过程中发生IO异常
     */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        // 通过传入由上述 properties() 方法返回的Quartz属性对象,确保该工厂bean使用指定的配置信息初始化
        // 后续通过该工厂bean创建的Quartz调度器将遵循这些自定义属性设定
        schedulerFactoryBean.setQuartzProperties(properties());
        return schedulerFactoryBean;
    }

    /**
     * 通过SchedulerFactoryBean获取Scheduler的实例
     * 简化了从工厂Bean获取实际调度器实例的过程,方便在应用中直接注入和使用。
     *
     * @return 初始化并准备就绪的Quartz {@link Scheduler}实例
     * @throws IOException 若在加载配置文件过程中发生IO异常
     */
    @Bean
    public Scheduler scheduler() throws IOException {
        return schedulerFactoryBean().getScheduler();
    }

    /**
     * 重写{@link SchedulerFactoryBeanCustomizer#customize(SchedulerFactoryBean)}方法,
     * 以实现对{@link SchedulerFactoryBean}的自定义配置。
     * <p>
     * 在此方法中,我们设置如下Quartz调度器参数:
     *
     * @param schedulerFactoryBean 需要进行定制化的{@link SchedulerFactoryBean}实例
     */
    @Override
    public void customize(@NotNull SchedulerFactoryBean schedulerFactoryBean) {
        // 设置启动延迟为2秒,即在Spring容器启动后2秒开始初始化调度器
        schedulerFactoryBean.setStartupDelay(2);
        // 设置自动启动调度器,即在Spring容器启动时自动启动调度器
        schedulerFactoryBean.setAutoStartup(true);
        // 设置允许覆盖已存在的作业,当存在同名作业时,使用新配置覆盖旧配置
        schedulerFactoryBean.setOverwriteExistingJobs(true);
    }
}

自定义工作类

该类用于定义定时任务触发时,需要执行什么样的逻辑。注意,我这里使用JobDataMap来获取一些信息,然后根据这些信息来判断要执行什么业务,这些信息是在添加定时任务的时候设置的。因为我这里需要执行的逻辑种类较少,只有发送工作通知和休息通知两种,所以直接使用了if判断的方式,如果逻辑种类较多,建议使用设计模式对代码进行优化,例如使用策略模式

import com.dam.ApplicationContextHolder;
import com.dam.model.enums.quartz.QuartzEnum;
import com.dam.service.QuartzNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;

/**
 * 自定义工作类
 */
@Slf4j
public class DamJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        log.info("执行定时任务");
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        //任务名称
        String jName = jobDataMap.get("jName").toString();
        //任务组
        String jGroup = jobDataMap.get("jGroup").toString();
        //触发器名称
        String tName = jobDataMap.get("tName").toString();
        //触发器组
        String tGroup = jobDataMap.get("tGroup").toString();
        System.out.println("jName:" + jName);
        System.out.println("jGroup:" + jGroup);

        if (jGroup.equals(QuartzEnum.J_GROUP_WORK_NOTICE.getCode().toString())) {
            System.out.println("发送工作通知");
            //--if--工作通知
            Long storeId = Long.parseLong(jName);
            int workNoticeType = Integer.parseInt(jobDataMap.get("workNoticeType").toString());
            QuartzNoticeService quartzNoticeService = (QuartzNoticeService) ApplicationContextHolder.getBean("quartzNoticeServiceImpl");
            quartzNoticeService.sendWorkNotice(storeId, workNoticeType);
        }

        if (jGroup.equals(QuartzEnum.J_GROUP_REST_NOTICE.getCode().toString())) {
            System.out.println("发送休息通知");
            //--if--休息通知
            Long storeId = Long.parseLong(jName);
            int holidayNoticeType = Integer.parseInt(jobDataMap.get("holidayNoticeType").toString());
            QuartzNoticeService quartzNoticeService = (QuartzNoticeService) ApplicationContextHolder.getBean("quartzNoticeServiceImpl");
            quartzNoticeService.sendRestNotice(storeId, holidayNoticeType);
        }
    }
}

Quartz相关表增删改查

实体类

在这里插入图片描述

Mapper

在这里插入图片描述

Dto

import lombok.Data;

import java.math.BigInteger;

@Data
public class JobAndTriggerDto {

    private String JOB_NAME;

    private String JOB_GROUP;

    private String JOB_CLASS_NAME;

    private String TRIGGER_NAME;

    private String TRIGGER_GROUP;

    private BigInteger REPEAT_INTERVAL;

    private BigInteger TIMES_TRIGGERED;

    private String CRON_EXPRESSION;

    private String TIME_ZONE_ID;
}

作业管理Service

import com.dam.dto.JobAndTriggerDto;
import com.github.pagehelper.PageInfo;
import org.quartz.SchedulerException;

import java.util.Map;

public interface QuartzService {

    PageInfo<JobAndTriggerDto> getJobAndTriggerDetails(Integer pageNum, Integer pageSize);

    void addJob(Map<String,Object> paramMap);

    void pauseJob(String jName, String jGroupe) throws SchedulerException;

    void resumeJob(String jName, String jGroup) throws SchedulerException;

    void rescheduleJob(String jName, String jGroup, String cron) throws SchedulerException;

    void deleteJob(String jName, String jGroup) throws SchedulerException;
}
impl
import com.dam.custom.quartz.DamJob;
import com.dam.dao.JobDetailMapper;
import com.dam.dto.JobAndTriggerDto;
import com.dam.service.QuartzService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class QuartzServiceImpl implements QuartzService {

    /**
     * 该类负责与数据库交互,管理作业详情。
     */
    @Autowired
    private JobDetailMapper jobDetailMapper;

    /**
     * Quartz Scheduler实例,负责调度、执行和管理作业与触发器。
     */
    @Autowired
    private Scheduler scheduler;

    /**
     * 分页查询定时任务
     *
     * @param pageNum  当前页码
     * @param pageSize 每页记录数
     * @return 包含分页信息的JobAndTriggerDto列表
     */
    @Override
    public PageInfo<JobAndTriggerDto> getJobAndTriggerDetails(Integer pageNum, Integer pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<JobAndTriggerDto> list = jobDetailMapper.getJobAndTriggerDetails();
        PageInfo<JobAndTriggerDto> pageInfo = new PageInfo<>(list);
        return pageInfo;
    }

    /**
     * 新增定时任务
     */
    @Override
    public void addJob(Map<String, Object> paramMap) {
        try {
            System.out.println("添加定时任务参数:" + paramMap);

            // 任务名称
            String jName = paramMap.get("jName").toString();
            // 任务组
            String jGroup = paramMap.get("jGroup").toString();
            // 触发器名称
            String tName = paramMap.get("tName").toString();
            // 触发器组
            String tGroup = paramMap.get("tGroup").toString();
            // cron表达式
            String cron = paramMap.get("cron").toString();

            // 删除同组同名的定时任务
            this.deleteJob(jName, jGroup);
            // 构建JobDetail对象,指定任务类为DamJob,并设置其身份(名称、组)
            JobDetail jobDetail = JobBuilder.newJob(DamJob.class)
                    .withIdentity(jName, jGroup)
                    .build();
            // 将传入参数存入JobDataMap中,以便在任务执行时使用
            for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
                jobDetail.getJobDataMap().put(entry.getKey(), entry.getValue().toString());
            }
            // 按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(tName, tGroup)
                    .startNow()
                    .withSchedule(CronScheduleBuilder.cronSchedule(cron))
                    .build();
            // 启动调度器
            scheduler.start();
            // 将JobDetail与Trigger关联,并提交至调度器
            scheduler.scheduleJob(jobDetail, trigger);
            log.info("添加定时任务成功");
        } catch (Exception e) {
            log.info("创建定时任务失败" + e);
        }
    }

    /**
     * 根据任务名、任务组 暂停任务
     *
     * @param jName
     * @param jGroup
     * @throws SchedulerException
     */
    @Override
    public void pauseJob(String jName, String jGroup) throws SchedulerException {
        scheduler.pauseJob(JobKey.jobKey(jName, jGroup));
    }

    /**
     * 根据任务名、任务组 恢复指定任务
     *
     * @param jName 任务名称
     * @param jGroup 任务组名
     * @throws SchedulerException 调度器操作异常
     */
    @Override
    public void resumeJob(String jName, String jGroup) throws SchedulerException {
        scheduler.resumeJob(JobKey.jobKey(jName, jGroup));
    }

    /**
     * 重新调度指定任务,更改其cron表达式
     *
     * @param jName 任务名称
     * @param jGroup 任务组名
     * @param cron 新的cron表达式
     * @throws SchedulerException 调度器操作异常
     */
    @Override
    public void rescheduleJob(String jName, String jGroup, String cron) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(jName, jGroup);
        // 使用新的cron表达式,创建新的CronScheduleBuilder
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
        // 获取原有的CronTrigger
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        // 按新的cronExpression表达式重新构建trigger
        trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
        // 使用新的CronTrigger重新设置任务执行计划,并重启触发器
        scheduler.rescheduleJob(triggerKey, trigger);
    }

    /**
     * 删除指定任务及其关联触发器
     *
     * @param jName 任务名称
     * @param jGroup 任务组名
     * @throws SchedulerException 调度器操作异常
     */
    @Override
    public void deleteJob(String jName, String jGroup) throws SchedulerException {
        // 先暂停触发器
        scheduler.pauseTrigger(TriggerKey.triggerKey(jName, jGroup));
        // 移除触发器的调度计划
        scheduler.unscheduleJob(TriggerKey.triggerKey(jName, jGroup));
        // 删除任务本身
        scheduler.deleteJob(JobKey.jobKey(jName, jGroup));
    }
}

定时通知管理

该部分代码用来给门店管理员设置门店的通知时间以及通知类型,门店管理员设置好通知之后,将通知转化为定时任务进行执行
在这里插入图片描述

定时通知实体类

import com.baomidou.mybatisplus.annotation.TableName;
import com.dam.model.entity.BaseEntity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 系统定时通知
 *
 * @author dam
 * @email 1782067308@qq.com
 * @date 2023-03-21 20:45:41
 */
@Data
@TableName("system_scheduled_notice")
public class SystemScheduledNoticeEntity extends BaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 门店id
     */
    @JsonSerialize(using = ToStringSerializer.class)
    private Long storeId;

    /**
     * 门店是否启用上班通知提醒
     */
    private Integer workNoticeUse;

    /**
     * 上班通知提醒时间,如每天晚上八点提醒相关人员第二天是否需要上班
     */
    private Date workNoticeTime;

    /**
     * 工作通知方式 0:系统发送消息 1:发送邮件 2:系统发送消息及发送邮件
     */
    private Integer workNoticeType;

    /**
     * 门店是否启用休假通知提醒
     */
    private Integer holidayNoticeUse;

    /**
     * 休假通知提醒时间,如每天晚上八点提醒相关人员第二天是否需要上班
     */
    private Date holidayNoticeTime;

    /**
     * 休假通知方式 0:系统发送消息 1:发送邮件 2:系统发送消息及发送邮件
     */
    private Integer holidayNoticeType;
}

定时通知Service

import com.baomidou.mybatisplus.extension.service.IService;
import com.dam.model.entity.system.SystemScheduledNoticeEntity;
import com.dam.utils.PageUtils;

import java.util.Map;

/**
 * 系统定时通知
 *
 * @author dam
 * @email 1782067308@qq.com
 * @date 2023-03-21 20:45:41
 */
public interface SystemScheduledNoticeService extends IService<SystemScheduledNoticeEntity> {

    PageUtils queryPage(Map<String, Object> params);

    public void addJob(SystemScheduledNoticeEntity systemScheduledNotice, Long storeId);

    public void deleteJob(SystemScheduledNoticeEntity systemScheduledNotice, Long storeId);
}

定时通知实现类

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cronutils.builder.CronBuilder;
import com.cronutils.descriptor.CronDescriptor;
import com.cronutils.model.Cron;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.dam.dao.SystemScheduledNoticeDao;
import com.dam.feign.EnterpriseFeignService;
import com.dam.model.entity.system.SystemScheduledNoticeEntity;
import com.dam.model.enums.quartz.QuartzEnum;
import com.dam.service.SystemScheduledNoticeService;
import com.dam.utils.PageUtils;
import com.dam.utils.Query;
import com.dam.utils.date.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import static com.cronutils.model.field.expression.FieldExpression.questionMark;
import static com.cronutils.model.field.expression.FieldExpressionFactory.always;
import static com.cronutils.model.field.expression.FieldExpressionFactory.on;


@Service("systemScheduledNoticeService")
public class SystemScheduledNoticeServiceImpl extends ServiceImpl<SystemScheduledNoticeDao, SystemScheduledNoticeEntity> implements SystemScheduledNoticeService {

    @Autowired
    private EnterpriseFeignService enterpriseFeignService;

    /**
     * 分页查询
     *
     * @param params
     * @return
     */
    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<SystemScheduledNoticeEntity> page = this.page(
                new Query<SystemScheduledNoticeEntity>().getPage(params),
                new QueryWrapper<SystemScheduledNoticeEntity>()
        );
        return new PageUtils(page);
    }

    /**
     * 添加定时任务
     *
     * @param systemScheduledNotice
     * @param storeId
     */
    public void addJob(SystemScheduledNoticeEntity systemScheduledNotice, Long storeId) {
         添加工作提醒定时任务
        if (systemScheduledNotice.getWorkNoticeUse() == 1) {
            // 上班通知时间
            Date workNoticeTime = systemScheduledNotice.getWorkNoticeTime();
            // 工作通知方式 0:系统发送消息 1:发送邮件 2:系统发送消息及发送邮件
            Integer workNoticeType = systemScheduledNotice.getWorkNoticeType();
            // 解析日期时间,获取小时、分钟、秒信息
            DateUtil.DateEntity dateEntity = DateUtil.parseDate(workNoticeTime);

            String cronAsString = getCronAsString(dateEntity);
            /// 构建参数字典
            HashMap<String, Object> paramMap = new HashMap<>();
            paramMap.put("workNoticeType", workNoticeType);
            // 任务名称
            paramMap.put("jName", storeId.toString());
            // 任务组
            paramMap.put("jGroup", QuartzEnum.J_GROUP_WORK_NOTICE.getCode().toString());
            // 触发器名称
            paramMap.put("tName", QuartzEnum.T_NAME_SEND_WORK_NOTICE.getCode().toString());
            // 触发器组
            paramMap.put("tGroup", QuartzEnum.T_GROUP_DAILY_NOTICE.getCode().toString());
            paramMap.put("cron", cronAsString);
            /// 添加到定时任务
            enterpriseFeignService.addJob(
                    paramMap
            );
        }

         添加休息提醒定时任务
        /// 生成cron
        Integer holidayNoticeUse = systemScheduledNotice.getHolidayNoticeUse();
        Date holidayNoticeTime = systemScheduledNotice.getHolidayNoticeTime();
        Integer holidayNoticeType = systemScheduledNotice.getHolidayNoticeType();
        DateUtil.DateEntity dateEntity1 = DateUtil.parseDate(holidayNoticeTime);
        if (holidayNoticeUse == 1) {

            // 获取cron表达式
            String cronAsString = getCronAsString(dateEntity1);

            //构建参数字典
            HashMap<String, Object> paramMap = new HashMap<>();
            paramMap.put("holidayNoticeType", holidayNoticeType);
            //任务名称
            paramMap.put("jName", storeId.toString());
            //任务组
            paramMap.put("jGroup", QuartzEnum.J_GROUP_REST_NOTICE.getCode().toString());
            //触发器名称
            paramMap.put("tName", QuartzEnum.T_NAME_SEND_REST_NOTICE.getCode().toString());
            //触发器组
            paramMap.put("tGroup", QuartzEnum.T_GROUP_DAILY_NOTICE.getCode().toString());
            paramMap.put("cron", cronAsString);
            //添加到定时任务
            enterpriseFeignService.addJob(
                    paramMap
            );
        }
    }

    /**
     * 根据日期实体,获取cron表达式
     * @param dateEntity
     * @return
     */
    private static String getCronAsString(DateUtil.DateEntity dateEntity) {
        /// 生成cron
        // 每天的这个时、分、秒 执行任务
        Cron cron = CronBuilder.cron(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ))
                // 年,设置为always(),表示每一年
                .withYear(always())
                // 月
                .withMonth(always())
                // 一个月的第几天
                // questionMark()相当于在 Cron 表达式中使用字符 '?',注意:withDoM和withDoW不能同时设置questionMark()
                .withDoM(questionMark())
                // 一周的第几天
                .withDoW(always())
                // 时
                .withHour(on(dateEntity.getHour()))
                // 分
                .withMinute(on(dateEntity.getMinute()))
                // 秒
                .withSecond(on(dateEntity.getSecond()))
                .instance();
        // 获取cron表达式
        String cronAsString = cron.asString();
        // System.out.println("cronAsString:" + cronAsString);
        // 翻译cron表达式,Locale.UK设置用英文描述(这段代码可以删除)
        CronDescriptor descriptor = CronDescriptor.instance(Locale.UK);
        String description = descriptor.describe(cron);
        System.out.println("上班通知cron表达式翻译:" + description);
        return cronAsString;
    }

    @Override
    public void deleteJob(SystemScheduledNoticeEntity systemScheduledNotice, Long storeId) {
        // 构建参数字典
        HashMap<String, Object> paramMap = new HashMap<>();
        // 任务名称
        paramMap.put("jName", storeId.toString());
        // 任务组
        paramMap.put("jGroup", QuartzEnum.J_GROUP_WORK_NOTICE.getCode().toString());
        enterpriseFeignService.deleteJob(paramMap);
    }

}

Controller

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dam.model.entity.system.SystemScheduledNoticeEntity;
import com.dam.model.result.R;
import com.dam.service.SystemScheduledNoticeService;
import com.dam.utils.JwtUtil;
import com.dam.utils.PageUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Map;


/**
 * 系统定时通知
 *
 * @author dam
 * @email 1782067308@qq.com
 * @date 2023-03-21 20:45:41
 */
@RestController
@RequestMapping("enterprise/systemScheduledNotice")
public class SystemScheduledNoticeController {
    @Autowired
    private SystemScheduledNoticeService systemScheduledNoticeService;

    /**
     * 列表
     */
    @RequestMapping("/list")
    public R list(@RequestParam Map<String, Object> params) {
        PageUtils page = systemScheduledNoticeService.queryPage(params);

        return R.ok().addData("page", page);
    }

    /**
     * 信息
     */
    @RequestMapping("/info/{id}")
    public R info(@PathVariable("id") Long id) {
        SystemScheduledNoticeEntity systemScheduledNotice = systemScheduledNoticeService.getById(id);

        return R.ok().addData("systemScheduledNotice", systemScheduledNotice);
    }

    /**
     * 信息
     */
    @RequestMapping("/infoByToken")
    public R infoByToken(HttpServletRequest httpServletRequest) {
        Long storeId = Long.parseLong(JwtUtil.getStoreId(httpServletRequest.getHeader("token")));
        SystemScheduledNoticeEntity systemScheduledNotice = systemScheduledNoticeService.getOne(new QueryWrapper<SystemScheduledNoticeEntity>().eq("store_id", storeId));

        return R.ok().addData("systemScheduledNotice", systemScheduledNotice);
    }

    /**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@RequestBody SystemScheduledNoticeEntity systemScheduledNotice, HttpServletRequest httpServletRequest) {
        Long storeId = Long.parseLong(JwtUtil.getStoreId(httpServletRequest.getHeader("token")));
        systemScheduledNotice.setStoreId(storeId);
        systemScheduledNoticeService.save(systemScheduledNotice);
        // 添加定时任务
        systemScheduledNoticeService.addJob(systemScheduledNotice,storeId);
        return R.ok();
    }

    /**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@RequestBody SystemScheduledNoticeEntity systemScheduledNotice, HttpServletRequest httpServletRequest) {
        Long storeId = Long.parseLong(JwtUtil.getStoreId(httpServletRequest.getHeader("token")));
        systemScheduledNoticeService.updateById(systemScheduledNotice);
        // 删除之前的定时任务
        systemScheduledNoticeService.deleteJob(systemScheduledNotice,storeId);
        // 添加定时任务
        systemScheduledNoticeService.addJob(systemScheduledNotice,storeId);
        return R.ok();
    }

    /**
     * 删除
     */
    @RequestMapping("/deleteBatch")
    public R deleteBatch(@RequestBody Long[] ids) {
        systemScheduledNoticeService.removeByIds(Arrays.asList(ids));
        return R.ok();
    }
}

通知发送实现

该部分代码为消息发送实现方式

在这里插入图片描述

/**
 * 定时通知
 */
public interface QuartzNoticeService {
    void sendWorkNotice(Long storeId, Integer workNoticeType);

    void sendRestNotice(Long storeId, Integer workNoticeType);
}

【impl】

package com.dam.service.impl;

import com.alibaba.fastjson.TypeReference;
import com.dam.constant.RabbitMqConstant;
import com.dam.feign.ShiftSchedulingCalculateFeignService;
import com.dam.feign.SystemFeignService;
import com.dam.model.dto.scheduling_calculate_service.StaffWorkDto;
import com.dam.model.dto.third_party.EmailDto;
import com.dam.model.entity.enterprise.EnterpriseEntity;
import com.dam.model.entity.enterprise.MessageEntity;
import com.dam.model.entity.enterprise.StoreEntity;
import com.dam.model.entity.enterprise.UserMessageEntity;
import com.dam.model.entity.shiftScheduling.SchedulingShiftEntity;
import com.dam.model.entity.system.UserEntity;
import com.dam.model.enums.ResultCodeEnum;
import com.dam.model.result.R;
import com.dam.service.*;
import com.dam.utils.mail.MailUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;

/**
 * 用来发送通知
 */
@Slf4j
@Service
public class QuartzNoticeServiceImpl implements QuartzNoticeService {
    @Autowired
    private ShiftSchedulingCalculateFeignService shiftSchedulingCalculateFeignService;
    @Autowired
    private MessageService messageService;
    @Autowired
    private UserMessageService userMessageService;
    @Autowired
    private SystemFeignService systemFeignService;
    @Autowired
    private StoreService storeService;
    @Autowired
    private EnterpriseService enterpriseService;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送工作通知
     *
     * @param storeId
     * @param workNoticeType 工作通知方式 0:系统发送消息 1:发送邮件 2:系统发送消息及发送邮件
     */
    @Override
    public void sendWorkNotice(Long storeId, Integer workNoticeType) {
        log.info("发送工作通知");
         组装要查询的日期信息
        // 今天日期
        LocalDate today = LocalDate.now();
        LocalDate secondDay = today.plusDays(1);
        // 将LocalDate转换为Date
        Date secondDate = Date.from(secondDay.atStartOfDay(ZoneId.systemDefault()).toInstant());
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("date", secondDate);
        paramMap.put("storeId", storeId);
//        System.out.println("paramMap:"+paramMap);

         先判断那天是否需要上班
        R r1 = shiftSchedulingCalculateFeignService.judgeOneDateIsRest(paramMap);
        Boolean isRest = r1.getData("isRest", new TypeReference<Boolean>() {
        });
        System.out.println("sendWorkNotice,isRest:" + isRest);
        System.out.println("sendWorkNotice,workNoticeType:" + workNoticeType);
        if (isRest == false) {
            /// 查询出企业和门店信息
            StoreEntity store = storeService.getById(storeId);
            EnterpriseEntity enterprise = enterpriseService.getById(store.getEnterpriseId());

            /// 查询出需要上班的员工及其上班时间
            R r2 = shiftSchedulingCalculateFeignService.listStaffWorkDtoByWorkDate(paramMap);
            List<StaffWorkDto> staffWorkDtoList = r2.getData("staffWorkDtoList", new TypeReference<List<StaffWorkDto>>() {
            });
//            System.out.println("需要工作的员工:"+userIdList.toString());

            String subject = "智能排班系统——明日上班通知";
            String contentStart = "<html>\n" +
                    "<head>\n" +
                    "    <style>\n" +
                    "        /* Set background color */\n" +
                    "        body {\n" +
                    "            background-color: #f2f2f2;\n" +
                    "        }\n" +
                    "\n" +
                    "        /* Add background image */\n" +
                    "        /* .background-image {\n" +
                    "            background-image: url('https://example.com/background-image.jpg');\n" +
                    "            background-repeat: no-repeat;\n" +
                    "            background-position: center;\n" +
                    "            background-size: cover;\n" +
                    "            opacity: 0.7;\n" +
                    "            position: absolute;\n" +
                    "            top: 0;\n" +
                    "            left: 0;\n" +
                    "            width: 100%;\n" +
                    "            height: 100%;\n" +
                    "            z-index: -1;\n" +
                    "        } */\n" +
                    "\n" +
                    "        /* Style for the content */\n" +
                    "        h1 {\n" +
                    "            color: #0b0e67;\n" +
                    "            text-align: center;\n" +
                    "            margin-top: 50px;\n" +
                    "        }\n" +
                    "\n" +
                    "        p {\n" +
                    "            font-size: 16px;\n" +
                    "            line-height: 1.5;\n" +
                    "            text-align: justify;\n" +
                    "            margin: 20px auto;\n" +
                    "            max-width: 700px;\n" +
                    "        }\n" +
                    "    </style>\n" +
                    "</head>\n" +
                    "<body>\n" +
                    "    <!-- Add a background image div -->\n" +
                    "    <div class=\"background-image\"></div>\n" +
                    "    <h2>门店上班通知</h2>\n" +
                    "    <div style=\"display: flex;justify-content: center;\">\n" +
                    "        <div>\n" +
                    "            <p>尊敬的员工:</p>\n" +
                    "            <p>&emsp;&emsp;您好!明天是我们门店的营业日,您被排班上班。我们特此通知您,希望您今晚早点休息,保持良好的精神状态,明天按时上班。\n" +
                    "                请您在明天早上提前留出足够的时间,为自己安排好交通和出行计划,避免迟到的情况发生。为了保证门店正常的运营和服务,我们希望您能够严格遵守上班时间和规定。\n" +
                    "                我们希望您能够认真对待自己的工作,为门店的发展和顾客的服务贡献自己的力量。如果您有任何疑问或需要帮助,请随时联系门店管理员。谢谢您的支持和合作!具体排班信息如下:</p>\n";

            String contentEnd = "            <p style=\"text-align: right;\">祝好!</p>\n" +
                    "            <p style=\"text-align: right;\">" + enterprise.getName() + "——" + store.getName() + "</p>\n" +
                    "        </div>\n" +
                    "    </div>\n" +
                    "</body>\n" +
                    "</html>\n";

            List<String> shiftMessageList = new ArrayList<>();
            List<Long> userIdList = new ArrayList<>();
            Map<Long, String> userIdAndMessageMap = new HashMap<>();
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
            for (int i = 0; i < staffWorkDtoList.size(); i++) {
                userIdList.add(staffWorkDtoList.get(i).getUserId());
                StringBuilder stringBuilder = new StringBuilder();
                for (SchedulingShiftEntity shift : staffWorkDtoList.get(i).getShiftEntityList()) {
                    String str = "";
                    if (shift.getMealType() == 0) {
                        str = "(安排午餐)";
                    } else if (shift.getMealType() == 1) {
                        str = "(安排晚餐)";
                    }
                    stringBuilder.append("<p>上班时间:" + sdf.format(shift.getStartDate()) + "至" + sdf.format(shift.getEndDate()) + str + "</p>\n");
                }
                shiftMessageList.add(stringBuilder.toString());
                userIdAndMessageMap.put(staffWorkDtoList.get(i).getUserId(), stringBuilder.toString());
            }
//            System.out.println("userIdList:" + userIdList.toString());

            /// 系统发送消息
            if (workNoticeType == 0 || workNoticeType == 2) {
                System.out.println("sendWorkNotice,系统发送消息");

                // 存储用户和消息的绑定关系
                List<UserMessageEntity> userMessageEntityList = new ArrayList<>();
                for (int i = 0; i < staffWorkDtoList.size(); i++) {
                    // 存储消息
                    MessageEntity messageEntity = new MessageEntity();
                    messageEntity.setType(2);
                    messageEntity.setSubject(subject);
                    messageEntity.setContent(contentStart + shiftMessageList.get(i) + contentEnd);
                    messageEntity.setStoreId(storeId);
                    messageEntity.setEnterpriseId(null);
                    messageEntity.setIsPublish(1);
                    messageEntity.setPublishTime(new Date());
                    messageService.save(messageEntity);

                    UserMessageEntity userMessageEntity = new UserMessageEntity();
                    userMessageEntity.setUserId(staffWorkDtoList.get(i).getUserId());
                    userMessageEntity.setMessageId(messageEntity.getId());
                    userMessageEntityList.add(userMessageEntity);
                }
                userMessageService.saveBatch(userMessageEntityList);
            }

            /// 发送邮件
            if (workNoticeType == 1 || workNoticeType == 2) {
                System.out.println("sendWorkNotice,发送邮件");
                R r3 = systemFeignService.getUserIdAndMailMapByUserIdList(userIdList);
                Map<Long, String> userIdAndMailMap = r3.getData("userIdAndMailMap", new TypeReference<Map<Long, String>>() {
                });
//                System.out.println("userIdAndMailMap:" + userIdAndMailMap.toString());
                HashSet<String> mailSet = new HashSet<>();
//                System.out.println("mailList:" + mailList.toString());
                for (Long userId : userIdList) {
                    String mail = userIdAndMailMap.get(userId);
                    if (MailUtil.judgeWhetherTheMailIsLegal(mail) == false || mailSet.contains(mail)) {
                        continue;
                    }
                    mailSet.add(mail);
                    System.out.println("正在给" + mail + "发送邮件");
                    EmailDto emailDto = new EmailDto();
                    emailDto.setTo(mail);
                    emailDto.setSubject(subject);
                    emailDto.setContent(contentStart + userIdAndMessageMap.get(userId) + contentEnd);
                    emailDto.setType(1);
                    //发送消息到邮件发送
                    rabbitTemplate.convertAndSend(RabbitMqConstant.MAIL_SENDER_EXCHANGE, RabbitMqConstant.MAIL_SENDER_ROUTER_KEY, emailDto);
                }
            }
        }
    }

    /**
     * 发送消息通知
     *
     * @param storeId
     * @param workNoticeType
     */
    @Override
    public void sendRestNotice(Long storeId, Integer workNoticeType) {
        log.info("发送休息通知");
        查询出明天有工作的员工id
        //当天日期
        LocalDate today = LocalDate.now();
        LocalDate secondDay = today.plusDays(1);
        // 将LocalDate转换为Date
        Date secondDate = Date.from(secondDay.atStartOfDay(ZoneId.systemDefault()).toInstant());

        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("date", secondDate);
        paramMap.put("storeId", storeId);

        判断明天是否休息
        R r1 = shiftSchedulingCalculateFeignService.judgeOneDateIsRest(paramMap);
        Boolean isRest = r1.getData("isRest", new TypeReference<Boolean>() {
        });

        if (isRest == true) {
            ///查询出企业和门店信息
            StoreEntity store = storeService.getById(storeId);
            EnterpriseEntity enterprise = enterpriseService.getById(store.getEnterpriseId());

            String subject = "智能排班系统——明日休息通知";
            String content = "<html>\n" +
                    "<head>\n" +
                    "    <style>\n" +
                    "        /* Set background color */\n" +
                    "        body {\n" +
                    "            background-color: #f2f2f2;\n" +
                    "        }\n" +
                    "\n" +
                    "        /* Style for the content */\n" +
                    "        h1 {\n" +
                    "            color: #0b0e67;\n" +
                    "            text-align: center;\n" +
                    "            margin-top: 50px;\n" +
                    "        }\n" +
                    "\n" +
                    "        p {\n" +
                    "            font-size: 16px;\n" +
                    "            line-height: 1.5;\n" +
                    "            text-align: justify;\n" +
                    "            margin: 20px auto;\n" +
                    "            max-width: 700px;\n" +
                    "        }\n" +
                    "    </style>\n" +
                    "</head>\n" +
                    "<body>\n" +
                    "    <!-- Add a background image div -->\n" +
                    "    <div class=\"background-image\"></div>\n" +
                    "    <h2>门店休息通知</h2>\n" +
                    "    <div style=\"display: flex;justify-content: center;\">\n" +
                    "        <div>\n" +
                    "            <p>尊敬的员工:</p>\n" +
                    "            <p>&emsp;&emsp;        您好!明天我们门店将休息一天,为了让大家更好地调整状态和享受生活,我们特此通知您好好休息,做自己喜欢做的事情,放松心情,愉快度过这个美好的假期。\n" +
                    "            <p style=\"text-align: right;\">祝好!</p>\n" +
                    "            <p style=\"text-align: right;\">" + enterprise.getName() + "——" + store.getName() + "</p>\n" +
                    "        </div>\n" +
                    "    </div>\n" +
                    "</body>\n" +
                    "</html>\n";

            ///查出门店的所有员工
            R r2 = systemFeignService.listUserEntityByStoreId(storeId);
            if (r2.getCode() == ResultCodeEnum.SUCCESS.getCode().intValue()) {
                List<UserEntity> userList = r2.getData("userList", new TypeReference<List<UserEntity>>() {
                });
                List<Long> userIdList = new ArrayList<>();
                List<String> mailList = new ArrayList<>();
                for (UserEntity userInfoVo : userList) {
                    userIdList.add(userInfoVo.getId());
                    mailList.add(userInfoVo.getMail());
                }
                ///系统发送消息
                if (workNoticeType == 0 || workNoticeType == 2) {
                    ///存储消息
                    MessageEntity messageEntity = new MessageEntity();
                    messageEntity.setType(2);
                    messageEntity.setSubject(subject);
                    messageEntity.setContent(content);
                    messageEntity.setStoreId(storeId);
                    messageEntity.setEnterpriseId(null);
                    messageEntity.setIsPublish(1);
                    messageEntity.setPublishTime(new Date());
                    messageService.save(messageEntity);
                    ///存储用户和消息的绑定关系
                    List<UserMessageEntity> userMessageEntityList = new ArrayList<>();
                    for (Long userId : userIdList) {
                        UserMessageEntity userMessageEntity = new UserMessageEntity();
                        userMessageEntity.setUserId(userId);
                        userMessageEntity.setMessageId(messageEntity.getId());
                        userMessageEntityList.add(userMessageEntity);
                    }
                    userMessageService.saveBatch(userMessageEntityList);
                }

                ///发送邮件
                if (workNoticeType == 1 || workNoticeType == 2) {
                    HashSet<String> mailSet=new HashSet<>();
                    for (String mail : mailList) {
                        mailSet.add(mail);
                    }
                    for (String mail : mailSet) {
                        if (MailUtil.judgeWhetherTheMailIsLegal(mail) == false) {
                            continue;
                        }
                        EmailDto emailDto = new EmailDto();
                        emailDto.setTo(mail);
                        emailDto.setSubject(subject);
                        emailDto.setContent(content);
                        emailDto.setType(1);
                        //发送消息到邮件发送
                        rabbitTemplate.convertAndSend(RabbitMqConstant.MAIL_SENDER_EXCHANGE, RabbitMqConstant.MAIL_SENDER_ROUTER_KEY, emailDto);
                    }
                }
            }
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/552085.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

人工智能揭示矩阵乘法的新可能性

人工智能揭示矩阵乘法的新可能性 数学家酷爱漂亮的谜题。当你尝试找到最有效的方法时&#xff0c;即使像乘法矩阵&#xff08;二维数字表&#xff09;这样抽象的东西也会感觉像玩一场游戏。这有点像尝试用尽可能少的步骤解开魔方——具有挑战性&#xff0c;但也很诱人。除了魔方…

基于GIS、python机器学习技术的地质灾害风险评价与信息化建库应用

结合项目实践案例和科研论文成果进行讲解。入门篇&#xff0c;ArcGIS软件的快速入门与GIS数据源的获取与理解&#xff1b;方法篇&#xff0c;致灾因子提取方法、灾害危险性因子分析指标体系的建立方法和灾害危险性评价模型构建方法&#xff1b;拓展篇&#xff0c;GIS在灾害重建…

IEDA 的各种常用插件汇总

目录 IEDA 的各种常用插件汇总1、 Alibaba Java Coding Guidelines2、Translation3、Rainbow Brackets4、MyBatisX5、MyBatis Log Free6、Lombok7、Gitee IEDA 的各种常用插件汇总 1、 Alibaba Java Coding Guidelines 作用&#xff1a;阿里巴巴代码规范检查插件&#xff0c;…

JavaScript之分时函数、分时间段渲染页面、提高用户体验、参数归一化、高阶函数、分段、appendChild、requestIdleCallback

MENU 前言效果图html原始写法优化方式一(参数归一化)优化方式二(当浏览器不支持requestIdleCallback方法的时候)优化方式三(判断环境) 前言 当前需要向页面插入十万个div元素&#xff0c;如果使用普通的渲染方式&#xff0c;会造成延迟。这时候就需要通过分时函数来实现渲染了。…

[element] 简单封装一个表格展示

简单封装 如果你想呈现一个表格,直接复制案例的话是这样的,圈出来的你需要写进入,麻烦 这时候把需要显示的列数据弄成一个对象数组, 给它列名和标题就行 记得这个prop和源数据的prop要对应!! const columns [{label: "日期",prop: date},{label: "姓名",…

【管理咨询宝藏72】MBB大型城投集团能源板块行业分析报告

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏72】MBB大型城投集团能源板块行业分析报告 【格式】PDF版本 【关键词】战略规划、商业分析、管理咨询、MBB顶级咨询公司 【强烈推荐】 这是一套…

通讯录的实现(顺序表)

前言&#xff1a;上篇文章我们讲解的顺序表以及顺序表的具体实现过程&#xff0c;那么我们的顺序表在实际应用中又有什么作用呢&#xff1f;今天我们就基于顺序表来实现一下通讯录。 目录 一.准备工作 二.通讯录的实现 1.通讯录的初始化 2.插入联系人 3.删除联系人 4.…

Arthas实战教程:定位Java应用CPU过高与线程死锁

引言 在Java应用开发中&#xff0c;我们可能会遇到CPU占用过高和线程死锁的问题。本文将介绍如何使用Arthas工具快速定位这些问题。 准备工作 首先&#xff0c;我们创建一个简单的Java应用&#xff0c;模拟CPU过高和线程死锁的情况。在这个示例中&#xff0c;我们将编写一个…

OpenHarmony C/C++三方库移植适配

简介 众所周知&#xff0c;C/C三方库相对与JS/ETS的三方组件来说&#xff0c;其运行效率高。那如何将一个C/C三方库移植到OH系统上呢&#xff1f;本文将介绍如何快速高效的移植一个C/C三方库到OpenHarmony上。 C/C三方库适配问题与解决方案 由上图可以看出&#xff0c;三方库…

Ypay源支付前端美化模板

功能&#xff1a; 首页加了运行时间&#xff0c;加了首页一言打字效果&#xff0c;加了访问次数&#xff0c;还有底部也适当的加了一点美化 而且加了一个播放器功能&#xff0c;可以自定义歌曲之类的 完美契合于源支付 直接上传主题包使用即可 演示图: 使用: 请不要在后台…

C语言学习笔记之指针(一)

目录 什么是指针&#xff1f; 指针和指针类型 指针的类型 指针类型的意义 指针-整数 指针的解引用 指针 - 指针 指针的关系运算 野指针 什么是野指针&#xff1f; 野指针的成因 如何规避野指针&#xff1f; 二级指针 什么是指针&#xff1f; 在介绍指针之前&#…

Ubuntu上安装Chrome浏览器

安装步骤 1.下载安装chrome安装包 wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb2.安装Chrome浏览器 sudo dpkg -i google-chrome-stable_current_amd64.debsudo apt-get -f install3.启动Chrome浏览器 查看收藏夹里的Chrome图标 单击C…

LeetCode刷题总结 | 图论3—并查集

并查集理论基础 1.背景 首先要知道并查集可以解决什么问题呢&#xff1f; 并查集常用来解决连通性问题。大白话就是当我们需要判断两个元素是否在同一个集合里的时候&#xff0c;我们就要想到用并查集。 并查集主要有两个功能&#xff1a; 将两个元素添加到一个集合中。判…

python怎么连接oracle

一&#xff1a;弄清版本&#xff0c;最重要&#xff01;&#xff01;&#xff01; 首先安装配置时&#xff0c;必须把握一个点&#xff0c;就是版本一致&#xff01;包括&#xff1a;系统版本&#xff0c;python版本&#xff0c;oracle客户端的版本&#xff0c;cx_Oracle的版本…

IAR 使用笔记(IAR BIN大小为0异常解决)

烧写 由于芯片的内部SPI FLASH的0级BOOT 程序起到到开启JTAG SW 仿真功能&#xff0c;一旦内部SPI FLASH存储的BL0启动代码被损坏&#xff0c;芯片的JTAG 将不能被连接。所以对BL0的烧写需要谨慎&#xff0c;烧写BL0过程保证芯片不断电。 如果烧写了多备份的启动代码&#xff…

深度学习架构(CNN、RNN、GAN、Transformers、编码器-解码器架构)的友好介绍。

一、说明 本博客旨在对涉及卷积神经网络 &#xff08;CNN&#xff09;、递归神经网络 &#xff08;RNN&#xff09;、生成对抗网络 &#xff08;GAN&#xff09;、转换器和编码器-解码器架构的深度学习架构进行友好介绍。让我们开始吧&#xff01;&#xff01; 二、卷积神经网络…

【Java探索之旅】掌握数组操作,轻松应对编程挑战

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; Java编程秘籍 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一、数组巩固练习1.1 数组转字符串1.2 数组拷贝1.3 求数组中的平均值1.4 查找数组中指…

手写签名功能(vue3)

手写签名功能&#xff08;vue3&#xff09; 效果 显示效果 签名版效果 代码 代码引入 写成子组件形式&#xff0c;直接引入即可 <signature-features />代码结构 signatureFeatures&#xff1a;签名的显示效果 vueEsign&#xff1a;画板 xnSignName&#xff1a;打开…

Ubuntu修改DNS

【永久修改DNS】 临时修改DNS的方法是在 /etc/resolv.conf 添加&#xff1a;nameserver 8.8.8.8 nameserver 8.8.8.8 注意到/etc/resolv.conf最上面有这么一行&#xff1a; DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN 说明重启之后这个文件会被自动…

关于系统数据缓存的思考以及设计

文章目录 引言案例A项目B项目 分析我的实现总结 引言 缓存&#xff0c;这是一个经久不衰的话题&#xff0c;它通过“空间换时间”的战术不仅能够极大提升处理查询性能还能很好的保护底层资源。最近针对系统数据缓存的优化后&#xff0c;由于这是一个通用的场景并且有了一点心得…