Java项目实战笔记--基于SpringBoot3.0开发仿12306高并发售票系统--(二)项目实现-第四篇-quartz的使用和每日火车数据的生成

本文参考自

Springboot3+微服务实战12306高性能售票系统 - 慕课网 (imooc.com)

本文是仿12306项目实战第(二)章——项目实现 的第四篇,本篇讲解该项目中关于调度框架quartz的使用,以及通过火车基础数据生成每日火车数据的实现过程

一、定时任务介绍

  • 为什么要有定时任务

在这里插入图片描述

  • 本项目定时任务

    • 集成quartz(为什么不用SpringBoot自带定时任务,区别?)
    • 使用控台来操作定时任务:新增、暂停、重启、删除

二、项目增加batch定时任务调度模块

  • 新增batch模块

    操作同之前增加business模块

  • 代码展示

    • pom文件

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <parent>
              <artifactId>train</artifactId>
              <groupId>com.neilxu</groupId>
              <version>0.0.1-SNAPSHOT</version>
          </parent>
          <modelVersion>4.0.0</modelVersion>
      
          <artifactId>batch</artifactId>
      
          <properties>
              <maven.compiler.source>17</maven.compiler.source>
              <maven.compiler.target>17</maven.compiler.target>
          </properties>
          
          <dependencies>
              <dependency>
                  <groupId>com.neilxu</groupId>
                  <artifactId>common</artifactId>
              </dependency>
      
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-devtools</artifactId>
                  <scope>runtime</scope>
                  <optional>true</optional>
              </dependency>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
              </dependency>
          </dependencies>
      
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                  </plugin>
              </plugins>
          </build>
      </project>
      
    • TestController.java

      package com.neilxu.train.batch.controller;
      
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      public class TestController {
          @GetMapping("/hello")
          public String hello() {
              return "Hello World! Batch!";
          }
      }
      
    • application.properties

      server.port=8003
      server.servlet.context-path=/batch
      
      # 数据库连接
      spring.datasource.url=jdbc:mysql://localhost/train_batch?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai
      spring.datasource.username=train_batch
      spring.datasource.password=train_batch
      spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
      # mybatis xml路径
      mybatis.mapper-locations=classpath:/mapper/**/*.xml
      logging.level.com.neilxu.train.batch.mapper=trace
      
    • logback-spring.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <configuration>
          <!-- 修改一下路径-->
          <property name="PATH" value="./log/batch"></property>
      
          <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
              <encoder>
                  <!--            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
                  <Pattern>%d{mm:ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
              </encoder>
          </appender>
      
          <appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
              <file>${PATH}/trace.log</file>
              <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                  <FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
                  <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                      <maxFileSize>10MB</maxFileSize>
                  </timeBasedFileNamingAndTriggeringPolicy>
              </rollingPolicy>
              <layout>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
              </layout>
          </appender>
      
          <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
              <file>${PATH}/error.log</file>
              <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                  <FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
                  <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                      <maxFileSize>10MB</maxFileSize>
                  </timeBasedFileNamingAndTriggeringPolicy>
              </rollingPolicy>
              <layout>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
              </layout>
              <filter class="ch.qos.logback.classic.filter.LevelFilter">
                  <level>ERROR</level>
                  <onMatch>ACCEPT</onMatch>
                  <onMismatch>DENY</onMismatch>
              </filter>
          </appender>
      
          <root level="ERROR">
              <appender-ref ref="ERROR_FILE" />
          </root>
      
          <root level="TRACE">
              <appender-ref ref="TRACE_FILE" />
          </root>
      
          <root level="INFO">
              <appender-ref ref="STDOUT" />
          </root>
      </configuration>
      
    • BatchApplication.java

      package com.neilxu.train.batch.config;
      
      import org.mybatis.spring.annotation.MapperScan;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.core.env.Environment;
      
      @SpringBootApplication
      @ComponentScan("com.neilxu")
      @MapperScan("com.neilxu.train.*.mapper")
      public class BatchApplication {
      
          private static final Logger LOG = LoggerFactory.getLogger(BatchApplication.class);
      
          public static void main(String[] args) {
              SpringApplication app = new SpringApplication(BatchApplication.class);
              Environment env = app.run(args).getEnvironment();
              LOG.info("启动成功!!");
              LOG.info("测试地址: \thttp://127.0.0.1:{}{}/hello", env.getProperty("server.port"), env.getProperty("server.servlet.context-path"));
          }
      }
      
    • gateway 的application.properties

      # 路由转发,将/batch/...的请求转发了batch模块
      spring.cloud.gateway.routes[2].id=batch
      spring.cloud.gateway.routes[2].uri=http://127.0.0.1:8003
      #spring.cloud.gateway.routes[2].uri=lb://batch
      spring.cloud.gateway.routes[2].predicates[0]=Path=/batch/**
      
    • batch-test.http

      GET http://localhost:8003/batch/hello
      Accept: application/json
      
      ###
      
      GET http://localhost:8000/batch/hello
      Accept: application/json
      
      ###
      
  • 测试

在这里插入图片描述

三、为batch模块配置持久层生成器

1.新建数据库,新建用户,新建连接

操作同之前一样

在这里插入图片描述

2.新建表测试生成代码

同之前,用member表测试生成代码

  • batch.sql

    drop table if exists `member`;
    create table `member` (
      `id` bigint not null comment 'id',
      `mobile` varchar(11) comment '手机号',
      primary key (`id`),
      unique key `mobile_unique` (`mobile`)
    ) engine=innodb default charset=utf8mb4 comment='会员';
    
  • generator-config-batch.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration
            PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
            "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    
    <generatorConfiguration>
        <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
    
            <!-- 自动检查关键字,为关键字增加反引号 -->
            <property name="autoDelimitKeywords" value="true"/>
            <property name="beginningDelimiter" value="`"/>
            <property name="endingDelimiter" value="`"/>
    
            <!--覆盖生成XML文件-->
            <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
            <!-- 生成的实体类添加toString()方法 -->
            <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
    
            <!-- 不生成注释 -->
            <commentGenerator>
                <property name="suppressAllComments" value="true"/>
            </commentGenerator>
    
            <!-- 配置数据源,需要根据自己的项目修改 -->
            <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                            connectionURL="jdbc:mysql://localhost/train_batch?serverTimezone=Asia/Shanghai"
                            userId="train_batch"
                            password="train_batch">
            </jdbcConnection>
    
            <!-- domain类的位置 targetProject是相对pom.xml的路径-->
            <javaModelGenerator targetProject="..\batch\src\main\java"
                                targetPackage="com.neilxu.train.batch.domain"/>
    
            <!-- mapper xml的位置 targetProject是相对pom.xml的路径 -->
            <sqlMapGenerator targetProject="..\batch\src\main\resources"
                             targetPackage="mapper"/>
    
            <!-- mapper类的位置 targetProject是相对pom.xml的路径 -->
            <javaClientGenerator targetProject="..\batch\src\main\java"
                                 targetPackage="com.neilxu.train.batch.mapper"
                                 type="XMLMAPPER"/>
    
            <table tableName="member" domainObjectName="Member"/>
        </context>
    </generatorConfiguration>
    
  • generator/pom.xml

                        <!--<configurationFile>src/main/resources/generator-config-member.xml</configurationFile>-->
    <!--                    <configurationFile>src/main/resources/generator-config-business.xml</configurationFile>-->
                        <configurationFile>src/main/resources/generator-config-batch.xml</configurationFile>
    
  • 测试生成

在这里插入图片描述

没有问题

四、演示SpringBoot自带的定时任务

1.cron表达式

在线Cron表达式生成器:https://www.matools.com/cron/
定时任务三大要素:

  1. 执行的内容:功能逻辑
  2. 执行的策略:cron表达式
  3. 开关:开启定时任务

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:
  (1) Seconds Minutes Hours DayofMonth Month DayofWeek Year
  (2)Seconds Minutes Hours DayofMonth Month DayofWeek

cron从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份

举例说明:

下图是2024/3/17 生成

0 0 1 1/5 * ? * 或者 0 0 1 1/5 * ?

含义:每一年每个月,从1号开始每5天执行一次,执行时间是01:00:00

在这里插入图片描述

2.springboot自带定时任务

  • SpringBootTestJob.java

    package com.neilxu.train.batch.job;
    
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    /**
     * 适合单体应用,不适合集群
     * 没法实时更改定时任务状态和策略
     */
    @Component
    @EnableScheduling
    public class SpringBootTestJob {
    
        @Scheduled(cron = "0/5 * * * * ?")
        private void test() {
            // 增加分布式锁,解决集群问题
            System.out.println("SpringBootTestJob TEST");
        }
    }
    
  • 效果

在这里插入图片描述

  • 问题

    • 在不增加分布式锁的情况下,集群部署时,容易造成多个应用同时都跑了任务
    • 不能实时更新定时任务状态和策略

五、定时调度模块集成quartz

1.引入依赖

batch的 pom文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2.编写TestJob.java

package com.neilxu.train.batch.job;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class TestJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("TestJob TEST");
    }
}

3.编写QuartzConfig.java

package com.neilxu.train.batch.config;

import com.neilxu.train.batch.job.TestJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {

    /**
     * 声明一个任务
     * @return
     */
    @Bean
    public JobDetail jobDetail() {
        return JobBuilder.newJob(TestJob.class)
                .withIdentity("TestJob", "test")
                .storeDurably()
                .build();
    }

    /**
     * 声明一个触发器,什么时候触发这个任务
     * @return
     */
    @Bean
    public Trigger trigger() {
        return TriggerBuilder.newTrigger()
                .forJob(jobDetail())
                .withIdentity("trigger", "trigger")
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule("*/2 * * * * ?"))
                .build();
    }
}

4.测试

重启

在这里插入图片描述

定时任务成功

这里我把之前springboot的定时给注释掉了,其实他们两个是可以同时进行的,但是实际项目中一般只选一种定时框架

六、关于调度任务的并发执行

并发执行:上一周期还没执行完,下一周期又开始了
使用@DisallowConcurrentExecution禁用并发执行

  • 禁止任务并发执行

    TestJob.java

    package com.neilxu.train.batch.job;
    
    import org.quartz.DisallowConcurrentExecution;
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    
    @DisallowConcurrentExecution
    public class TestJob implements Job {
    
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            System.out.println("TestJob TEST开始");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("TestJob TEST结束");
        }
    }
    
  • 测试

    不会出现任务没执行完,就开始下个任务的情况了

七、使用数据库配置quartz调度任务

1.增加quartz相关的12张表

  • batch.sql

    #
    # Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar
    #
    # PLEASE consider using mysql with innodb tables to avoid locking issues
    #
    # In your Quartz properties file, you'll need to set
    # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    #
    DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
    DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
    DROP TABLE IF EXISTS QRTZ_LOCKS;
    DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
    DROP TABLE IF EXISTS QRTZ_CALENDARS;
    CREATE TABLE QRTZ_JOB_DETAILS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        JOB_NAME  VARCHAR(200) NOT NULL  comment 'job名称',
        JOB_GROUP VARCHAR(200) NOT NULL  comment 'job组',
        DESCRIPTION VARCHAR(250) NULL  comment '描述',
        JOB_CLASS_NAME   VARCHAR(250) NOT NULL  comment 'job类名',
        IS_DURABLE VARCHAR(1) NOT NULL  comment '是否持久化',
        IS_NONCONCURRENT VARCHAR(1) NOT NULL  comment '是否非同步',
        IS_UPDATE_DATA VARCHAR(1) NOT NULL  comment '是否更新数据',
        REQUESTS_RECOVERY VARCHAR(1) NOT NULL  comment '请求是否覆盖',
        JOB_DATA BLOB NULL  comment 'job数据',
        PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
    );
    CREATE TABLE QRTZ_TRIGGERS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        TRIGGER_NAME VARCHAR(200) NOT NULL comment '触发器名称',
        TRIGGER_GROUP VARCHAR(200) NOT NULL  comment '触发器组',
        JOB_NAME  VARCHAR(200) NOT NULL  comment 'job名称',
        JOB_GROUP VARCHAR(200) NOT NULL  comment 'job组',
        DESCRIPTION VARCHAR(250) NULL  comment '描述',
        NEXT_FIRE_TIME BIGINT(13) NULL  comment '下一次触发时间',
        PREV_FIRE_TIME BIGINT(13) NULL  comment '前一次触发时间',
        PRIORITY INTEGER NULL  comment '等级',
        TRIGGER_STATE VARCHAR(16) NOT NULL  comment '触发状态',
        TRIGGER_TYPE VARCHAR(8) NOT NULL  comment '触发类型',
        START_TIME BIGINT(13) NOT NULL  comment '开始时间',
        END_TIME BIGINT(13) NULL  comment '结束时间',
        CALENDAR_NAME VARCHAR(200) NULL  comment '日程名称',
        MISFIRE_INSTR SMALLINT(2) NULL  comment '未触发实例',
        JOB_DATA BLOB NULL  comment 'job数据',
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
            REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
    );
    CREATE TABLE QRTZ_SIMPLE_TRIGGERS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        TRIGGER_NAME VARCHAR(200) NOT NULL  comment '触发器名称',
        TRIGGER_GROUP VARCHAR(200) NOT NULL  comment '触发器组',
        REPEAT_COUNT BIGINT(7) NOT NULL  comment '重复执行次数',
        REPEAT_INTERVAL BIGINT(12) NOT NULL  comment '重复执行间隔',
        TIMES_TRIGGERED BIGINT(10) NOT NULL  comment '已经触发次数',
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
            REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    );
    CREATE TABLE QRTZ_CRON_TRIGGERS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        TRIGGER_NAME VARCHAR(200) NOT NULL  comment '触发器名称',
        TRIGGER_GROUP VARCHAR(200) NOT NULL  comment '触发器组',
        CRON_EXPRESSION VARCHAR(200) NOT NULL  comment 'cron表达式',
        TIME_ZONE_ID VARCHAR(80)  comment '时区',
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
            REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    );
    CREATE TABLE QRTZ_SIMPROP_TRIGGERS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        TRIGGER_NAME VARCHAR(200) NOT NULL  comment '触发器名称',
        TRIGGER_GROUP VARCHAR(200) NOT NULL  comment '触发器组',
        STR_PROP_1 VARCHAR(512) NULL  comment '开始配置1',
        STR_PROP_2 VARCHAR(512) NULL  comment '开始配置2',
        STR_PROP_3 VARCHAR(512) NULL  comment '开始配置3',
        INT_PROP_1 INT NULL  comment 'int配置1',
        INT_PROP_2 INT NULL  comment 'int配置2',
        LONG_PROP_1 BIGINT NULL  comment 'long配置1',
        LONG_PROP_2 BIGINT NULL  comment 'long配置2',
        DEC_PROP_1 NUMERIC(13,4) NULL  comment '配置描述1',
        DEC_PROP_2 NUMERIC(13,4) NULL  comment '配置描述2',
        BOOL_PROP_1 VARCHAR(1) NULL  comment 'bool配置1',
        BOOL_PROP_2 VARCHAR(1) NULL  comment 'bool配置2',
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
            REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    );
    CREATE TABLE QRTZ_BLOB_TRIGGERS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        TRIGGER_NAME VARCHAR(200) NOT NULL  comment '触发器名称',
        TRIGGER_GROUP VARCHAR(200) NOT NULL  comment '触发器组',
        BLOB_DATA BLOB NULL  comment '数据',
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
            REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    );
    CREATE TABLE QRTZ_CALENDARS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        CALENDAR_NAME  VARCHAR(200) NOT NULL comment '日程名称',
        CALENDAR BLOB NOT NULL  comment '日程数据',
        PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
    );
    CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        TRIGGER_GROUP  VARCHAR(200) NOT NULL  comment '触发器组',
        PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
    );
    CREATE TABLE QRTZ_FIRED_TRIGGERS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        ENTRY_ID VARCHAR(95) NOT NULL  comment 'entryId',
        TRIGGER_NAME VARCHAR(200) NOT NULL  comment '触发器名称',
        TRIGGER_GROUP VARCHAR(200) NOT NULL  comment '触发器组',
        INSTANCE_NAME VARCHAR(200) NOT NULL  comment '实例名称',
        FIRED_TIME BIGINT(13) NOT NULL  comment '执行时间',
        SCHED_TIME BIGINT(13) NOT NULL  comment '定时任务时间',
        PRIORITY INTEGER NOT NULL  comment '等级',
        STATE VARCHAR(16) NOT NULL  comment '状态',
        JOB_NAME VARCHAR(200) NULL  comment 'job名称',
        JOB_GROUP VARCHAR(200) NULL  comment 'job组',
        IS_NONCONCURRENT VARCHAR(1) NULL  comment '是否异步',
        REQUESTS_RECOVERY VARCHAR(1) NULL  comment '是否请求覆盖',
        PRIMARY KEY (SCHED_NAME,ENTRY_ID)
    );
    CREATE TABLE QRTZ_SCHEDULER_STATE
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        INSTANCE_NAME VARCHAR(200) NOT NULL  comment '实例名称',
        LAST_CHECKIN_TIME BIGINT(13) NOT NULL  comment '最近检入时间',
        CHECKIN_INTERVAL BIGINT(13) NOT NULL  comment '检入间隔',
        PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
    );
    CREATE TABLE QRTZ_LOCKS
    (
        SCHED_NAME VARCHAR(120) NOT NULL  comment '定时任务名称',
        LOCK_NAME  VARCHAR(40) NOT NULL  comment 'lock名称',
        PRIMARY KEY (SCHED_NAME,LOCK_NAME)
    );
    
  • 注释原来的QuartzConfig.java

    注释掉QuartzConfig.java,使用数据库方式来配置quartz

  • 新增MyJobFactory.java和SchedulerConfig.java

    固定做法

    • MyJobFactory.java

      package com.neilxu.train.batch.config;
      
      import jakarta.annotation.Resource;
      import org.quartz.spi.TriggerFiredBundle;
      import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
      import org.springframework.scheduling.quartz.SpringBeanJobFactory;
      import org.springframework.stereotype.Component;
      
      @Component
      public class MyJobFactory extends SpringBeanJobFactory {
      
          @Resource
          private AutowireCapableBeanFactory beanFactory;
      
          /**
           * 这里覆盖了super的createJobInstance方法,对其创建出来的类再进行autowire。
           */
          @Override
          protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
              Object jobInstance = super.createJobInstance(bundle);
              beanFactory.autowireBean(jobInstance);
              return jobInstance;
          }
      }
      
    • SchedulerConfig.java
      package com.neilxu.train.batch.config;
      
      import jakarta.annotation.Resource;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.scheduling.quartz.SchedulerFactoryBean;
      
      import javax.sql.DataSource;
      import java.io.IOException;
      
      @Configuration
      public class SchedulerConfig {
      
          @Resource
          private MyJobFactory myJobFactory;
      
          @Bean
          public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("dataSource") DataSource dataSource) throws IOException {
              SchedulerFactoryBean factory = new SchedulerFactoryBean();
              factory.setDataSource(dataSource);
              factory.setJobFactory(myJobFactory);
              factory.setStartupDelay(2);
              return factory;
          }
      }
      

2.增加接口来操作quartz

  • CronJobReq.java

    package com.neilxu.train.batch.req;
    
    public class CronJobReq {
        private String group;
    
        private String name;
    
        private String description;
    
        private String cronExpression;
    
        @Override
        public String toString() {
            final StringBuffer sb = new StringBuffer("CronJobDto{");
            sb.append("cronExpression='").append(cronExpression).append('\'');
            sb.append(", group='").append(group).append('\'');
            sb.append(", name='").append(name).append('\'');
            sb.append(", description='").append(description).append('\'');
            sb.append('}');
            return sb.toString();
        }
    
        public String getGroup() {
            return group;
        }
    
        public void setGroup(String group) {
            this.group = group;
        }
    
        public String getCronExpression() {
            return cronExpression;
        }
    
        public void setCronExpression(String cronExpression) {
            this.cronExpression = cronExpression;
        }
    
        public String getDescription() {
            return description;
        }
    
        public void setDescription(String description) {
            this.description = description;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
  • CronJobResp.java

    package com.neilxu.train.batch.resp;
    
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    import com.fasterxml.jackson.annotation.JsonInclude;
    
    import java.util.Date;
    
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    public class CronJobResp {
        private String group;
    
        private String name;
    
        private String description;
    
        private String state;
    
        private String cronExpression;
    
        @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
        private Date nextFireTime;
    
        @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
        private Date preFireTime;
    
        @Override
        public String toString() {
            final StringBuffer sb = new StringBuffer("CronJobDto{");
            sb.append("cronExpression='").append(cronExpression).append('\'');
            sb.append(", group='").append(group).append('\'');
            sb.append(", name='").append(name).append('\'');
            sb.append(", description='").append(description).append('\'');
            sb.append(", state='").append(state).append('\'');
            sb.append(", nextFireTime=").append(nextFireTime);
            sb.append(", preFireTime=").append(preFireTime);
            sb.append('}');
            return sb.toString();
        }
    
        public String getGroup() {
            return group;
        }
    
        public void setGroup(String group) {
            this.group = group;
        }
    
        public String getCronExpression() {
            return cronExpression;
        }
    
        public void setCronExpression(String cronExpression) {
            this.cronExpression = cronExpression;
        }
    
        public String getDescription() {
            return description;
        }
    
        public void setDescription(String description) {
            this.description = description;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Date getNextFireTime() {
            return nextFireTime;
        }
    
        public void setNextFireTime(Date nextFireTime) {
            this.nextFireTime = nextFireTime;
        }
    
        public Date getPreFireTime() {
            return preFireTime;
        }
    
        public void setPreFireTime(Date preFireTime) {
            this.preFireTime = preFireTime;
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    }
    
  • JobController.java

    package com.neilxu.train.batch.controller;
    
    import com.neilxu.train.batch.req.CronJobReq;
    import com.neilxu.train.batch.resp.CronJobResp;
    import com.neilxu.train.common.resp.CommonResp;
    import org.quartz.*;
    import org.quartz.impl.matchers.GroupMatcher;
    import org.quartz.impl.triggers.CronTriggerImpl;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.quartz.SchedulerFactoryBean;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    
    @RestController
    @RequestMapping(value = "/admin/job")
    public class JobController {
    
        private static Logger LOG = LoggerFactory.getLogger(JobController.class);
    
        @Autowired
        private SchedulerFactoryBean schedulerFactoryBean;
    
        @RequestMapping(value = "/add")
        public CommonResp add(@RequestBody CronJobReq cronJobReq) {
            String jobClassName = cronJobReq.getName();
            String jobGroupName = cronJobReq.getGroup();
            String cronExpression = cronJobReq.getCronExpression();
            String description = cronJobReq.getDescription();
            LOG.info("创建定时任务开始:{},{},{},{}", jobClassName, jobGroupName, cronExpression, description);
            CommonResp commonResp = new CommonResp();
    
            try {
                // 通过SchedulerFactory获取一个调度器实例
                Scheduler sched = schedulerFactoryBean.getScheduler();
    
                // 启动调度器
                sched.start();
    
                //构建job信息
                JobDetail jobDetail = JobBuilder.newJob((Class<? extends Job>) Class.forName(jobClassName)).withIdentity(jobClassName, jobGroupName).build();
    
                //表达式调度构建器(即任务执行的时间)
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
    
                //按新的cronExpression表达式构建一个新的trigger
                CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName).withDescription(description).withSchedule(scheduleBuilder).build();
    
                sched.scheduleJob(jobDetail, trigger);
    
            } catch (SchedulerException e) {
                LOG.error("创建定时任务失败:" + e);
                commonResp.setSuccess(false);
                commonResp.setMessage("创建定时任务失败:调度异常");
            } catch (ClassNotFoundException e) {
                LOG.error("创建定时任务失败:" + e);
                commonResp.setSuccess(false);
                commonResp.setMessage("创建定时任务失败:任务类不存在");
            }
            LOG.info("创建定时任务结束:{}", commonResp);
            return commonResp;
        }
    
        @RequestMapping(value = "/pause")
        public CommonResp pause(@RequestBody CronJobReq cronJobReq) {
            String jobClassName = cronJobReq.getName();
            String jobGroupName = cronJobReq.getGroup();
            LOG.info("暂停定时任务开始:{},{}", jobClassName, jobGroupName);
            CommonResp commonResp = new CommonResp();
            try {
                Scheduler sched = schedulerFactoryBean.getScheduler();
                sched.pauseJob(JobKey.jobKey(jobClassName, jobGroupName));
            } catch (SchedulerException e) {
                LOG.error("暂停定时任务失败:" + e);
                commonResp.setSuccess(false);
                commonResp.setMessage("暂停定时任务失败:调度异常");
            }
            LOG.info("暂停定时任务结束:{}", commonResp);
            return commonResp;
        }
    
        @RequestMapping(value = "/resume")
        public CommonResp resume(@RequestBody CronJobReq cronJobReq) {
            String jobClassName = cronJobReq.getName();
            String jobGroupName = cronJobReq.getGroup();
            LOG.info("重启定时任务开始:{},{}", jobClassName, jobGroupName);
            CommonResp commonResp = new CommonResp();
            try {
                Scheduler sched = schedulerFactoryBean.getScheduler();
                sched.resumeJob(JobKey.jobKey(jobClassName, jobGroupName));
            } catch (SchedulerException e) {
                LOG.error("重启定时任务失败:" + e);
                commonResp.setSuccess(false);
                commonResp.setMessage("重启定时任务失败:调度异常");
            }
            LOG.info("重启定时任务结束:{}", commonResp);
            return commonResp;
        }
    
        @RequestMapping(value = "/reschedule")
        public CommonResp reschedule(@RequestBody CronJobReq cronJobReq) {
            String jobClassName = cronJobReq.getName();
            String jobGroupName = cronJobReq.getGroup();
            String cronExpression = cronJobReq.getCronExpression();
            String description = cronJobReq.getDescription();
            LOG.info("更新定时任务开始:{},{},{},{}", jobClassName, jobGroupName, cronExpression, description);
            CommonResp commonResp = new CommonResp();
            try {
                Scheduler scheduler = schedulerFactoryBean.getScheduler();
                TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);
                // 表达式调度构建器
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
                CronTriggerImpl trigger1 = (CronTriggerImpl) scheduler.getTrigger(triggerKey);
                trigger1.setStartTime(new Date()); // 重新设置开始时间
                CronTrigger trigger = trigger1;
    
                // 按新的cronExpression表达式重新构建trigger
                trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withDescription(description).withSchedule(scheduleBuilder).build();
    
                // 按新的trigger重新设置job执行
                scheduler.rescheduleJob(triggerKey, trigger);
            } catch (Exception e) {
                LOG.error("更新定时任务失败:" + e);
                commonResp.setSuccess(false);
                commonResp.setMessage("更新定时任务失败:调度异常");
            }
            LOG.info("更新定时任务结束:{}", commonResp);
            return commonResp;
        }
    
        @RequestMapping(value = "/delete")
        public CommonResp delete(@RequestBody CronJobReq cronJobReq) {
            String jobClassName = cronJobReq.getName();
            String jobGroupName = cronJobReq.getGroup();
            LOG.info("删除定时任务开始:{},{}", jobClassName, jobGroupName);
            CommonResp commonResp = new CommonResp();
            try {
                Scheduler scheduler = schedulerFactoryBean.getScheduler();
                scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName));
                scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName));
                scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName));
            } catch (SchedulerException e) {
                LOG.error("删除定时任务失败:" + e);
                commonResp.setSuccess(false);
                commonResp.setMessage("删除定时任务失败:调度异常");
            }
            LOG.info("删除定时任务结束:{}", commonResp);
            return commonResp;
        }
    
        @RequestMapping(value="/query")
        public CommonResp query() {
            LOG.info("查看所有定时任务开始");
            CommonResp commonResp = new CommonResp();
            List<CronJobResp> cronJobDtoList = new ArrayList();
            try {
                Scheduler scheduler = schedulerFactoryBean.getScheduler();
                for (String groupName : scheduler.getJobGroupNames()) {
                    for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
                        CronJobResp cronJobResp = new CronJobResp();
                        cronJobResp.setName(jobKey.getName());
                        cronJobResp.setGroup(jobKey.getGroup());
    
                        //get job's trigger
                        List<Trigger> triggers = (List<Trigger>) scheduler.getTriggersOfJob(jobKey);
                        CronTrigger cronTrigger = (CronTrigger) triggers.get(0);
                        cronJobResp.setNextFireTime(cronTrigger.getNextFireTime());
                        cronJobResp.setPreFireTime(cronTrigger.getPreviousFireTime());
                        cronJobResp.setCronExpression(cronTrigger.getCronExpression());
                        cronJobResp.setDescription(cronTrigger.getDescription());
                        Trigger.TriggerState triggerState = scheduler.getTriggerState(cronTrigger.getKey());
                        cronJobResp.setState(triggerState.name());
    
                        cronJobDtoList.add(cronJobResp);
                    }
    
                }
            } catch (SchedulerException e) {
                LOG.error("查看定时任务失败:" + e);
                commonResp.setSuccess(false);
                commonResp.setMessage("查看定时任务失败:调度异常");
            }
            commonResp.setContent(cronJobDtoList);
            LOG.info("查看定时任务结束:{}", commonResp);
            return commonResp;
        }
    
    }
    

3.测试

http/batch-job.http

POST http://localhost:8000/batch/admin/job/add
Content-Type: application/json

{
  "name": "com.neilxu.train.batch.job.TestJob",
  "jobGroupName": "default",
  "cronExpression": "*/2 * * * * ?",
  "desc": "test job"
}

###

GET http://localhost:8000/batch/admin/job/query

###

POST http://localhost:8000/batch/admin/job/pause
Content-Type: application/json

{
  "name": "com.neilxu.train.batch.job.TestJob",
  "jobGroupName": "default"
}

###

POST http://localhost:8000/batch/admin/job/resume
Content-Type: application/json

{
  "name": "com.neilxu.train.batch.job.TestJob",
  "jobGroupName": "default"
}

###

POST http://localhost:8000/batch/admin/job/reschedule
Content-Type: application/json

{
  "name": "com.neilxu.train.batch.job.TestJob",
  "jobGroupName": "default",
  "cronExpression": "*/5 * * * * ?",
  "desc": "test job"
}

###

POST http://localhost:8000/batch/admin/job/delete
Content-Type: application/json

{
  "name": "com.neilxu.train.batch.job.TestJob",
  "jobGroupName": "default"
}

依次测试,结果正常

八、通过控台界面操作定时任务

1.新增控台页面

  • job.vue

    <template>
      <div class="job">
        <p>
          <a-button type="primary" @click="handleAdd()">
            新增
          </a-button>&nbsp;
          <a-button type="primary" @click="handleQuery()">
            刷新
          </a-button>
        </p>
        <a-table :dataSource="jobs"
                 :columns="columns"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
              <a-space>
                <a-popconfirm
                    title="确定重启?"
                    ok-text="是"
                    cancel-text="否"
                    @confirm="handleResume(record)"
                >
                  <a-button v-show="record.state === 'PAUSED' || record.state === 'ERROR'" type="primary" size="small">
                    重启
                  </a-button>
                </a-popconfirm>
                <a-popconfirm
                    title="确定暂停?"
                    ok-text="是"
                    cancel-text="否"
                    @confirm="handlePause(record)"
                >
                  <a-button v-show="record.state === 'NORMAL' || record.state === 'BLOCKED'" type="primary" size="small">
                    暂停
                  </a-button>
                </a-popconfirm>
                <a-button type="primary" @click="handleEdit(record)" size="small">
                  编辑
                </a-button>
                <a-popconfirm
                    title="删除后不可恢复,确认删除?"
                    ok-text="是"
                    cancel-text="否"
                    @confirm="handleDelete(record)"
                >
                  <a-button type="danger" size="small">
                    删除
                  </a-button>
                </a-popconfirm>
              </a-space>
            </template>
          </template>
        </a-table>
    
        <a-modal
            title="用户"
            v-model:visible="modalVisible"
            :confirm-loading="modalLoading"
            @ok="handleModalOk"
        >
          <a-form :model="job" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
            <a-form-item label="类名">
              <a-input v-model:value="job.name" />
            </a-form-item>
            <a-form-item label="描述">
              <a-input v-model:value="job.description" />
            </a-form-item>
            <a-form-item label="分组">
              <a-input v-model:value="job.group" :disabled="!!job.state"/>
            </a-form-item>
            <a-form-item label="表达式">
              <a-input v-model:value="job.cronExpression" />
              <div class="ant-alert ant-alert-success">
                每5秒执行一次:0/5 * * * * ?
                <br>
                每5分钟执行一次:* 0/5 * * * ?
              </div>
            </a-form-item>
          </a-form>
        </a-modal>
      </div>
    </template>
    
    <script>
    import { defineComponent, onMounted, ref } from 'vue';
    import axios from 'axios';
    import { notification } from 'ant-design-vue';
    
    export default defineComponent({
      name: 'batch-job-view',
      setup () {
        const jobs = ref();
        const loading = ref();
    
        const columns = [{
          title: '分组',
          dataIndex: 'group',
        }, {
          title: '类名',
          dataIndex: 'name',
        }, {
          title: '描述',
          dataIndex: 'description',
        }, {
          title: '状态',
          dataIndex: 'state',
        }, {
          title: '表达式',
          dataIndex: 'cronExpression',
        }, {
          title: '上次时间',
          dataIndex: 'preFireTime',
        }, {
          title: '下次时间',
          dataIndex: 'nextFireTime',
        }, {
          title: '操作',
          dataIndex: 'operation'
        }];
    
        const handleQuery = () => {
          loading.value = true;
          jobs.value = [];
          axios.get('/batch/admin/job/query').then((response) => {
            loading.value = false;
            const data = response.data;
            if (data.success) {
              jobs.value = data.content;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        // -------- 表单 ---------
        const job = ref();
        job.value = {};
        const modalVisible = ref(false);
        const modalLoading = ref(false);
        const handleModalOk = () => {
          modalLoading.value = true;
          let url = "add";
          if (job.value.state) {
            url = "reschedule";
          }
          axios.post('/batch/admin/job/' + url, job.value).then((response) => {
            modalLoading.value = false;
            const data = response.data;
            if (data.success) {
              modalVisible.value = false;
              notification.success({description: "保存成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 新增
         */
        const handleAdd = () => {
          modalVisible.value = true;
          job.value = {
            group: 'DEFAULT'
          };
        };
    
        /**
         * 编辑
         */
        const handleEdit = (record) => {
          modalVisible.value = true;
          job.value = Tool.copy(record);
        };
    
        /**
         * 删除
         */
        const handleDelete = (record) => {
          axios.post('/batch/admin/job/delete', {
            name: record.name,
            group: record.group
          }).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 暂停
         */
        const handlePause = (record) => {
          axios.post('/batch/admin/job/pause', {
            name: record.name,
            group: record.group
          }).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "暂停成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 重启
         */
        const handleResume = (record) => {
          axios.post('/batch/admin/job/reschedule', record).then((response) => {
            modalLoading.value = false;
            const data = response.data;
            if (data.success) {
              modalVisible.value = false;
              notification.success({description: "重启成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const getEnumValue = (key, obj) => {
          return Tool.getEnumValue(key, obj);
        };
    
        onMounted(() => {
          console.log('index mounted!');
          handleQuery();
        });
    
        return {
          columns,
          jobs,
          loading,
          handleQuery,
    
          handleAdd,
          handleEdit,
          handleDelete,
          handleResume,
          handlePause,
    
          job,
          modalVisible,
          modalLoading,
          handleModalOk,
    
          getEnumValue
        };
      }
    })
    </script>
    
    <style scoped>
    </style>
    
  • 路由、侧边栏

    import { createRouter, createWebHistory } from 'vue-router'
    
    const routes = [{
      path: '/',
      component: () => import('../views/main.vue'),
      children: [{
        path: 'welcome',
        component: () => import('../views/main/welcome.vue'),
      }, {
        path: 'about',
        component: () => import('../views/main/about.vue'),
      }, {
        path: 'station',
        component: () => import('../views/main/station.vue'),
      }, {
        path: 'train',
        component: () => import('../views/main/train.vue'),
      }, {
        path: 'train-station',
        component: () => import('../views/main/train-station.vue'),
      }, {
        path: 'train-carriage',
        component: () => import('../views/main/train-carriage.vue'),
      }, {
        path: 'train-seat',
        component: () => import('../views/main/train-seat.vue'),
      }, {
        path: 'batch/job',
        name: 'batch/job',
        component: () => import('../views/main/job.vue')
      }]
    }, {
      path: '',
      redirect: '/welcome'
    }];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL),
      routes
    })
    
    export default router
    
    <template>
      <a-layout-sider width="200" style="background: #fff">
        <a-menu
            v-model:selectedKeys="selectedKeys"
            mode="inline"
            :style="{ height: '100%', borderRight: 0 }"
        >
          <a-menu-item key="/welcome">
            <router-link to="/welcome">
              <coffee-outlined /> &nbsp; 欢迎
            </router-link>
          </a-menu-item>
          <a-menu-item key="/about">
            <router-link to="/about">
              <user-outlined /> &nbsp; 关于
            </router-link>
          </a-menu-item>
          <a-menu-item key="/batch/job">
            <router-link to="/batch/job">
              <MenuUnfoldOutlined /> &nbsp; 任务管理
            </router-link>
          </a-menu-item>
          <a-menu-item key="/station">
            <router-link to="/station">
              <user-outlined /> &nbsp; 车站管理
            </router-link>
          </a-menu-item>
          <a-menu-item key="/train">
            <router-link to="/train">
              <user-outlined /> &nbsp; 火车管理
            </router-link>
          </a-menu-item>
          <a-menu-item key="/train-station">
            <router-link to="/train-station">
              <user-outlined /> &nbsp; 火车车站
            </router-link>
          </a-menu-item>
          <a-menu-item key="/train-carriage">
            <router-link to="/train-carriage">
              <user-outlined /> &nbsp; 火车车厢
            </router-link>
          </a-menu-item>
          <a-menu-item key="/train-seat">
            <router-link to="/train-seat">
              <user-outlined /> &nbsp; 火车座位
            </router-link>
          </a-menu-item>
        </a-menu>
      </a-layout-sider>
    </template>
    
    <script>
    import {defineComponent, ref, watch} from 'vue';
    import router from "@/router";
    
    export default defineComponent({
      name: "the-sider-view",
      setup() {
        const selectedKeys = ref([]);
    
        watch(() => router.currentRoute.value.path, (newValue) => {
          console.log('watch', newValue);
          selectedKeys.value = [];
          selectedKeys.value.push(newValue);
        }, {immediate: true});
        return {
          selectedKeys
        };
      },
    });
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>
    
    </style>
    
  • 测试

在这里插入图片描述

2.对菜单作分类,改为两级菜单;路由也改为多级路由

  • the-sider.vue

    <template>
      <a-layout-sider width="200" style="background: #fff">
        <a-menu
            v-model:selectedKeys="selectedKeys"
            :openKeys="['batch', 'base']"
            mode="inline"
            :style="{ height: '100%', borderRight: 0 }"
        >
          <a-menu-item key="/welcome">
            <router-link to="/welcome">
              <coffee-outlined /> &nbsp; 欢迎
            </router-link>
          </a-menu-item>
          <a-menu-item key="/about">
            <router-link to="/about">
              <user-outlined /> &nbsp; 关于
            </router-link>
          </a-menu-item>
          <a-sub-menu key="batch">
            <template #title>
              <span>
                <UnorderedListOutlined />
                跑批管理
              </span>
            </template>
            <a-menu-item key="/batch/job">
              <router-link to="/batch/job">
                <MenuUnfoldOutlined /> &nbsp; 任务管理
              </router-link>
            </a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="base">
            <template #title>
              <span>
                <UnorderedListOutlined />
                基础数据
              </span>
            </template>
            <a-menu-item key="/base/station">
              <router-link to="/base/station">
                <user-outlined /> &nbsp; 车站管理
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train">
              <router-link to="/base/train">
                <user-outlined /> &nbsp; 火车管理
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-station">
              <router-link to="/base/train-station">
                <user-outlined /> &nbsp; 火车车站
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-carriage">
              <router-link to="/base/train-carriage">
                <user-outlined /> &nbsp; 火车车厢
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-seat">
              <router-link to="/base/train-seat">
                <user-outlined /> &nbsp; 火车座位
              </router-link>
            </a-menu-item>
          </a-sub-menu>
        </a-menu>
      </a-layout-sider>
    </template>
    
    <script>
    import {defineComponent, ref, watch} from 'vue';
    import router from "@/router";
    
    export default defineComponent({
      name: "the-sider-view",
      setup() {
        const selectedKeys = ref([]);
    
        watch(() => router.currentRoute.value.path, (newValue) => {
          console.log('watch', newValue);
          selectedKeys.value = [];
          selectedKeys.value.push(newValue);
        }, {immediate: true});
        return {
          selectedKeys
        };
      },
    });
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>
    
    </style>
    
  • /router/index.js

    import { createRouter, createWebHistory } from 'vue-router'
    
    const routes = [{
      path: '/',
      component: () => import('../views/main.vue'),
      children: [{
        path: 'welcome',
        component: () => import('../views/main/welcome.vue'),
      }, {
        path: 'about',
        component: () => import('../views/main/about.vue'),
      }, {
        path: 'base/',
        children: [{
          path: 'station',
          component: () => import('../views/main/station.vue'),
        }, {
          path: 'train',
          component: () => import('../views/main/train.vue'),
        }, {
          path: 'train-station',
          component: () => import('../views/main/train-station.vue'),
        }, {
          path: 'train-carriage',
          component: () => import('../views/main/train-carriage.vue'),
        }, {
          path: 'train-seat',
          component: () => import('../views/main/train-seat.vue'),
        }]
      }, {
        path: 'batch/',
        children: [{
          path: 'job',
          component: () => import('../views/main/job.vue')
        }]
      }]
    }, {
      path: '',
      redirect: '/welcome'
    }];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL),
      routes
    })
    
    export default router
    
  • 测试

在这里插入图片描述

3.修改vue文件位置,保持层级关系和路由一致。

修改方法:只修改移动文件位置,路由index.ts自动会修改为新位置

在这里插入图片描述

九、增加任务手工补偿功能

增加手动执行定时任务的功能

  • JobController.java

    @RequestMapping(value = "/run")
    public CommonResp<Object> run(@RequestBody CronJobReq cronJobReq) throws SchedulerException {
        String jobClassName = cronJobReq.getName();
        String jobGroupName = cronJobReq.getGroup();
        LOG.info("手动执行任务开始:{}, {}", jobClassName, jobGroupName);
        schedulerFactoryBean.getScheduler().triggerJob(JobKey.jobKey(jobClassName, jobGroupName));
        return new CommonResp<>();
    }
    
  • job.vue

    <template>
      <div class="job">
        <p>
          <a-button type="primary" @click="handleAdd()">
            新增
          </a-button>&nbsp;
          <a-button type="primary" @click="handleQuery()">
            刷新
          </a-button>
        </p>
        <a-table :dataSource="jobs"
                 :columns="columns"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
              <a-space>
                <a-popconfirm
                    title="手动执行会立即执行一次,确定执行?"
                    ok-text="是"
                    cancel-text="否"
                    @confirm="handleRun(record)"
                >
                  <a-button type="primary" size="small">
                    手动执行
                  </a-button>
                </a-popconfirm>
                <a-popconfirm
                    title="确定重启?"
                    ok-text="是"
                    cancel-text="否"
                    @confirm="handleResume(record)"
                >
                  <a-button v-show="record.state === 'PAUSED' || record.state === 'ERROR'" type="primary" size="small">
                    重启
                  </a-button>
                </a-popconfirm>
                <a-popconfirm
                    title="确定暂停?"
                    ok-text="是"
                    cancel-text="否"
                    @confirm="handlePause(record)"
                >
                  <a-button v-show="record.state === 'NORMAL' || record.state === 'BLOCKED'" type="primary" size="small">
                    暂停
                  </a-button>
                </a-popconfirm>
                <a-button type="primary" @click="handleEdit(record)" size="small">
                  编辑
                </a-button>
                <a-popconfirm
                    title="删除后不可恢复,确认删除?"
                    ok-text="是"
                    cancel-text="否"
                    @confirm="handleDelete(record)"
                >
                  <a-button type="danger" size="small">
                    删除
                  </a-button>
                </a-popconfirm>
              </a-space>
            </template>
          </template>
        </a-table>
    
        <a-modal
            title="用户"
            v-model:visible="modalVisible"
            :confirm-loading="modalLoading"
            @ok="handleModalOk"
        >
          <a-form :model="job" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
            <a-form-item label="类名">
              <a-input v-model:value="job.name" />
            </a-form-item>
            <a-form-item label="描述">
              <a-input v-model:value="job.description" />
            </a-form-item>
            <a-form-item label="分组">
              <a-input v-model:value="job.group" :disabled="!!job.state"/>
            </a-form-item>
            <a-form-item label="表达式">
              <a-input v-model:value="job.cronExpression" />
              <div class="ant-alert ant-alert-success">
                每5秒执行一次:0/5 * * * * ?
                <br>
                每5分钟执行一次:* 0/5 * * * ?
              </div>
            </a-form-item>
          </a-form>
        </a-modal>
      </div>
    </template>
    
    <script>
    import { defineComponent, onMounted, ref } from 'vue';
    import axios from 'axios';
    import { notification } from 'ant-design-vue';
    
    export default defineComponent({
      name: 'batch-job-view',
      setup () {
        const jobs = ref();
        const loading = ref();
    
        const columns = [{
          title: '分组',
          dataIndex: 'group',
        }, {
          title: '类名',
          dataIndex: 'name',
        }, {
          title: '描述',
          dataIndex: 'description',
        }, {
          title: '状态',
          dataIndex: 'state',
        }, {
          title: '表达式',
          dataIndex: 'cronExpression',
        }, {
          title: '上次时间',
          dataIndex: 'preFireTime',
        }, {
          title: '下次时间',
          dataIndex: 'nextFireTime',
        }, {
          title: '操作',
          dataIndex: 'operation'
        }];
    
        const handleQuery = () => {
          loading.value = true;
          jobs.value = [];
          axios.get('/batch/admin/job/query').then((response) => {
            loading.value = false;
            const data = response.data;
            if (data.success) {
              jobs.value = data.content;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        // -------- 表单 ---------
        const job = ref();
        job.value = {};
        const modalVisible = ref(false);
        const modalLoading = ref(false);
        const handleModalOk = () => {
          modalLoading.value = true;
          let url = "add";
          if (job.value.state) {
            url = "reschedule";
          }
          axios.post('/batch/admin/job/' + url, job.value).then((response) => {
            modalLoading.value = false;
            const data = response.data;
            if (data.success) {
              modalVisible.value = false;
              notification.success({description: "保存成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 新增
         */
        const handleAdd = () => {
          modalVisible.value = true;
          job.value = {
            group: 'DEFAULT'
          };
        };
    
        /**
         * 编辑
         */
        const handleEdit = (record) => {
          modalVisible.value = true;
          job.value = Tool.copy(record);
        };
    
        /**
         * 删除
         */
        const handleDelete = (record) => {
          axios.post('/batch/admin/job/delete', {
            name: record.name,
            group: record.group
          }).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 暂停
         */
        const handlePause = (record) => {
          axios.post('/batch/admin/job/pause', {
            name: record.name,
            group: record.group
          }).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "暂停成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 重启
         */
        const handleResume = (record) => {
          axios.post('/batch/admin/job/reschedule', record).then((response) => {
            modalLoading.value = false;
            const data = response.data;
            if (data.success) {
              modalVisible.value = false;
              notification.success({description: "重启成功!"});
              handleQuery();
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        /**
         * 手动执行
         */
        const handleRun = (record) => {
          axios.post('/batch/admin/job/run', record).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "手动执行成功!"});
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const getEnumValue = (key, obj) => {
          return Tool.getEnumValue(key, obj);
        };
    
        onMounted(() => {
          console.log('index mounted!');
          handleQuery();
        });
    
        return {
          columns,
          jobs,
          loading,
          handleQuery,
    
          handleAdd,
          handleEdit,
          handleDelete,
          handleResume,
          handlePause,
          job,
          modalVisible,
          modalLoading,
          handleModalOk,
          getEnumValue,
          handleRun
        };
      }
    })
    </script>
    
    <style scoped>
    </style>
    
  • 测试

在这里插入图片描述

执行成功

十、演示多节点场景中quartz的调度情况

  • 创建18003节点

在这里插入图片描述

启动两个应用

在这里插入图片描述

  • 新增job

    TestJob2.java

    package com.neilxu.train.batch.job;
    
    import org.quartz.DisallowConcurrentExecution;
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    
    @DisallowConcurrentExecution
    public class TestJob2 implements Job {
    
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            System.out.println("TestJob222 TEST开始");
    //        try {
    //            Thread.sleep(3000);
    //        } catch (InterruptedException e) {
    //            e.printStackTrace();
    //        }
            System.out.println("TestJob222 TEST结束");
        }
    }
    

    TestJob3.java

    package com.neilxu.train.batch.job;
    
    import org.quartz.DisallowConcurrentExecution;
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    
    @DisallowConcurrentExecution
    public class TestJob3 implements Job {
    
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            System.out.println("TestJob333 TEST开始");
    //        try {
    //            Thread.sleep(3000);
    //        } catch (InterruptedException e) {
    //            e.printStackTrace();
    //        }
            System.out.println("TestJob333 TEST结束");
        }
    }
    
  • 测试

在这里插入图片描述

在这里插入图片描述

效果:多个节点轮询执行

十一、通过火车基础数据生成每日火车数据

使用Quartz来实现如下的内容

在这里插入图片描述

1.快速生成每日车次数据管理功能

  • 新增每日车次表

    sql/business.sql

    drop table if exists `daily_train`;
    create table `daily_train` (
      `id` bigint not null comment 'id',
      `date` date not null comment '日期',
      `code` varchar(20) not null comment '车次编号',
      `type` char(1) not null comment '车次类型|枚举[TrainTypeEnum]',
      `start` varchar(20) not null comment '始发站',
      `start_pinyin` varchar(50) not null comment '始发站拼音',
      `start_time` time not null comment '出发时间',
      `end` varchar(20) not null comment '终点站',
      `end_pinyin` varchar(50) not null comment '终点站拼音',
      `end_time` time not null comment '到站时间',
      `create_time` datetime(3) comment '新增时间',
      `update_time` datetime(3) comment '修改时间',
      primary key (`id`),
      unique key `date_code_unique` (`date`, `code`)
    ) engine=innodb default charset=utf8mb4 comment='每日车次';
    
  • 修改generator-config-business.xml

            <!--<table tableName="station" domainObjectName="Station"/>-->
            <!--<table tableName="train" domainObjectName="Train"/>-->
    <!--        <table tableName="train_station" domainObjectName="TrainStation"/>-->
    <!--        <table tableName="train_carriage" domainObjectName="TrainCarriage"/>-->
    <!--        <table tableName="train_seat" domainObjectName="TrainSeat"/>-->
            <table tableName="daily_train" domainObjectName="DailyTrain"/>
    
  • 修改ServerGenerator.java

    package com.neilxu.train.generator.gen;
    
    import com.neilxu.train.generator.util.DbUtil;
    import com.neilxu.train.generator.util.Field;
    import com.neilxu.train.generator.util.FreemarkerUtil;
    import freemarker.template.TemplateException;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.*;
    
    public class ServerGenerator {
        static boolean readOnly = true;
    //    static String vuePath = "web/src/views/main/";
        static String vuePath = "admin/src/views/main/";
        static String serverPath = "[module]/src/main/java/com/neilxu/train/[module]/";
        static String pomPath = "generator\\pom.xml";
        static String module = "";
        static {
            new File(serverPath).mkdirs();
        }
    
        public static void main(String[] args) throws Exception {
            // 获取mybatis-generator
            String generatorPath = getGeneratorPath();
            // 比如generator-config-member.xml,得到module = member
            module = generatorPath.replace("src/main/resources/generator-config-", "").replace(".xml", "");
            System.out.println("module: " + module);
            serverPath = serverPath.replace("[module]", module);
            // new File(servicePath).mkdirs();
            System.out.println("servicePath: " + serverPath);
    
            // 读取table节点
            Document document = new SAXReader().read("generator/" + generatorPath);
            Node table = document.selectSingleNode("//table");
            System.out.println(table);
            Node tableName = table.selectSingleNode("@tableName");
            Node domainObjectName = table.selectSingleNode("@domainObjectName");
            System.out.println(tableName.getText() + "/" + domainObjectName.getText());
    
            // 为DbUtil设置数据源
            Node connectionURL = document.selectSingleNode("//@connectionURL");
            Node userId = document.selectSingleNode("//@userId");
            Node password = document.selectSingleNode("//@password");
            System.out.println("url: " + connectionURL.getText());
            System.out.println("user: " + userId.getText());
            System.out.println("password: " + password.getText());
            DbUtil.url = connectionURL.getText();
            DbUtil.user = userId.getText();
            DbUtil.password = password.getText();
    
            // 示例:表名 neilxu_test
            // Domain = neilxuTest
            String Domain = domainObjectName.getText();
            // domain = neilxuTest
            String domain = Domain.substring(0, 1).toLowerCase() + Domain.substring(1);
            // do_main = neilxu-test
            String do_main = tableName.getText().replaceAll("_", "-");
            // 表中文名
            String tableNameCn = DbUtil.getTableComment(tableName.getText());
            List<Field> fieldList = DbUtil.getColumnByTableName(tableName.getText());
            Set<String> typeSet = getJavaTypes(fieldList);
    
            // 组装参数
            Map<String, Object> param = new HashMap<>();
            param.put("module", module);
            param.put("Domain", Domain);
            param.put("domain", domain);
            param.put("do_main", do_main);
            param.put("tableNameCn", tableNameCn);
            param.put("fieldList", fieldList);
            param.put("typeSet", typeSet);
            param.put("readOnly", readOnly);
            System.out.println("组装参数:" + param);
    
            gen(Domain, param, "service", "service");
            gen(Domain, param, "controller/admin", "adminController");
    //        gen(Domain, param, "controller", "controller");
            gen(Domain, param, "req", "saveReq");
            gen(Domain, param, "req", "queryReq");
            gen(Domain, param, "resp", "queryResp");
            genVue(do_main, param);
        }
    
        private static void gen(String Domain, Map<String, Object> param, String packageName, String target) throws IOException, TemplateException {
            FreemarkerUtil.initConfig(target + ".ftl");
            String toPath = serverPath + packageName + "/";
            new File(toPath).mkdirs();
            String Target = target.substring(0, 1).toUpperCase() + target.substring(1);
            String fileName = toPath + Domain + Target + ".java";
            System.out.println("开始生成:" + fileName);
            FreemarkerUtil.generator(fileName, param);
        }
    
        private static void genVue(String do_main, Map<String, Object> param) throws IOException, TemplateException {
            FreemarkerUtil.initConfig("vue.ftl");
            new File(vuePath + module).mkdirs();
            String fileName = vuePath + module + "/" + do_main + ".vue";
            System.out.println("开始生成:" + fileName);
            FreemarkerUtil.generator(fileName, param);
        }
    
        private static String getGeneratorPath() throws DocumentException {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
            return node.getText();
        }
    
        /**
         * 获取所有的Java类型,使用Set去重
         */
        private static Set<String> getJavaTypes(List<Field> fieldList) {
            Set<String> set = new HashSet<>();
            for (int i = 0; i < fieldList.size(); i++) {
                Field field = fieldList.get(i);
                set.add(field.getJavaType());
            }
            return set;
        }
    }
    
  • 修改generator/pom.xml

                    <configuration>
                        <!--<configurationFile>src/main/resources/generator-config-member.xml</configurationFile>-->
                        <configurationFile>src/main/resources/generator-config-business.xml</configurationFile>
    <!--                    <configurationFile>src/main/resources/generator-config-batch.xml</configurationFile>-->
                        <overwrite>true</overwrite>
                        <verbose>true</verbose>
                    </configuration>
    
  • 生成持久层代码、前后端代码

在这里插入图片描述

  • 修改路由、侧边栏

    import { createRouter, createWebHistory } from 'vue-router'
    
    const routes = [{
      path: '/',
      component: () => import('../views/main.vue'),
      children: [{
        path: 'welcome',
        component: () => import('../views/main/welcome.vue'),
      }, {
        path: 'about',
        component: () => import('../views/main/about.vue'),
      }, {
        path: 'base/',
        children: [{
          path: 'station',
          component: () => import('../views/main/base/station.vue'),
        }, {
          path: 'train',
          component: () => import('../views/main/base/train.vue'),
        }, {
          path: 'train-station',
          component: () => import('../views/main/base/train-station.vue'),
        }, {
          path: 'train-carriage',
          component: () => import('../views/main/base/train-carriage.vue'),
        }, {
          path: 'train-seat',
          component: () => import('../views/main/base/train-seat.vue'),
        }]
      }, {
        path: 'business/',
        children: [{
          path: 'daily-train',
          component: () => import('../views/main/business/daily-train.vue'),
        }]
      }, {
        path: 'batch/',
        children: [{
          path: 'job',
          component: () => import('../views/main/batch/job.vue')
        }]
      }]
    }, {
      path: '',
      redirect: '/welcome'
    }];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL),
      routes
    })
    
    export default router
    
    <template>
      <a-layout-sider width="200" style="background: #fff">
        <a-menu
            v-model:selectedKeys="selectedKeys"
            :openKeys="['batch', 'base', 'business']"
            mode="inline"
            :style="{ height: '100%', borderRight: 0 }"
        >
          <a-menu-item key="/welcome">
            <router-link to="/welcome">
              <coffee-outlined /> &nbsp; 欢迎
            </router-link>
          </a-menu-item>
          <a-menu-item key="/about">
            <router-link to="/about">
              <user-outlined /> &nbsp; 关于
            </router-link>
          </a-menu-item>
          <a-sub-menu key="business">
            <template #title>
              <span>
                <UnorderedListOutlined />
                业务管理
              </span>
            </template>
            <a-menu-item key="/business/daily-train">
              <router-link to="/business/daily-train">
                <user-outlined /> &nbsp; 每日车次
              </router-link>
            </a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="base">
            <template #title>
              <span>
                <UnorderedListOutlined />
                基础数据
              </span>
            </template>
            <a-menu-item key="/base/station">
              <router-link to="/base/station">
                <user-outlined /> &nbsp; 车站管理
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train">
              <router-link to="/base/train">
                <user-outlined /> &nbsp; 火车管理
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-station">
              <router-link to="/base/train-station">
                <user-outlined /> &nbsp; 火车车站
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-carriage">
              <router-link to="/base/train-carriage">
                <user-outlined /> &nbsp; 火车车厢
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-seat">
              <router-link to="/base/train-seat">
                <user-outlined /> &nbsp; 火车座位
              </router-link>
            </a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="batch">
            <template #title>
              <span>
                <UnorderedListOutlined />
                跑批管理
              </span>
            </template>
            <a-menu-item key="/batch/job">
              <router-link to="/batch/job">
                <MenuUnfoldOutlined /> &nbsp; 任务管理
              </router-link>
            </a-menu-item>
          </a-sub-menu>
        </a-menu>
      </a-layout-sider>
    </template>
    
    <script>
    import {defineComponent, ref, watch} from 'vue';
    import router from "@/router";
    
    export default defineComponent({
      name: "the-sider-view",
      setup() {
        const selectedKeys = ref([]);
    
        watch(() => router.currentRoute.value.path, (newValue) => {
          console.log('watch', newValue);
          selectedKeys.value = [];
          selectedKeys.value.push(newValue);
        }, {immediate: true});
        return {
          selectedKeys
        };
      },
    });
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>
    
    </style>
    
  • 测试

在这里插入图片描述

2.完善每日车次管理页面功能

1.为每日车次表单增加车次下拉选择,并自动填充车次相关数据

  • daily-train.vue

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="dailyTrains"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'type'">
            <span v-for="item in TRAIN_TYPE_ARRAY" :key="item.code">
              <span v-if="item.code === record.type">
                {{item.desc}}
              </span>
            </span>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="每日车次" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="dailyTrain" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="日期">
            <a-date-picker v-model:value="dailyTrain.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
          </a-form-item>
          <a-form-item label="车次编号">
            <train-select-view v-model="dailyTrain.code" @change="onChangeCode"></train-select-view>
          </a-form-item>
          <a-form-item label="车次类型">
            <a-select v-model:value="dailyTrain.type">
              <a-select-option v-for="item in TRAIN_TYPE_ARRAY" :key="item.code" :value="item.code">
                {{item.desc}}
              </a-select-option>
            </a-select>
          </a-form-item>
          <a-form-item label="始发站">
            <a-input v-model:value="dailyTrain.start" />
          </a-form-item>
          <a-form-item label="始发站拼音">
            <a-input v-model:value="dailyTrain.startPinyin" />
          </a-form-item>
          <a-form-item label="出发时间">
            <a-time-picker v-model:value="dailyTrain.startTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
          <a-form-item label="终点站">
            <a-input v-model:value="dailyTrain.end" />
          </a-form-item>
          <a-form-item label="终点站拼音">
            <a-input v-model:value="dailyTrain.endPinyin" />
          </a-form-item>
          <a-form-item label="到站时间">
            <a-time-picker v-model:value="dailyTrain.endTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import TrainSelectView from "@/components/train-select";
    
    export default defineComponent({
      name: "daily-train-view",
      components: {TrainSelectView},
      setup() {
        const TRAIN_TYPE_ARRAY = window.TRAIN_TYPE_ARRAY;
        const visible = ref(false);
        let dailyTrain = ref({
          id: undefined,
          date: undefined,
          code: undefined,
          type: undefined,
          start: undefined,
          startPinyin: undefined,
          startTime: undefined,
          end: undefined,
          endPinyin: undefined,
          endTime: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dailyTrains = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        const columns = [
          {
            title: '日期',
            dataIndex: 'date',
            key: 'date',
          },
          {
            title: '车次编号',
            dataIndex: 'code',
            key: 'code',
          },
          {
            title: '车次类型',
            dataIndex: 'type',
            key: 'type',
          },
          {
            title: '始发站',
            dataIndex: 'start',
            key: 'start',
          },
          {
            title: '始发站拼音',
            dataIndex: 'startPinyin',
            key: 'startPinyin',
          },
          {
            title: '出发时间',
            dataIndex: 'startTime',
            key: 'startTime',
          },
          {
            title: '终点站',
            dataIndex: 'end',
            key: 'end',
          },
          {
            title: '终点站拼音',
            dataIndex: 'endPinyin',
            key: 'endPinyin',
          },
          {
            title: '到站时间',
            dataIndex: 'endTime',
            key: 'endTime',
          },
          {
            title: '操作',
            dataIndex: 'operation'
          }
        ];
    
        const onAdd = () => {
          dailyTrain.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          dailyTrain.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/business/admin/daily-train/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/business/admin/daily-train/save", dailyTrain.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/business/admin/daily-train/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              dailyTrains.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        const onChangeCode = (train) => {
          console.log("车次下拉组件选择:", train);
          let t = Tool.copy(train);
          delete t.id;
          // 用assign可以合并
          dailyTrain.value = Object.assign(dailyTrain.value, t);
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          TRAIN_TYPE_ARRAY,
          dailyTrain,
          visible,
          dailyTrains,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onAdd,
          handleOk,
          onEdit,
          onDelete,
          onChangeCode
        };
      },
    });
    </script>
    
  • 测试

在这里插入图片描述

2.为每日车次页面增加日期和车次查询条件

注意:GET请求的日期是拼接在URL里的,需要用spring自带的@DateTimeFormat(pattern = "yyyy-MM-dd"),后端才能接收到参数

  • 修改DailyTrainQueryReq.java

    @Data
    public class DailyTrainQueryReq extends PageReq {
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date date;
    
        private String code;
    }
    
  • 修改DailyTrainService.java

    public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {
        DailyTrainExample dailyTrainExample = new DailyTrainExample();
        dailyTrainExample.setOrderByClause("date desc, code asc");
        DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();
        if (ObjectUtil.isNotNull(req.getDate())) {
            criteria.andDateEqualTo(req.getDate());
        }
        if (ObjectUtil.isNotEmpty(req.getCode())) {
            criteria.andCodeEqualTo(req.getCode());
        }
    
        LOG.info("查询页码:{}", req.getPage());
        LOG.info("每页条数:{}", req.getSize());
        PageHelper.startPage(req.getPage(), req.getSize());
        List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);
    
        PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);
        LOG.info("总行数:{}", pageInfo.getTotal());
        LOG.info("总页数:{}", pageInfo.getPages());
    
        List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);
    
        PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();
        pageResp.setTotal(pageInfo.getTotal());
        pageResp.setList(list);
        return pageResp;
    }
    
  • daily-train.vue

    <template>
      <p>
        <a-space>
          <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
          <train-select-view v-model="params.code" width="200px"></train-select-view>
          <a-button type="primary" @click="handleQuery()">查找</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="dailyTrains"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'type'">
            <span v-for="item in TRAIN_TYPE_ARRAY" :key="item.code">
              <span v-if="item.code === record.type">
                {{item.desc}}
              </span>
            </span>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="每日车次" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="dailyTrain" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="日期">
            <a-date-picker v-model:value="dailyTrain.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
          </a-form-item>
          <a-form-item label="车次编号">
            <train-select-view v-model="dailyTrain.code" @change="onChangeCode"></train-select-view>
          </a-form-item>
          <a-form-item label="车次类型">
            <a-select v-model:value="dailyTrain.type">
              <a-select-option v-for="item in TRAIN_TYPE_ARRAY" :key="item.code" :value="item.code">
                {{item.desc}}
              </a-select-option>
            </a-select>
          </a-form-item>
          <a-form-item label="始发站">
            <a-input v-model:value="dailyTrain.start" />
          </a-form-item>
          <a-form-item label="始发站拼音">
            <a-input v-model:value="dailyTrain.startPinyin" />
          </a-form-item>
          <a-form-item label="出发时间">
            <a-time-picker v-model:value="dailyTrain.startTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
          <a-form-item label="终点站">
            <a-input v-model:value="dailyTrain.end" />
          </a-form-item>
          <a-form-item label="终点站拼音">
            <a-input v-model:value="dailyTrain.endPinyin" />
          </a-form-item>
          <a-form-item label="到站时间">
            <a-time-picker v-model:value="dailyTrain.endTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import TrainSelectView from "@/components/train-select";
    
    export default defineComponent({
      name: "daily-train-view",
      components: {TrainSelectView},
      setup() {
        const TRAIN_TYPE_ARRAY = window.TRAIN_TYPE_ARRAY;
        const visible = ref(false);
        let dailyTrain = ref({
          id: undefined,
          date: undefined,
          code: undefined,
          type: undefined,
          start: undefined,
          startPinyin: undefined,
          startTime: undefined,
          end: undefined,
          endPinyin: undefined,
          endTime: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dailyTrains = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        let params = ref({
          code: null
        });
        const columns = [
          {
            title: '日期',
            dataIndex: 'date',
            key: 'date',
          },
          {
            title: '车次编号',
            dataIndex: 'code',
            key: 'code',
          },
          {
            title: '车次类型',
            dataIndex: 'type',
            key: 'type',
          },
          {
            title: '始发站',
            dataIndex: 'start',
            key: 'start',
          },
          {
            title: '始发站拼音',
            dataIndex: 'startPinyin',
            key: 'startPinyin',
          },
          {
            title: '出发时间',
            dataIndex: 'startTime',
            key: 'startTime',
          },
          {
            title: '终点站',
            dataIndex: 'end',
            key: 'end',
          },
          {
            title: '终点站拼音',
            dataIndex: 'endPinyin',
            key: 'endPinyin',
          },
          {
            title: '到站时间',
            dataIndex: 'endTime',
            key: 'endTime',
          },
          {
            title: '操作',
            dataIndex: 'operation'
          }
        ];
    
        const onAdd = () => {
          dailyTrain.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          dailyTrain.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/business/admin/daily-train/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/business/admin/daily-train/save", dailyTrain.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/business/admin/daily-train/query-list", {
            params: {
              page: param.page,
              size: param.size,
              code: params.value.code,
              date: params.value.date
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              dailyTrains.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        const onChangeCode = (train) => {
          console.log("车次下拉组件选择:", train);
          let t = Tool.copy(train);
          delete t.id;
          // 用assign可以合并
          dailyTrain.value = Object.assign(dailyTrain.value, t);
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          TRAIN_TYPE_ARRAY,
          dailyTrain,
          visible,
          dailyTrains,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onAdd,
          handleOk,
          onEdit,
          onDelete,
          onChangeCode,
          params
        };
      },
    });
    </script>
    
  • 测试

在这里插入图片描述

2.快速生成每日车站数据管理功能

步骤同上

  • 新增表

    drop table if exists `daily_train_station`;
    create table `daily_train_station` (
      `id` bigint not null comment 'id',
      `date` date not null comment '日期',
      `train_code` varchar(20) not null comment '车次编号',
      `index` int not null comment '站序',
      `name` varchar(20) not null comment '站名',
      `name_pinyin` varchar(50) not null comment '站名拼音',
      `in_time` time comment '进站时间',
      `out_time` time comment '出站时间',
      `stop_time` time comment '停站时长',
      `km` decimal(8, 2) not null comment '里程(公里)|从上一站到本站的距离',
      `create_time` datetime(3) comment '新增时间',
      `update_time` datetime(3) comment '修改时间',
      primary key (`id`),
      unique key `date_train_code_index_unique` (`date`, `train_code`, `index`),
      unique key `date_train_code_name_unique` (`date`, `train_code`, `name`)
    ) engine=innodb default charset=utf8mb4 comment='每日车站';
    
  • generator-config-business.xml

            <!--<table tableName="station" domainObjectName="Station"/>-->
            <!--<table tableName="train" domainObjectName="Train"/>-->
    <!--        <table tableName="train_station" domainObjectName="TrainStation"/>-->
    <!--        <table tableName="train_carriage" domainObjectName="TrainCarriage"/>-->
    <!--        <table tableName="train_seat" domainObjectName="TrainSeat"/>-->
    <!--        <table tableName="daily_train" domainObjectName="DailyTrain"/>-->
            <table tableName="daily_train_station" domainObjectName="DailyTrainStation"/>
    
  • 生成持久层、前后端代码

    同上

  • 修改路由、侧边栏

    同上

    <template>
      <a-layout-sider width="200" style="background: #fff">
        <a-menu
            v-model:selectedKeys="selectedKeys"
            :openKeys="['batch', 'base', 'business']"
            mode="inline"
            :style="{ height: '100%', borderRight: 0 }"
        >
          <a-menu-item key="/welcome">
            <router-link to="/welcome">
              <coffee-outlined /> &nbsp; 欢迎
            </router-link>
          </a-menu-item>
          <a-menu-item key="/about">
            <router-link to="/about">
              <user-outlined /> &nbsp; 关于
            </router-link>
          </a-menu-item>
          <a-sub-menu key="business">
            <template #title>
              <span>
                <UnorderedListOutlined />
                业务管理
              </span>
            </template>
            <a-menu-item key="/business/daily-train">
              <router-link to="/business/daily-train">
                <user-outlined /> &nbsp; 每日车次
              </router-link>
            </a-menu-item>
            <a-menu-item key="/business/daily-train-station">
              <router-link to="/business/daily-train-station">
                <user-outlined /> &nbsp; 每日车站
              </router-link>
            </a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="base">
            <template #title>
              <span>
                <UnorderedListOutlined />
                基础数据
              </span>
            </template>
            <a-menu-item key="/base/station">
              <router-link to="/base/station">
                <user-outlined /> &nbsp; 车站管理
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train">
              <router-link to="/base/train">
                <user-outlined /> &nbsp; 火车管理
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-station">
              <router-link to="/base/train-station">
                <user-outlined /> &nbsp; 火车车站
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-carriage">
              <router-link to="/base/train-carriage">
                <user-outlined /> &nbsp; 火车车厢
              </router-link>
            </a-menu-item>
            <a-menu-item key="/base/train-seat">
              <router-link to="/base/train-seat">
                <user-outlined /> &nbsp; 火车座位
              </router-link>
            </a-menu-item>
          </a-sub-menu>
          <a-sub-menu key="batch">
            <template #title>
              <span>
                <UnorderedListOutlined />
                跑批管理
              </span>
            </template>
            <a-menu-item key="/batch/job">
              <router-link to="/batch/job">
                <MenuUnfoldOutlined /> &nbsp; 任务管理
              </router-link>
            </a-menu-item>
          </a-sub-menu>
        </a-menu>
      </a-layout-sider>
    </template>
    
    <script>
    import {defineComponent, ref, watch} from 'vue';
    import router from "@/router";
    
    export default defineComponent({
      name: "the-sider-view",
      setup() {
        const selectedKeys = ref([]);
    
        watch(() => router.currentRoute.value.path, (newValue) => {
          console.log('watch', newValue);
          selectedKeys.value = [];
          selectedKeys.value.push(newValue);
        }, {immediate: true});
        return {
          selectedKeys
        };
      },
    });
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>
    
    </style>
    
    import { createRouter, createWebHistory } from 'vue-router'
    
    const routes = [{
      path: '/',
      component: () => import('../views/main.vue'),
      children: [{
        path: 'welcome',
        component: () => import('../views/main/welcome.vue'),
      }, {
        path: 'about',
        component: () => import('../views/main/about.vue'),
      }, {
        path: 'base/',
        children: [{
          path: 'station',
          component: () => import('../views/main/base/station.vue'),
        }, {
          path: 'train',
          component: () => import('../views/main/base/train.vue'),
        }, {
          path: 'train-station',
          component: () => import('../views/main/base/train-station.vue'),
        }, {
          path: 'train-carriage',
          component: () => import('../views/main/base/train-carriage.vue'),
        }, {
          path: 'train-seat',
          component: () => import('../views/main/base/train-seat.vue'),
        }]
      }, {
        path: 'business/',
        children: [{
          path: 'daily-train',
          component: () => import('../views/main/business/daily-train.vue'),
        }, {
          path: 'daily-train-station',
          component: () => import('../views/main/business/daily-train-station.vue'),
        }]
      }, {
        path: 'batch/',
        children: [{
          path: 'job',
          component: () => import('../views/main/batch/job.vue')
        }]
      }]
    }, {
      path: '',
      redirect: '/welcome'
    }];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL),
      routes
    })
    
    export default router
    
  • 测试

在这里插入图片描述

3.完善每日车站页面,增加日期和车次查询条件

  • DailyTrainStationQueryReq.java

    @Data
    public class DailyTrainStationQueryReq extends PageReq {
        /**
         * 日期
         */
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date date;
    
        /**
         * 车次编号
         */
        private String trainCode;
    }
    
  • DailyTrainStationService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.ObjUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrainStation;
    import com.neilxu.train.business.domain.DailyTrainStationExample;
    import com.neilxu.train.business.mapper.DailyTrainStationMapper;
    import com.neilxu.train.business.req.DailyTrainStationQueryReq;
    import com.neilxu.train.business.req.DailyTrainStationSaveReq;
    import com.neilxu.train.business.resp.DailyTrainStationQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service
    public class DailyTrainStationService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainStationService.class);
    
        @Resource
        private DailyTrainStationMapper dailyTrainStationMapper;
    
        public void save(DailyTrainStationSaveReq req) {
            DateTime now = DateTime.now();
            DailyTrainStation dailyTrainStation = BeanUtil.copyProperties(req, DailyTrainStation.class);
            if (ObjectUtil.isNull(dailyTrainStation.getId())) {
                dailyTrainStation.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainStation.setCreateTime(now);
                dailyTrainStation.setUpdateTime(now);
                dailyTrainStationMapper.insert(dailyTrainStation);
            } else {
                dailyTrainStation.setUpdateTime(now);
                dailyTrainStationMapper.updateByPrimaryKey(dailyTrainStation);
            }
        }
    
        public PageResp<DailyTrainStationQueryResp> queryList(DailyTrainStationQueryReq req) {
            DailyTrainStationExample dailyTrainStationExample = new DailyTrainStationExample();
            dailyTrainStationExample.setOrderByClause("date desc, train_code asc, `index` asc");
            DailyTrainStationExample.Criteria criteria = dailyTrainStationExample.createCriteria();
            if (ObjUtil.isNotNull(req.getDate())) {
                criteria.andDateEqualTo(req.getDate());
            }
            if (ObjUtil.isNotEmpty(req.getTrainCode())) {
                criteria.andTrainCodeEqualTo(req.getTrainCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrainStation> dailyTrainStationList = dailyTrainStationMapper.selectByExample(dailyTrainStationExample);
    
            PageInfo<DailyTrainStation> pageInfo = new PageInfo<>(dailyTrainStationList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainStationQueryResp> list = BeanUtil.copyToList(dailyTrainStationList, DailyTrainStationQueryResp.class);
    
            PageResp<DailyTrainStationQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
    
        public void delete(Long id) {
            dailyTrainStationMapper.deleteByPrimaryKey(id);
        }
    }
    
  • daily-train-station.vue

    <template>
      <p>
        <a-space>
          <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
          <train-select-view v-model="params.trainCode" width="200px"></train-select-view>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="dailyTrainStations"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="每日车站" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="dailyTrainStation" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="日期">
            <a-date-picker v-model:value="dailyTrainStation.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
          </a-form-item>
          <a-form-item label="车次编号">
            <train-select-view v-model="dailyTrainStation.trainCode"></train-select-view>
          </a-form-item>
          <a-form-item label="站序">
            <a-input v-model:value="dailyTrainStation.index" />
          </a-form-item>
          <a-form-item label="站名">
            <a-input v-model:value="dailyTrainStation.name" />
          </a-form-item>
          <a-form-item label="站名拼音">
            <a-input v-model:value="dailyTrainStation.namePinyin" disabled/>
          </a-form-item>
          <a-form-item label="进站时间">
            <a-time-picker v-model:value="dailyTrainStation.inTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
          <a-form-item label="出站时间">
            <a-time-picker v-model:value="dailyTrainStation.outTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
          <a-form-item label="停站时长">
            <a-time-picker v-model:value="dailyTrainStation.stopTime" valueFormat="HH:mm:ss" placeholder="请选择时间" disabled/>
          </a-form-item>
          <a-form-item label="里程(公里)">
            <a-input v-model:value="dailyTrainStation.km" />
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    
    <script>
    import {defineComponent, ref, onMounted, watch} from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import {pinyin} from "pinyin-pro";
    import dayjs from "dayjs";
    import TrainSelectView from "@/components/train-select";
    
    export default defineComponent({
      name: "daily-train-station-view",
      components: {TrainSelectView},
      setup() {
        const visible = ref(false);
        let dailyTrainStation = ref({
          id: undefined,
          date: undefined,
          trainCode: undefined,
          index: undefined,
          name: undefined,
          namePinyin: undefined,
          inTime: undefined,
          outTime: undefined,
          stopTime: undefined,
          km: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dailyTrainStations = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        let params = ref({
          trainCode: null,
          date: null
        });
        const columns = [
          {
            title: '日期',
            dataIndex: 'date',
            key: 'date',
          },
          {
            title: '车次编号',
            dataIndex: 'trainCode',
            key: 'trainCode',
          },
          {
            title: '站序',
            dataIndex: 'index',
            key: 'index',
          },
          {
            title: '站名',
            dataIndex: 'name',
            key: 'name',
          },
          {
            title: '站名拼音',
            dataIndex: 'namePinyin',
            key: 'namePinyin',
          },
          {
            title: '进站时间',
            dataIndex: 'inTime',
            key: 'inTime',
          },
          {
            title: '出站时间',
            dataIndex: 'outTime',
            key: 'outTime',
          },
          {
            title: '停站时长',
            dataIndex: 'stopTime',
            key: 'stopTime',
          },
          {
            title: '里程(公里)',
            dataIndex: 'km',
            key: 'km',
          },
          {
            title: '操作',
            dataIndex: 'operation'
          }
        ];
        watch(() => dailyTrainStation.value.name, ()=>{
          if (Tool.isNotEmpty(dailyTrainStation.value.name)) {
            dailyTrainStation.value.namePinyin = pinyin(dailyTrainStation.value.name, { toneType: 'none'}).replaceAll(" ", "");
          } else {
            dailyTrainStation.value.namePinyin = "";
          }
        }, {immediate: true});
    
        // 自动计算停车时长
        watch(() => dailyTrainStation.value.inTime, ()=>{
          let diff = dayjs(dailyTrainStation.value.outTime, 'HH:mm:ss').diff(dayjs(dailyTrainStation.value.inTime, 'HH:mm:ss'), 'seconds');
          dailyTrainStation.value.stopTime = dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
        }, {immediate: true});
    
        // 自动计算停车时长
        watch(() => dailyTrainStation.value.outTime, ()=>{
          let diff = dayjs(dailyTrainStation.value.outTime, 'HH:mm:ss').diff(dayjs(dailyTrainStation.value.inTime, 'HH:mm:ss'), 'seconds');
          dailyTrainStation.value.stopTime = dayjs('00:00:00', 'HH:mm:ss').second(diff).format('HH:mm:ss');
        }, {immediate: true});
    
        const onAdd = () => {
          dailyTrainStation.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          dailyTrainStation.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/business/admin/daily-train-station/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/business/admin/daily-train-station/save", dailyTrainStation.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/business/admin/daily-train-station/query-list", {
            params: {
              page: param.page,
              size: param.size,
              trainCode: params.value.trainCode,
              date: params.value.date
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              dailyTrainStations.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          dailyTrainStation,
          visible,
          dailyTrainStations,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onAdd,
          handleOk,
          onEdit,
          onDelete,
          params
        };
      },
    });
    </script>
    
  • 测试

在这里插入图片描述

4.快速生成每日车厢数据管理功能

操作同上

  • 新建表

    drop table if exists `daily_train_carriage`;
    create table `daily_train_carriage` (
      `id` bigint not null comment 'id',
      `date` date not null comment '日期',
      `train_code` varchar(20) not null comment '车次编号',
      `index` int not null comment '箱序',
      `seat_type` char(1) not null comment '座位类型|枚举[SeatTypeEnum]',
      `seat_count` int not null comment '座位数',
      `row_count` int not null comment '排数',
      `col_count` int not null comment '列数',
      `create_time` datetime(3) comment '新增时间',
      `update_time` datetime(3) comment '修改时间',
      primary key (`id`),
      unique key `date_train_code_index_unique` (`date`, `train_code`, `index`)
    ) engine=innodb default charset=utf8mb4 comment='每日车厢';
    
  • generator-config-business.xml

            <!--<table tableName="station" domainObjectName="Station"/>-->
            <!--<table tableName="train" domainObjectName="Train"/>-->
    <!--        <table tableName="train_station" domainObjectName="TrainStation"/>-->
    <!--        <table tableName="train_carriage" domainObjectName="TrainCarriage"/>-->
    <!--        <table tableName="train_seat" domainObjectName="TrainSeat"/>-->
    <!--        <table tableName="daily_train" domainObjectName="DailyTrain"/>-->
    <!--        <table tableName="daily_train_station" domainObjectName="DailyTrainStation"/>-->
            <table tableName="daily_train_carriage" domainObjectName="DailyTrainCarriage"/>
    
  • 生成持久层、前后端代码

    同上

  • 修改路由、侧边栏

    同上

    import { createRouter, createWebHistory } from 'vue-router'
    
    const routes = [{
      path: '/',
      component: () => import('../views/main.vue'),
      children: [{
        path: 'welcome',
        component: () => import('../views/main/welcome.vue'),
      }, {
        path: 'about',
        component: () => import('../views/main/about.vue'),
      }, {
        path: 'base/',
        children: [{
          path: 'station',
          component: () => import('../views/main/base/station.vue'),
        }, {
          path: 'train',
          component: () => import('../views/main/base/train.vue'),
        }, {
          path: 'train-station',
          component: () => import('../views/main/base/train-station.vue'),
        }, {
          path: 'train-carriage',
          component: () => import('../views/main/base/train-carriage.vue'),
        }, {
          path: 'train-seat',
          component: () => import('../views/main/base/train-seat.vue'),
        }]
      }, {
        path: 'business/',
        children: [{
          path: 'daily-train',
          component: () => import('../views/main/business/daily-train.vue'),
        }, {
          path: 'daily-train-station',
          component: () => import('../views/main/business/daily-train-station.vue'),
        }, {
          path: 'daily-train-carriage',
          component: () => import('../views/main/business/daily-train-carriage.vue'),
        }]
      }, {
        path: 'batch/',
        children: [{
          path: 'job',
          component: () => import('../views/main/batch/job.vue')
        }]
      }]
    }, {
      path: '',
      redirect: '/welcome'
    }];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL),
      routes
    })
    
    export default router
    
  • 测试

在这里插入图片描述

  • 完善每日车厢界面,增加日期和车次查询条件

    • DailyTrainCarriageSaveReq.java

          /**
           * 座位数
           */
      //    @NotNull(message = "【座位数】不能为空")
          private Integer seatCount;
      
          /**
           * 排数
           */
          @NotNull(message = "【排数】不能为空")
          private Integer rowCount;
      
          /**
           * 列数
           */
      //    @NotNull(message = "【列数】不能为空")
          private Integer colCount;
      
    • DailyTrainCarriageQueryReq.java

      @Data
      public class DailyTrainCarriageQueryReq extends PageReq {
          /**
           * 日期
           */
          @DateTimeFormat(pattern = "yyyy-MM-dd")
          private Date date;
      
          /**
           * 车次编号
           */
          private String trainCode;
      
      }
      
    • DailyTrainCarriageService.java

      package com.neilxu.train.business.service;
      
      import cn.hutool.core.bean.BeanUtil;
      import cn.hutool.core.date.DateTime;
      import cn.hutool.core.util.ObjUtil;
      import cn.hutool.core.util.ObjectUtil;
      import com.github.pagehelper.PageHelper;
      import com.github.pagehelper.PageInfo;
      import com.neilxu.train.business.domain.DailyTrainCarriage;
      import com.neilxu.train.business.domain.DailyTrainCarriageExample;
      import com.neilxu.train.business.enums.SeatColEnum;
      import com.neilxu.train.business.mapper.DailyTrainCarriageMapper;
      import com.neilxu.train.business.req.DailyTrainCarriageQueryReq;
      import com.neilxu.train.business.req.DailyTrainCarriageSaveReq;
      import com.neilxu.train.business.resp.DailyTrainCarriageQueryResp;
      import com.neilxu.train.common.resp.PageResp;
      import com.neilxu.train.common.util.SnowUtil;
      import jakarta.annotation.Resource;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.stereotype.Service;
      
      import java.util.List;
      
      @Service
      public class DailyTrainCarriageService {
      
          private static final Logger LOG = LoggerFactory.getLogger(DailyTrainCarriageService.class);
      
          @Resource
          private DailyTrainCarriageMapper dailyTrainCarriageMapper;
      
          public void save(DailyTrainCarriageSaveReq req) {
              DateTime now = DateTime.now();
      
              // 自动计算出列数和总座位数
              List<SeatColEnum> seatColEnums = SeatColEnum.getColsByType(req.getSeatType());
              req.setColCount(seatColEnums.size());
              req.setSeatCount(req.getColCount() * req.getRowCount());
      
              DailyTrainCarriage dailyTrainCarriage = BeanUtil.copyProperties(req, DailyTrainCarriage.class);
              if (ObjectUtil.isNull(dailyTrainCarriage.getId())) {
                  dailyTrainCarriage.setId(SnowUtil.getSnowflakeNextId());
                  dailyTrainCarriage.setCreateTime(now);
                  dailyTrainCarriage.setUpdateTime(now);
                  dailyTrainCarriageMapper.insert(dailyTrainCarriage);
              } else {
                  dailyTrainCarriage.setUpdateTime(now);
                  dailyTrainCarriageMapper.updateByPrimaryKey(dailyTrainCarriage);
              }
          }
      
          public PageResp<DailyTrainCarriageQueryResp> queryList(DailyTrainCarriageQueryReq req) {
              DailyTrainCarriageExample dailyTrainCarriageExample = new DailyTrainCarriageExample();
              dailyTrainCarriageExample.setOrderByClause("date desc, train_code asc, `index` asc");
              DailyTrainCarriageExample.Criteria criteria = dailyTrainCarriageExample.createCriteria();
              if (ObjUtil.isNotNull(req.getDate())) {
                  criteria.andDateEqualTo(req.getDate());
              }
              if (ObjUtil.isNotEmpty(req.getTrainCode())) {
                  criteria.andTrainCodeEqualTo(req.getTrainCode());
              }
      
              LOG.info("查询页码:{}", req.getPage());
              LOG.info("每页条数:{}", req.getSize());
              PageHelper.startPage(req.getPage(), req.getSize());
              List<DailyTrainCarriage> dailyTrainCarriageList = dailyTrainCarriageMapper.selectByExample(dailyTrainCarriageExample);
      
              PageInfo<DailyTrainCarriage> pageInfo = new PageInfo<>(dailyTrainCarriageList);
              LOG.info("总行数:{}", pageInfo.getTotal());
              LOG.info("总页数:{}", pageInfo.getPages());
      
              List<DailyTrainCarriageQueryResp> list = BeanUtil.copyToList(dailyTrainCarriageList, DailyTrainCarriageQueryResp.class);
      
              PageResp<DailyTrainCarriageQueryResp> pageResp = new PageResp<>();
              pageResp.setTotal(pageInfo.getTotal());
              pageResp.setList(list);
              return pageResp;
          }
      
          public void delete(Long id) {
              dailyTrainCarriageMapper.deleteByPrimaryKey(id);
          }
      }
      
    • daily-train-carriage.vue

      <template>
        <p>
          <a-space>
            <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
            <train-select-view v-model="params.trainCode" width="200px"></train-select-view>
            <a-button type="primary" @click="handleQuery()">查询</a-button>
            <a-button type="primary" @click="onAdd">新增</a-button>
          </a-space>
        </p>
        <a-table :dataSource="dailyTrainCarriages"
                 :columns="columns"
                 :pagination="pagination"
                 @change="handleTableChange"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
              <a-space>
                <a-popconfirm
                    title="删除后不可恢复,确认删除?"
                    @confirm="onDelete(record)"
                    ok-text="确认" cancel-text="取消">
                  <a style="color: red">删除</a>
                </a-popconfirm>
                <a @click="onEdit(record)">编辑</a>
              </a-space>
            </template>
            <template v-else-if="column.dataIndex === 'seatType'">
              <span v-for="item in SEAT_TYPE_ARRAY" :key="item.code">
                <span v-if="item.code === record.seatType">
                  {{item.desc}}
                </span>
              </span>
            </template>
          </template>
        </a-table>
        <a-modal v-model:visible="visible" title="每日车箱" @ok="handleOk"
                 ok-text="确认" cancel-text="取消">
          <a-form :model="dailyTrainCarriage" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
            <a-form-item label="日期">
              <a-date-picker v-model:value="dailyTrainCarriage.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
            </a-form-item>
            <a-form-item label="车次编号">
              <train-select-view v-model="dailyTrainCarriage.trainCode" width="200px"></train-select-view>
            </a-form-item>
            <a-form-item label="箱序">
              <a-input v-model:value="dailyTrainCarriage.index" />
            </a-form-item>
            <a-form-item label="座位类型">
              <a-select v-model:value="dailyTrainCarriage.seatType">
                <a-select-option v-for="item in SEAT_TYPE_ARRAY" :key="item.code" :value="item.code">
                  {{item.desc}}
                </a-select-option>
              </a-select>
            </a-form-item>
            <!--<a-form-item label="座位数">-->
            <!--  <a-input v-model:value="dailyTrainCarriage.seatCount" />-->
            <!--</a-form-item>-->
            <a-form-item label="排数">
              <a-input v-model:value="dailyTrainCarriage.rowCount" />
            </a-form-item>
            <!--<a-form-item label="列数">-->
            <!--  <a-input v-model:value="dailyTrainCarriage.colCount" />-->
            <!--</a-form-item>-->
          </a-form>
        </a-modal>
      </template>
      
      <script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";
      import TrainSelectView from "@/components/train-select";
      
      export default defineComponent({
        name: "daily-train-carriage-view",
        components: {TrainSelectView},
        setup() {
          const SEAT_TYPE_ARRAY = window.SEAT_TYPE_ARRAY;
          const visible = ref(false);
          let dailyTrainCarriage = ref({
            id: undefined,
            date: undefined,
            trainCode: undefined,
            index: undefined,
            seatType: undefined,
            seatCount: undefined,
            rowCount: undefined,
            colCount: undefined,
            createTime: undefined,
            updateTime: undefined,
          });
          const dailyTrainCarriages = ref([]);
          // 分页的三个属性名是固定的
          const pagination = ref({
            total: 0,
            current: 1,
            pageSize: 10,
          });
          let loading = ref(false);
          let params = ref({
            trainCode: null,
            date: null
          });
          const columns = [
            {
              title: '日期',
              dataIndex: 'date',
              key: 'date',
            },
            {
              title: '车次编号',
              dataIndex: 'trainCode',
              key: 'trainCode',
            },
            {
              title: '箱序',
              dataIndex: 'index',
              key: 'index',
            },
            {
              title: '座位类型',
              dataIndex: 'seatType',
              key: 'seatType',
            },
            {
              title: '座位数',
              dataIndex: 'seatCount',
              key: 'seatCount',
            },
            {
              title: '排数',
              dataIndex: 'rowCount',
              key: 'rowCount',
            },
            {
              title: '列数',
              dataIndex: 'colCount',
              key: 'colCount',
            },
            {
              title: '操作',
              dataIndex: 'operation'
            }
          ];
      
          const onAdd = () => {
            dailyTrainCarriage.value = {};
            visible.value = true;
          };
      
          const onEdit = (record) => {
            dailyTrainCarriage.value = window.Tool.copy(record);
            visible.value = true;
          };
      
          const onDelete = (record) => {
            axios.delete("/business/admin/daily-train-carriage/delete/" + record.id).then((response) => {
              const data = response.data;
              if (data.success) {
                notification.success({description: "删除成功!"});
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize,
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleOk = () => {
            axios.post("/business/admin/daily-train-carriage/save", dailyTrainCarriage.value).then((response) => {
              let data = response.data;
              if (data.success) {
                notification.success({description: "保存成功!"});
                visible.value = false;
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleQuery = (param) => {
            if (!param) {
              param = {
                page: 1,
                size: pagination.value.pageSize
              };
            }
            loading.value = true;
            axios.get("/business/admin/daily-train-carriage/query-list", {
              params: {
                page: param.page,
                size: param.size,
                trainCode: params.value.trainCode,
                date: params.value.date
              }
            }).then((response) => {
              loading.value = false;
              let data = response.data;
              if (data.success) {
                dailyTrainCarriages.value = data.content.list;
                // 设置分页控件的值
                pagination.value.current = param.page;
                pagination.value.total = data.content.total;
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleTableChange = (pagination) => {
            // console.log("看看自带的分页参数都有啥:" + pagination);
            handleQuery({
              page: pagination.current,
              size: pagination.pageSize
            });
          };
      
          onMounted(() => {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          });
      
          return {
            SEAT_TYPE_ARRAY,
            dailyTrainCarriage,
            visible,
            dailyTrainCarriages,
            pagination,
            columns,
            handleTableChange,
            handleQuery,
            loading,
            onAdd,
            handleOk,
            onEdit,
            onDelete,
            params
          };
        },
      });
      </script>
      
    • 测试

在这里插入图片描述

5.快速生成每日座位数据管理功能

操作同上

  • 将生成器中的\改成/,兼容MAC

    这里是为了兼容mac,把之前generator-config-member.xml、generator-config-business.xml、generator-config-batch.xml、FreemarkerUtil.java、ServerGenerator.java所有的 \\\都改成了/

  • 新增表

    这里注意多了关于售卖的字段

    drop table if exists `daily_train_seat`;
    create table `daily_train_seat` (
      `id` bigint not null comment 'id',
      `date` date not null comment '日期',
      `train_code` varchar(20) not null comment '车次编号',
      `carriage_index` int not null comment '箱序',
      `row` char(2) not null comment '排号|01, 02',
      `col` char(1) not null comment '列号|枚举[SeatColEnum]',
      `seat_type` char(1) not null comment '座位类型|枚举[SeatTypeEnum]',
      `carriage_seat_index` int not null comment '同车箱座序',
      `sell` varchar(50) not null comment '售卖情况|将经过的车站用01拼接,0表示可卖,1表示已卖',
      `create_time` datetime(3) comment '新增时间',
      `update_time` datetime(3) comment '修改时间',
      primary key (`id`)
    ) engine=innodb default charset=utf8mb4 comment='每日座位';
    

    sell存的值例如:1011,表示车站2到车站3未卖,其余都卖了

  • 生成持久层、前后端代码;修改路由、侧边栏

    过程同上

  • 每日座位改成只读

    修改ServerGenerator.java,重新生成vue

  • 修改每日座位页面,增加车次查询条件,修改列号的显示

    过程同上,列号问题参考之前的座位页面

  • 测试

    这里由于改了只读,不方便测试,直接放后面做完定时任务后测试

6.增加生成每日车次定时任务

功能如下:

在这里插入图片描述

1.增加生成每日车次定时任务

  • DailyTrainJob.java

    package com.neilxu.train.batch.job;
    
    import cn.hutool.core.util.RandomUtil;
    import org.quartz.DisallowConcurrentExecution;
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.slf4j.MDC;
    
    @DisallowConcurrentExecution
    public class DailyTrainJob implements Job {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainJob.class);
    
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            // 增加日志流水号
            MDC.put("LOG_ID", System.currentTimeMillis() + RandomUtil.randomString(3));
            LOG.info("生成每日车次数据开始");
            LOG.info("生成每日车次数据结束");
        }
    }
    
  • 页面配置

在这里插入图片描述

2.集成openfeign,实现服务间调用。给每个应用增加应用名,可搭配配置中心使用

  • 修改properties,增加应用名

    例如:member/src/main/resources/application.properties

    spring.application.name=member
    

    其余同上

  • 新增openfeign依赖

    <!--远程调用openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--openfeign默认使用的是loadBalance的负载均衡器  -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-loadbalancer</artifactId>
    </dependency>
    
  • BusinessFeign.java

    package com.neilxu.train.batch.feign;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @FeignClient("business")
    // @FeignClient(name = "business", url = "http://127.0.0.1:8002/business")
    public interface BusinessFeign {
    
        @GetMapping("/hello")
        String hello();
    }
    
  • TestController.java

    测试openfeign

    package com.neilxu.train.batch.controller;
    
    import com.neilxu.train.batch.feign.BusinessFeign;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class TestController {
    
        private static final Logger LOG = LoggerFactory.getLogger(TestController.class);
    
        @Resource
        BusinessFeign businessFeign;
    
        @GetMapping("/hello")
        public String hello() {
            String businessHello = businessFeign.hello();
            LOG.info(businessHello);
            return "Hello World! Batch!";
        }
    }
    
  • 开启openfeign

    BatchApplication.java

    package com.neilxu.train.batch.config;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.core.env.Environment;
    
    @SpringBootApplication
    @ComponentScan("com.neilxu")
    @MapperScan("com.neilxu.train.*.mapper")
    @EnableFeignClients("com.neilxu.train.batch.feign")
    public class BatchApplication {
    
        private static final Logger LOG = LoggerFactory.getLogger(BatchApplication.class);
    
        public static void main(String[] args) {
            SpringApplication app = new SpringApplication(BatchApplication.class);
            Environment env = app.run(args).getEnvironment();
            LOG.info("启动成功!!");
            LOG.info("测试地址: \thttp://127.0.0.1:{}{}/hello", env.getProperty("server.port"), env.getProperty("server.servlet.context-path"));
        }
    }
    
  • 重启测试

    GET http://localhost:8000/batch/hello
    Accept: application/json
    
    ###
    

在这里插入图片描述

3.实现job——增加生成每日车次功能

  • 修改TrainService.java

    public List<TrainQueryResp> queryAll() {
        List<Train> trainList = selectAll();
        return BeanUtil.copyToList(trainList, TrainQueryResp.class);
    }
    
    public List<Train> selectAll() {
        TrainExample trainExample = new TrainExample();
        trainExample.setOrderByClause("code asc");
        return trainMapper.selectByExample(trainExample);
    }
    
  • DailyTrainService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrain;
    import com.neilxu.train.business.domain.DailyTrainExample;
    import com.neilxu.train.business.domain.Train;
    import com.neilxu.train.business.mapper.DailyTrainMapper;
    import com.neilxu.train.business.req.DailyTrainQueryReq;
    import com.neilxu.train.business.req.DailyTrainSaveReq;
    import com.neilxu.train.business.resp.DailyTrainQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class DailyTrainService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class);
    
        @Resource
        private DailyTrainMapper dailyTrainMapper;
    
        @Resource
        private TrainService trainService;
    
        public void save(DailyTrainSaveReq req) {
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(req, DailyTrain.class);
            if (ObjectUtil.isNull(dailyTrain.getId())) {
                dailyTrain.setId(SnowUtil.getSnowflakeNextId());
                dailyTrain.setCreateTime(now);
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.insert(dailyTrain);
            } else {
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.updateByPrimaryKey(dailyTrain);
            }
        }
    
        public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.setOrderByClause("date desc, code asc");
            DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();
            if (ObjectUtil.isNotNull(req.getDate())) {
                criteria.andDateEqualTo(req.getDate());
            }
            if (ObjectUtil.isNotEmpty(req.getCode())) {
                criteria.andCodeEqualTo(req.getCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);
    
            PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);
    
            PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            dailyTrainMapper.deleteByPrimaryKey(id);
        }
    
        /**
         * 生成某日所有车次信息,包括车次、车站、车厢、座位
         * @param date
         */
        public void genDaily(Date date) {
            List<Train> trainList = trainService.selectAll();
            if (CollUtil.isEmpty(trainList)) {
                LOG.info("没有车次基础数据,任务结束");
                return;
            }
    
            for (Train train : trainList) {
                genDailyTrain(date, train);
            }
        }
    
        public void genDailyTrain(Date date, Train train) {
            // 删除该车次已有的数据
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.createCriteria()
                    .andDateEqualTo(date)
                    .andCodeEqualTo(train.getCode());
            dailyTrainMapper.deleteByExample(dailyTrainExample);
    
            // 生成该车次的数据
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class);
            dailyTrain.setId(SnowUtil.getSnowflakeNextId());
            dailyTrain.setCreateTime(now);
            dailyTrain.setUpdateTime(now);
            dailyTrain.setDate(date);
            dailyTrainMapper.insert(dailyTrain);
        }
    }
    
  • DailyTrainAdminController.java

    package com.neilxu.train.business.controller.admin;
    
    import com.neilxu.train.business.req.DailyTrainQueryReq;
    import com.neilxu.train.business.req.DailyTrainSaveReq;
    import com.neilxu.train.business.resp.DailyTrainQueryResp;
    import com.neilxu.train.business.service.DailyTrainService;
    import com.neilxu.train.common.resp.CommonResp;
    import com.neilxu.train.common.resp.PageResp;
    import jakarta.annotation.Resource;
    import jakarta.validation.Valid;
    import org.springframework.format.annotation.DateTimeFormat;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.Date;
    
    @RestController
    @RequestMapping("/admin/daily-train")
    public class DailyTrainAdminController {
    
        @Resource
        private DailyTrainService dailyTrainService;
    
        @PostMapping("/save")
        public CommonResp<Object> save(@Valid @RequestBody DailyTrainSaveReq req) {
            dailyTrainService.save(req);
            return new CommonResp<>();
        }
    
        @GetMapping("/query-list")
        public CommonResp<PageResp<DailyTrainQueryResp>> queryList(@Valid DailyTrainQueryReq req) {
            PageResp<DailyTrainQueryResp> list = dailyTrainService.queryList(req);
            return new CommonResp<>(list);
        }
    
        @DeleteMapping("/delete/{id}")
        public CommonResp<Object> delete(@PathVariable Long id) {
            dailyTrainService.delete(id);
            return new CommonResp<>();
        }
    
        @GetMapping("/gen-daily/{date}")
        public CommonResp<Object> genDaily(@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
            dailyTrainService.genDaily(date);
            return new CommonResp<>();
        }
    
    }
    
  • BusinessFeign.java

    package com.neilxu.train.batch.feign;
    
    import com.neilxu.train.common.resp.CommonResp;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.format.annotation.DateTimeFormat;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    import java.util.Date;
    
    // @FeignClient("business")
    @FeignClient(name = "business", url = "http://127.0.0.1:8002/business")
    public interface BusinessFeign {
    
        @GetMapping("/hello")
        String hello();
    
        @GetMapping("/admin/daily-train/gen-daily/{date}")
        CommonResp<Object> genDaily(@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") Date date);
    }
    
  • DailyTrainJob.java

    package com.neilxu.train.batch.job;
    
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.RandomUtil;
    import com.neilxu.train.batch.feign.BusinessFeign;
    import com.neilxu.train.common.resp.CommonResp;
    import jakarta.annotation.Resource;
    import org.quartz.DisallowConcurrentExecution;
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.slf4j.MDC;
    
    import java.util.Date;
    
    @DisallowConcurrentExecution
    public class DailyTrainJob implements Job {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainJob.class);
    
        @Resource
        BusinessFeign businessFeign;
    
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            // 增加日志流水号
            MDC.put("LOG_ID", System.currentTimeMillis() + RandomUtil.randomString(3));
            LOG.info("生成15天后的车次数据开始");
            Date date = new Date();
            DateTime dateTime = DateUtil.offsetDay(date, 15);
            Date offsetDate = dateTime.toJdkDate();
            CommonResp<Object> commonResp = businessFeign.genDaily(offsetDate);
            LOG.info("生成15天后的车次数据结束,结果:{}", commonResp);
        }
    }
    
  • 测试

    http/batch-job.http

    POST http://localhost:8000/batch/admin/job/run
    Content-Type: application/json
    
    {
      "name": "com.neilxu.train.batch.job.DailyTrainJob",
      "jobGroupName": "default"
    }
    

在这里插入图片描述

7.增加生成每日车站功能

知识点:

Mybatis的查询列表,查不到返回是空集合,不是null

  • TrainStationService.java

    public List<TrainStation> selectByTrainCode(String trainCode) {
        TrainStationExample trainStationExample = new TrainStationExample();
        trainStationExample.setOrderByClause("`index` asc");
        trainStationExample.createCriteria().andTrainCodeEqualTo(trainCode);
        return trainStationMapper.selectByExample(trainStationExample);
    }
    
  • DailyTrainStationService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrainStation;
    import com.neilxu.train.business.domain.DailyTrainStationExample;
    import com.neilxu.train.business.domain.TrainStation;
    import com.neilxu.train.business.mapper.DailyTrainStationMapper;
    import com.neilxu.train.business.req.DailyTrainStationQueryReq;
    import com.neilxu.train.business.req.DailyTrainStationSaveReq;
    import com.neilxu.train.business.resp.DailyTrainStationQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class DailyTrainStationService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainStationService.class);
    
        @Resource
        private DailyTrainStationMapper dailyTrainStationMapper;
    
        @Resource
        private TrainStationService trainStationService;
    
        public void save(DailyTrainStationSaveReq req) {
            DateTime now = DateTime.now();
            DailyTrainStation dailyTrainStation = BeanUtil.copyProperties(req, DailyTrainStation.class);
            if (ObjectUtil.isNull(dailyTrainStation.getId())) {
                dailyTrainStation.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainStation.setCreateTime(now);
                dailyTrainStation.setUpdateTime(now);
                dailyTrainStationMapper.insert(dailyTrainStation);
            } else {
                dailyTrainStation.setUpdateTime(now);
                dailyTrainStationMapper.updateByPrimaryKey(dailyTrainStation);
            }
        }
    
        public PageResp<DailyTrainStationQueryResp> queryList(DailyTrainStationQueryReq req) {
            DailyTrainStationExample dailyTrainStationExample = new DailyTrainStationExample();
            dailyTrainStationExample.setOrderByClause("date desc, train_code asc, `index` asc");
            DailyTrainStationExample.Criteria criteria = dailyTrainStationExample.createCriteria();
            if (ObjUtil.isNotNull(req.getDate())) {
                criteria.andDateEqualTo(req.getDate());
            }
            if (ObjUtil.isNotEmpty(req.getTrainCode())) {
                criteria.andTrainCodeEqualTo(req.getTrainCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrainStation> dailyTrainStationList = dailyTrainStationMapper.selectByExample(dailyTrainStationExample);
    
            PageInfo<DailyTrainStation> pageInfo = new PageInfo<>(dailyTrainStationList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainStationQueryResp> list = BeanUtil.copyToList(dailyTrainStationList, DailyTrainStationQueryResp.class);
    
            PageResp<DailyTrainStationQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            dailyTrainStationMapper.deleteByPrimaryKey(id);
        }
    
        public void genDaily(Date date, String trainCode) {
            LOG.info("生成日期【{}】车次【{}】的车站信息开始", DateUtil.formatDate(date), trainCode);
    
            // 删除某日某车次的车站信息
            DailyTrainStationExample dailyTrainStationExample = new DailyTrainStationExample();
            dailyTrainStationExample.createCriteria()
                    .andDateEqualTo(date)
                    .andTrainCodeEqualTo(trainCode);
            dailyTrainStationMapper.deleteByExample(dailyTrainStationExample);
    
            // 查出某车次的所有的车站信息
            List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);
            if (CollUtil.isEmpty(stationList)) {
                LOG.info("该车次没有车站基础数据,生成该车次的车站信息结束");
                return;
            }
    
            for (TrainStation trainStation : stationList) {
                DateTime now = DateTime.now();
                DailyTrainStation dailyTrainStation = BeanUtil.copyProperties(trainStation, DailyTrainStation.class);
                dailyTrainStation.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainStation.setCreateTime(now);
                dailyTrainStation.setUpdateTime(now);
                dailyTrainStation.setDate(date);
                dailyTrainStationMapper.insert(dailyTrainStation);
            }
            LOG.info("生成日期【{}】车次【{}】的车站信息结束", DateUtil.formatDate(date), trainCode);
        }
    }
    
  • DailyTrainService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrain;
    import com.neilxu.train.business.domain.DailyTrainExample;
    import com.neilxu.train.business.domain.Train;
    import com.neilxu.train.business.mapper.DailyTrainMapper;
    import com.neilxu.train.business.req.DailyTrainQueryReq;
    import com.neilxu.train.business.req.DailyTrainSaveReq;
    import com.neilxu.train.business.resp.DailyTrainQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class DailyTrainService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class);
    
        @Resource
        private DailyTrainMapper dailyTrainMapper;
    
        @Resource
        private TrainService trainService;
    
        @Resource
        private DailyTrainStationService dailyTrainStationService;
    
        public void save(DailyTrainSaveReq req) {
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(req, DailyTrain.class);
            if (ObjectUtil.isNull(dailyTrain.getId())) {
                dailyTrain.setId(SnowUtil.getSnowflakeNextId());
                dailyTrain.setCreateTime(now);
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.insert(dailyTrain);
            } else {
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.updateByPrimaryKey(dailyTrain);
            }
        }
    
        public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.setOrderByClause("date desc, code asc");
            DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();
            if (ObjectUtil.isNotNull(req.getDate())) {
                criteria.andDateEqualTo(req.getDate());
            }
            if (ObjectUtil.isNotEmpty(req.getCode())) {
                criteria.andCodeEqualTo(req.getCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);
    
            PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);
    
            PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            dailyTrainMapper.deleteByPrimaryKey(id);
        }
    
        /**
         * 生成某日所有车次信息,包括车次、车站、车厢、座位
         * @param date
         */
        public void genDaily(Date date) {
            List<Train> trainList = trainService.selectAll();
            if (CollUtil.isEmpty(trainList)) {
                LOG.info("没有车次基础数据,任务结束");
                return;
            }
    
            for (Train train : trainList) {
                genDailyTrain(date, train);
            }
        }
    
        public void genDailyTrain(Date date, Train train) {
            LOG.info("生成日期【{}】车次【{}】的信息开始", DateUtil.formatDate(date), train.getCode());
            // 删除该车次已有的数据
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.createCriteria()
                    .andDateEqualTo(date)
                    .andCodeEqualTo(train.getCode());
            dailyTrainMapper.deleteByExample(dailyTrainExample);
    
            // 生成该车次的数据
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class);
            dailyTrain.setId(SnowUtil.getSnowflakeNextId());
            dailyTrain.setCreateTime(now);
            dailyTrain.setUpdateTime(now);
            dailyTrain.setDate(date);
            dailyTrainMapper.insert(dailyTrain);
    
            // 生成该车次的车站数据
            dailyTrainStationService.genDaily(date, train.getCode());
            LOG.info("生成日期【{}】车次【{}】的信息结束", DateUtil.formatDate(date), train.getCode());
        }
    }
    
  • 测试

在这里插入图片描述

8.增加生成每日车厢功能

操作同上

  • DailyTrainCarriageService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.*;
    import com.neilxu.train.business.enums.SeatColEnum;
    import com.neilxu.train.business.mapper.DailyTrainCarriageMapper;
    import com.neilxu.train.business.req.DailyTrainCarriageQueryReq;
    import com.neilxu.train.business.req.DailyTrainCarriageSaveReq;
    import com.neilxu.train.business.resp.DailyTrainCarriageQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class DailyTrainCarriageService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainCarriageService.class);
    
        @Resource
        private DailyTrainCarriageMapper dailyTrainCarriageMapper;
    
        @Resource
        private TrainCarriageService trainCarriageService;
    
        public void save(DailyTrainCarriageSaveReq req) {
            DateTime now = DateTime.now();
    
            // 自动计算出列数和总座位数
            List<SeatColEnum> seatColEnums = SeatColEnum.getColsByType(req.getSeatType());
            req.setColCount(seatColEnums.size());
            req.setSeatCount(req.getColCount() * req.getRowCount());
    
            DailyTrainCarriage dailyTrainCarriage = BeanUtil.copyProperties(req, DailyTrainCarriage.class);
            if (ObjectUtil.isNull(dailyTrainCarriage.getId())) {
                dailyTrainCarriage.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainCarriage.setCreateTime(now);
                dailyTrainCarriage.setUpdateTime(now);
                dailyTrainCarriageMapper.insert(dailyTrainCarriage);
            } else {
                dailyTrainCarriage.setUpdateTime(now);
                dailyTrainCarriageMapper.updateByPrimaryKey(dailyTrainCarriage);
            }
        }
    
        public PageResp<DailyTrainCarriageQueryResp> queryList(DailyTrainCarriageQueryReq req) {
            DailyTrainCarriageExample dailyTrainCarriageExample = new DailyTrainCarriageExample();
            dailyTrainCarriageExample.setOrderByClause("date desc, train_code asc, `index` asc");
            DailyTrainCarriageExample.Criteria criteria = dailyTrainCarriageExample.createCriteria();
            if (ObjUtil.isNotNull(req.getDate())) {
                criteria.andDateEqualTo(req.getDate());
            }
            if (ObjUtil.isNotEmpty(req.getTrainCode())) {
                criteria.andTrainCodeEqualTo(req.getTrainCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrainCarriage> dailyTrainCarriageList = dailyTrainCarriageMapper.selectByExample(dailyTrainCarriageExample);
    
            PageInfo<DailyTrainCarriage> pageInfo = new PageInfo<>(dailyTrainCarriageList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainCarriageQueryResp> list = BeanUtil.copyToList(dailyTrainCarriageList, DailyTrainCarriageQueryResp.class);
    
            PageResp<DailyTrainCarriageQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            dailyTrainCarriageMapper.deleteByPrimaryKey(id);
        }
    
        public void genDaily(Date date, String trainCode) {
            LOG.info("生成日期【{}】车次【{}】的车厢信息开始", DateUtil.formatDate(date), trainCode);
    
            // 删除某日某车次的车厢信息
            DailyTrainCarriageExample dailyTrainCarriageExample = new DailyTrainCarriageExample();
            dailyTrainCarriageExample.createCriteria()
                    .andDateEqualTo(date)
                    .andTrainCodeEqualTo(trainCode);
            dailyTrainCarriageMapper.deleteByExample(dailyTrainCarriageExample);
    
            // 查出某车次的所有的车厢信息
            List<TrainCarriage> carriageList = trainCarriageService.selectByTrainCode(trainCode);
            if (CollUtil.isEmpty(carriageList)) {
                LOG.info("该车次没有车厢基础数据,生成该车次的车厢信息结束");
                return;
            }
    
            for (TrainCarriage trainCarriage : carriageList) {
                DateTime now = DateTime.now();
                DailyTrainCarriage dailyTrainCarriage = BeanUtil.copyProperties(trainCarriage, DailyTrainCarriage.class);
                dailyTrainCarriage.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainCarriage.setCreateTime(now);
                dailyTrainCarriage.setUpdateTime(now);
                dailyTrainCarriage.setDate(date);
                dailyTrainCarriageMapper.insert(dailyTrainCarriage);
            }
            LOG.info("生成日期【{}】车次【{}】的车厢信息结束", DateUtil.formatDate(date), trainCode);
        }
    }
    
  • DailyTrainService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrain;
    import com.neilxu.train.business.domain.DailyTrainExample;
    import com.neilxu.train.business.domain.Train;
    import com.neilxu.train.business.mapper.DailyTrainMapper;
    import com.neilxu.train.business.req.DailyTrainQueryReq;
    import com.neilxu.train.business.req.DailyTrainSaveReq;
    import com.neilxu.train.business.resp.DailyTrainQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class DailyTrainService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class);
    
        @Resource
        private DailyTrainMapper dailyTrainMapper;
    
        @Resource
        private TrainService trainService;
    
        @Resource
        private DailyTrainStationService dailyTrainStationService;
    
        @Resource
        private DailyTrainCarriageService dailyTrainCarriageService;
    
        public void save(DailyTrainSaveReq req) {
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(req, DailyTrain.class);
            if (ObjectUtil.isNull(dailyTrain.getId())) {
                dailyTrain.setId(SnowUtil.getSnowflakeNextId());
                dailyTrain.setCreateTime(now);
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.insert(dailyTrain);
            } else {
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.updateByPrimaryKey(dailyTrain);
            }
        }
    
        public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.setOrderByClause("date desc, code asc");
            DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();
            if (ObjectUtil.isNotNull(req.getDate())) {
                criteria.andDateEqualTo(req.getDate());
            }
            if (ObjectUtil.isNotEmpty(req.getCode())) {
                criteria.andCodeEqualTo(req.getCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);
    
            PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);
    
            PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            dailyTrainMapper.deleteByPrimaryKey(id);
        }
    
        /**
         * 生成某日所有车次信息,包括车次、车站、车厢、座位
         * @param date
         */
        public void genDaily(Date date) {
            List<Train> trainList = trainService.selectAll();
            if (CollUtil.isEmpty(trainList)) {
                LOG.info("没有车次基础数据,任务结束");
                return;
            }
    
            for (Train train : trainList) {
                genDailyTrain(date, train);
            }
        }
    
        public void genDailyTrain(Date date, Train train) {
            LOG.info("生成日期【{}】车次【{}】的信息开始", DateUtil.formatDate(date), train.getCode());
            // 删除该车次已有的数据
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.createCriteria()
                    .andDateEqualTo(date)
                    .andCodeEqualTo(train.getCode());
            dailyTrainMapper.deleteByExample(dailyTrainExample);
    
            // 生成该车次的数据
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class);
            dailyTrain.setId(SnowUtil.getSnowflakeNextId());
            dailyTrain.setCreateTime(now);
            dailyTrain.setUpdateTime(now);
            dailyTrain.setDate(date);
            dailyTrainMapper.insert(dailyTrain);
    
            // 生成该车次的车站数据
            dailyTrainStationService.genDaily(date, train.getCode());
    
            // 生成该车次的车厢数据
            dailyTrainCarriageService.genDaily(date, train.getCode());
    
            LOG.info("生成日期【{}】车次【{}】的信息结束", DateUtil.formatDate(date), train.getCode());
        }
    }
    
  • 测试

    结果正常

9.增加生成每日座位功能

操作同上,注意sell字段的逻辑,补0,个数是 经过车站数-1

  • TrainSeatService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.ObjectUtil;
    import cn.hutool.core.util.StrUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.TrainCarriage;
    import com.neilxu.train.business.domain.TrainSeat;
    import com.neilxu.train.business.domain.TrainSeatExample;
    import com.neilxu.train.business.enums.SeatColEnum;
    import com.neilxu.train.business.mapper.TrainSeatMapper;
    import com.neilxu.train.business.req.TrainSeatQueryReq;
    import com.neilxu.train.business.req.TrainSeatSaveReq;
    import com.neilxu.train.business.resp.TrainSeatQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    @Service
    public class TrainSeatService {
    
        private static final Logger LOG = LoggerFactory.getLogger(TrainSeatService.class);
    
        @Resource
        private TrainSeatMapper trainSeatMapper;
    
        @Resource
        private TrainCarriageService trainCarriageService;
    
        public void save(TrainSeatSaveReq req) {
            DateTime now = DateTime.now();
            TrainSeat trainSeat = BeanUtil.copyProperties(req, TrainSeat.class);
            if (ObjectUtil.isNull(trainSeat.getId())) {
                trainSeat.setId(SnowUtil.getSnowflakeNextId());
                trainSeat.setCreateTime(now);
                trainSeat.setUpdateTime(now);
                trainSeatMapper.insert(trainSeat);
            } else {
                trainSeat.setUpdateTime(now);
                trainSeatMapper.updateByPrimaryKey(trainSeat);
            }
        }
    
        public PageResp<TrainSeatQueryResp> queryList(TrainSeatQueryReq req) {
            TrainSeatExample trainSeatExample = new TrainSeatExample();
            trainSeatExample.setOrderByClause("train_code asc, carriage_index asc, carriage_seat_index asc");
            TrainSeatExample.Criteria criteria = trainSeatExample.createCriteria();
            if (ObjectUtil.isNotEmpty(req.getTrainCode())) {
                criteria.andTrainCodeEqualTo(req.getTrainCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<TrainSeat> trainSeatList = trainSeatMapper.selectByExample(trainSeatExample);
    
            PageInfo<TrainSeat> pageInfo = new PageInfo<>(trainSeatList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<TrainSeatQueryResp> list = BeanUtil.copyToList(trainSeatList, TrainSeatQueryResp.class);
    
            PageResp<TrainSeatQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            trainSeatMapper.deleteByPrimaryKey(id);
        }
    
        @Transactional
        public void genTrainSeat(String trainCode) {
            DateTime now = DateTime.now();
            // 清空当前车次下的所有的座位记录
            TrainSeatExample trainSeatExample = new TrainSeatExample();
            TrainSeatExample.Criteria criteria = trainSeatExample.createCriteria();
            criteria.andTrainCodeEqualTo(trainCode);
            trainSeatMapper.deleteByExample(trainSeatExample);
    
            // 查找当前车次下的所有的车厢
            List<TrainCarriage> carriageList = trainCarriageService.selectByTrainCode(trainCode);
            LOG.info("当前车次下的车厢数:{}", carriageList.size());
    
            // 循环生成每个车厢的座位
            for (TrainCarriage trainCarriage : carriageList) {
                // 拿到车厢数据:行数、座位类型(得到列数)
                Integer rowCount = trainCarriage.getRowCount();
                String seatType = trainCarriage.getSeatType();
                int seatIndex = 1;
    
                // 根据车厢的座位类型,筛选出所有的列,比如车箱类型是一等座,则筛选出columnList={ACDF}
                List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(seatType);
                LOG.info("根据车厢的座位类型,筛选出所有的列:{}", colEnumList);
    
                // 循环行数
                for (int row = 1; row <= rowCount; row++) {
                    // 循环列数
                    for (SeatColEnum seatColEnum : colEnumList) {
                        // 构造座位数据并保存数据库
                        TrainSeat trainSeat = new TrainSeat();
                        trainSeat.setId(SnowUtil.getSnowflakeNextId());
                        trainSeat.setTrainCode(trainCode);
                        trainSeat.setCarriageIndex(trainCarriage.getIndex());
                        trainSeat.setRow(StrUtil.fillBefore(String.valueOf(row), '0', 2));
                        trainSeat.setCol(seatColEnum.getCode());
                        trainSeat.setSeatType(seatType);
                        trainSeat.setCarriageSeatIndex(seatIndex++);
                        trainSeat.setCreateTime(now);
                        trainSeat.setUpdateTime(now);
                        trainSeatMapper.insert(trainSeat);
                    }
                }
            }
        }
    
        public List<TrainSeat> selectByTrainCode(String trainCode) {
            TrainSeatExample trainSeatExample = new TrainSeatExample();
            trainSeatExample.setOrderByClause("`id` asc");
            TrainSeatExample.Criteria criteria = trainSeatExample.createCriteria();
            criteria.andTrainCodeEqualTo(trainCode);
            return trainSeatMapper.selectByExample(trainSeatExample);
        }
    }
    
  • DailyTrainSeatService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjectUtil;
    import cn.hutool.core.util.StrUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.*;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import com.neilxu.train.business.mapper.DailyTrainSeatMapper;
    import com.neilxu.train.business.req.DailyTrainSeatQueryReq;
    import com.neilxu.train.business.req.DailyTrainSeatSaveReq;
    import com.neilxu.train.business.resp.DailyTrainSeatQueryResp;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class DailyTrainSeatService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainSeatService.class);
    
        @Resource
        private DailyTrainSeatMapper dailyTrainSeatMapper;
    
        @Resource
        private TrainSeatService trainSeatService;
    
        @Resource
        private TrainStationService trainStationService;
    
        public void save(DailyTrainSeatSaveReq req) {
            DateTime now = DateTime.now();
            DailyTrainSeat dailyTrainSeat = BeanUtil.copyProperties(req, DailyTrainSeat.class);
            if (ObjectUtil.isNull(dailyTrainSeat.getId())) {
                dailyTrainSeat.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainSeat.setCreateTime(now);
                dailyTrainSeat.setUpdateTime(now);
                dailyTrainSeatMapper.insert(dailyTrainSeat);
            } else {
                dailyTrainSeat.setUpdateTime(now);
                dailyTrainSeatMapper.updateByPrimaryKey(dailyTrainSeat);
            }
        }
    
        public PageResp<DailyTrainSeatQueryResp> queryList(DailyTrainSeatQueryReq req) {
            DailyTrainSeatExample dailyTrainSeatExample = new DailyTrainSeatExample();
            dailyTrainSeatExample.setOrderByClause("train_code asc, carriage_index asc, carriage_seat_index asc");
            DailyTrainSeatExample.Criteria criteria = dailyTrainSeatExample.createCriteria();
            if (ObjectUtil.isNotEmpty(req.getTrainCode())) {
                criteria.andTrainCodeEqualTo(req.getTrainCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrainSeat> dailyTrainSeatList = dailyTrainSeatMapper.selectByExample(dailyTrainSeatExample);
    
            PageInfo<DailyTrainSeat> pageInfo = new PageInfo<>(dailyTrainSeatList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainSeatQueryResp> list = BeanUtil.copyToList(dailyTrainSeatList, DailyTrainSeatQueryResp.class);
    
            PageResp<DailyTrainSeatQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            dailyTrainSeatMapper.deleteByPrimaryKey(id);
        }
    
        public void genDaily(Date date, String trainCode) {
            LOG.info("生成日期【{}】车次【{}】的座位信息开始", DateUtil.formatDate(date), trainCode);
    
            // 删除某日某车次的座位信息
            DailyTrainSeatExample dailyTrainSeatExample = new DailyTrainSeatExample();
            dailyTrainSeatExample.createCriteria()
                    .andDateEqualTo(date)
                    .andTrainCodeEqualTo(trainCode);
            dailyTrainSeatMapper.deleteByExample(dailyTrainSeatExample);
    
            List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);
            String sell = StrUtil.fillBefore("", '0', stationList.size() - 1);
    
            // 查出某车次的所有的座位信息
            List<TrainSeat> seatList = trainSeatService.selectByTrainCode(trainCode);
            if (CollUtil.isEmpty(seatList)) {
                LOG.info("该车次没有座位基础数据,生成该车次的座位信息结束");
                return;
            }
    
            for (TrainSeat trainSeat : seatList) {
                DateTime now = DateTime.now();
                DailyTrainSeat dailyTrainSeat = BeanUtil.copyProperties(trainSeat, DailyTrainSeat.class);
                dailyTrainSeat.setId(SnowUtil.getSnowflakeNextId());
                dailyTrainSeat.setCreateTime(now);
                dailyTrainSeat.setUpdateTime(now);
                dailyTrainSeat.setDate(date);
                dailyTrainSeat.setSell(sell);
                dailyTrainSeatMapper.insert(dailyTrainSeat);
            }
            LOG.info("生成日期【{}】车次【{}】的座位信息结束", DateUtil.formatDate(date), trainCode);
        }
    }
    
  • DailyTrainService.java

    package com.neilxu.train.business.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.date.DateUtil;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.business.domain.DailyTrain;
    import com.neilxu.train.business.domain.DailyTrainExample;
    import com.neilxu.train.business.domain.Train;
    import com.neilxu.train.business.mapper.DailyTrainMapper;
    import com.neilxu.train.business.req.DailyTrainQueryReq;
    import com.neilxu.train.business.req.DailyTrainSaveReq;
    import com.neilxu.train.business.resp.DailyTrainQueryResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class DailyTrainService {
    
        private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class);
    
        @Resource
        private DailyTrainMapper dailyTrainMapper;
    
        @Resource
        private TrainService trainService;
    
        @Resource
        private DailyTrainStationService dailyTrainStationService;
    
        @Resource
        private DailyTrainCarriageService dailyTrainCarriageService;
    
        @Resource
        private DailyTrainSeatService dailyTrainSeatService;
    
        public void save(DailyTrainSaveReq req) {
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(req, DailyTrain.class);
            if (ObjectUtil.isNull(dailyTrain.getId())) {
                dailyTrain.setId(SnowUtil.getSnowflakeNextId());
                dailyTrain.setCreateTime(now);
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.insert(dailyTrain);
            } else {
                dailyTrain.setUpdateTime(now);
                dailyTrainMapper.updateByPrimaryKey(dailyTrain);
            }
        }
    
        public PageResp<DailyTrainQueryResp> queryList(DailyTrainQueryReq req) {
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.setOrderByClause("date desc, code asc");
            DailyTrainExample.Criteria criteria = dailyTrainExample.createCriteria();
            if (ObjectUtil.isNotNull(req.getDate())) {
                criteria.andDateEqualTo(req.getDate());
            }
            if (ObjectUtil.isNotEmpty(req.getCode())) {
                criteria.andCodeEqualTo(req.getCode());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<DailyTrain> dailyTrainList = dailyTrainMapper.selectByExample(dailyTrainExample);
    
            PageInfo<DailyTrain> pageInfo = new PageInfo<>(dailyTrainList);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<DailyTrainQueryResp> list = BeanUtil.copyToList(dailyTrainList, DailyTrainQueryResp.class);
    
            PageResp<DailyTrainQueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            dailyTrainMapper.deleteByPrimaryKey(id);
        }
    
        /**
         * 生成某日所有车次信息,包括车次、车站、车厢、座位
         * @param date
         */
        public void genDaily(Date date) {
            List<Train> trainList = trainService.selectAll();
            if (CollUtil.isEmpty(trainList)) {
                LOG.info("没有车次基础数据,任务结束");
                return;
            }
    
            for (Train train : trainList) {
                genDailyTrain(date, train);
            }
        }
    
        public void genDailyTrain(Date date, Train train) {
            LOG.info("生成日期【{}】车次【{}】的信息开始", DateUtil.formatDate(date), train.getCode());
            // 删除该车次已有的数据
            DailyTrainExample dailyTrainExample = new DailyTrainExample();
            dailyTrainExample.createCriteria()
                    .andDateEqualTo(date)
                    .andCodeEqualTo(train.getCode());
            dailyTrainMapper.deleteByExample(dailyTrainExample);
    
            // 生成该车次的数据
            DateTime now = DateTime.now();
            DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class);
            dailyTrain.setId(SnowUtil.getSnowflakeNextId());
            dailyTrain.setCreateTime(now);
            dailyTrain.setUpdateTime(now);
            dailyTrain.setDate(date);
            dailyTrainMapper.insert(dailyTrain);
    
            // 生成该车次的车站数据
            dailyTrainStationService.genDaily(date, train.getCode());
    
            // 生成该车次的车厢数据
            dailyTrainCarriageService.genDaily(date, train.getCode());
    
            // 生成该车次的座位数据
            dailyTrainSeatService.genDaily(date, train.getCode());
    
            LOG.info("生成日期【{}】车次【{}】的信息结束", DateUtil.formatDate(date), train.getCode());
        }
    }
    
  • 测试

    注意:要先生成火车座位,再生成每日座位

在这里插入图片描述

10.增加手动生成某日车次数据功能

前端加按钮,选择日期,手动生成某日的车次数据

  • daily-train.vue

    <template>
      <p>
        <a-space>
          <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
          <train-select-view v-model="params.code" width="200px"></train-select-view>
          <a-button type="primary" @click="handleQuery()">查找</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
          <a-button type="danger" @click="onClickGenDaily">手动生成车次信息</a-button>
        </a-space>
      </p>
      <a-table :dataSource="dailyTrains"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'type'">
            <span v-for="item in TRAIN_TYPE_ARRAY" :key="item.code">
              <span v-if="item.code === record.type">
                {{item.desc}}
              </span>
            </span>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="每日车次" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="dailyTrain" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="日期">
            <a-date-picker v-model:value="dailyTrain.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
          </a-form-item>
          <a-form-item label="车次编号">
            <train-select-view v-model="dailyTrain.code" @change="onChangeCode"></train-select-view>
          </a-form-item>
          <a-form-item label="车次类型">
            <a-select v-model:value="dailyTrain.type">
              <a-select-option v-for="item in TRAIN_TYPE_ARRAY" :key="item.code" :value="item.code">
                {{item.desc}}
              </a-select-option>
            </a-select>
          </a-form-item>
          <a-form-item label="始发站">
            <a-input v-model:value="dailyTrain.start" />
          </a-form-item>
          <a-form-item label="始发站拼音">
            <a-input v-model:value="dailyTrain.startPinyin" />
          </a-form-item>
          <a-form-item label="出发时间">
            <a-time-picker v-model:value="dailyTrain.startTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
          <a-form-item label="终点站">
            <a-input v-model:value="dailyTrain.end" />
          </a-form-item>
          <a-form-item label="终点站拼音">
            <a-input v-model:value="dailyTrain.endPinyin" />
          </a-form-item>
          <a-form-item label="到站时间">
            <a-time-picker v-model:value="dailyTrain.endTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
          </a-form-item>
        </a-form>
      </a-modal>
      <a-modal v-model:visible="genDailyVisible" title="生成车次" @ok="handleGenDailyOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="genDaily" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="日期">
            <a-date-picker v-model:value="genDaily.date" placeholder="请选择日期"/>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    import TrainSelectView from "@/components/train-select";
    import dayjs from 'dayjs';
    
    export default defineComponent({
      name: "daily-train-view",
      components: {TrainSelectView},
      setup() {
        const TRAIN_TYPE_ARRAY = window.TRAIN_TYPE_ARRAY;
        const visible = ref(false);
        let dailyTrain = ref({
          id: undefined,
          date: undefined,
          code: undefined,
          type: undefined,
          start: undefined,
          startPinyin: undefined,
          startTime: undefined,
          end: undefined,
          endPinyin: undefined,
          endTime: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dailyTrains = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        let params = ref({
          code: null
        });
        const genDaily = ref({
          date: null
        });
        const genDailyVisible = ref(false);
        const columns = [
          {
            title: '日期',
            dataIndex: 'date',
            key: 'date',
          },
          {
            title: '车次编号',
            dataIndex: 'code',
            key: 'code',
          },
          {
            title: '车次类型',
            dataIndex: 'type',
            key: 'type',
          },
          {
            title: '始发站',
            dataIndex: 'start',
            key: 'start',
          },
          {
            title: '始发站拼音',
            dataIndex: 'startPinyin',
            key: 'startPinyin',
          },
          {
            title: '出发时间',
            dataIndex: 'startTime',
            key: 'startTime',
          },
          {
            title: '终点站',
            dataIndex: 'end',
            key: 'end',
          },
          {
            title: '终点站拼音',
            dataIndex: 'endPinyin',
            key: 'endPinyin',
          },
          {
            title: '到站时间',
            dataIndex: 'endTime',
            key: 'endTime',
          },
          {
            title: '操作',
            dataIndex: 'operation'
          }
        ];
    
        const onAdd = () => {
          dailyTrain.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          dailyTrain.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/business/admin/daily-train/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/business/admin/daily-train/save", dailyTrain.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/business/admin/daily-train/query-list", {
            params: {
              page: param.page,
              size: param.size,
              code: params.value.code,
              date: params.value.date
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              dailyTrains.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        const onChangeCode = (train) => {
          console.log("车次下拉组件选择:", train);
          let t = Tool.copy(train);
          delete t.id;
          // 用assign可以合并
          dailyTrain.value = Object.assign(dailyTrain.value, t);
        };
    
        const onClickGenDaily = () => {
          genDailyVisible.value = true;
        };
    
        const handleGenDailyOk = () => {
          let date = dayjs(genDaily.value.date).format("YYYY-MM-DD");
          axios.get("/business/admin/daily-train/gen-daily/" + date).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "生成成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          TRAIN_TYPE_ARRAY,
          dailyTrain,
          visible,
          dailyTrains,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onAdd,
          handleOk,
          onEdit,
          onDelete,
          onChangeCode,
          params,
          genDaily,
          genDailyVisible,
          handleGenDailyOk,
          onClickGenDaily
        };
      },
    });
    </script>
    
  • 测试

在这里插入图片描述

生成也没有问题

  • 修改生成器,前端支持自定义分页pageSize

    优化下分页

    • daily-train-seat.vue
      <template>
        <p>
          <a-space>
            <train-select-view v-model="params.trainCode" width="200px"></train-select-view>
            <a-button type="primary" @click="handleQuery()">查找</a-button>
          </a-space>
        </p>
        <a-table :dataSource="dailyTrainSeats"
                 :columns="columns"
                 :pagination="pagination"
                 @change="handleTableChange"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
            </template>
            <template v-else-if="column.dataIndex === 'col'">
              <span v-for="item in SEAT_COL_ARRAY" :key="item.code">
                <span v-if="item.code === record.col && item.type === record.seatType">
                  {{item.desc}}
                </span>
              </span>
            </template>
            <template v-else-if="column.dataIndex === 'seatType'">
              <span v-for="item in SEAT_TYPE_ARRAY" :key="item.code">
                <span v-if="item.code === record.seatType">
                  {{item.desc}}
                </span>
              </span>
            </template>
          </template>
        </a-table>
      </template>
      
      <script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";
      import TrainSelectView from "@/components/train-select";
      
      export default defineComponent({
        name: "daily-train-seat-view",
        components: {TrainSelectView},
        setup() {
          const SEAT_COL_ARRAY = window.SEAT_COL_ARRAY;
          const SEAT_TYPE_ARRAY = window.SEAT_TYPE_ARRAY;
          const visible = ref(false);
          let dailyTrainSeat = ref({
            id: undefined,
            date: undefined,
            trainCode: undefined,
            carriageIndex: undefined,
            row: undefined,
            col: undefined,
            seatType: undefined,
            carriageSeatIndex: undefined,
            sell: undefined,
            createTime: undefined,
            updateTime: undefined,
          });
          const dailyTrainSeats = ref([]);
          // 分页的三个属性名是固定的
          const pagination = ref({
            total: 0,
            current: 1,
            pageSize: 10,
          });
          let loading = ref(false);
          let params = ref({
            trainCode: null
          });
          const columns = [
            {
              title: '日期',
              dataIndex: 'date',
              key: 'date',
            },
            {
              title: '车次编号',
              dataIndex: 'trainCode',
              key: 'trainCode',
            },
            {
              title: '箱序',
              dataIndex: 'carriageIndex',
              key: 'carriageIndex',
            },
            {
              title: '排号',
              dataIndex: 'row',
              key: 'row',
            },
            {
              title: '列号',
              dataIndex: 'col',
              key: 'col',
            },
            {
              title: '座位类型',
              dataIndex: 'seatType',
              key: 'seatType',
            },
            {
              title: '同车箱座序',
              dataIndex: 'carriageSeatIndex',
              key: 'carriageSeatIndex',
            },
            {
              title: '售卖情况',
              dataIndex: 'sell',
              key: 'sell',
            },
          ];
      
      
          const handleQuery = (param) => {
            if (!param) {
              param = {
                page: 1,
                size: pagination.value.pageSize
              };
            }
            loading.value = true;
            axios.get("/business/admin/daily-train-seat/query-list", {
              params: {
                page: param.page,
                size: param.size,
                trainCode: params.value.trainCode
              }
            }).then((response) => {
              loading.value = false;
              let data = response.data;
              if (data.success) {
                dailyTrainSeats.value = data.content.list;
                // 设置分页控件的值
                pagination.value.current = param.page;
                pagination.value.total = data.content.total;
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleTableChange = (page) => {
            // console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
            pagination.value.pageSize = page.pageSize;
            handleQuery({
              page: page.current,
              size: page.pageSize
            });
          };
      
          onMounted(() => {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          });
      
          return {
            SEAT_COL_ARRAY,
            SEAT_TYPE_ARRAY,
            dailyTrainSeat,
            visible,
            dailyTrainSeats,
            pagination,
            columns,
            handleTableChange,
            handleQuery,
            loading,
            params
          };
        },
      });
      </script>
      
    • vue.ftl

      <template>
        <p>
          <a-space>
            <a-button type="primary" @click="handleQuery()">刷新</a-button>
            <#if !readOnly><a-button type="primary" @click="onAdd">新增</a-button></#if>
          </a-space>
        </p>
        <a-table :dataSource="${domain}s"
                 :columns="columns"
                 :pagination="pagination"
                 @change="handleTableChange"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
              <#if !readOnly>
                <a-space>
                  <a-popconfirm
                          title="删除后不可恢复,确认删除?"
                          @confirm="onDelete(record)"
                          ok-text="确认" cancel-text="取消">
                    <a style="color: red">删除</a>
                  </a-popconfirm>
                  <a @click="onEdit(record)">编辑</a>
                </a-space>
              </#if>
            </template>
            <#list fieldList as field>
              <#if field.enums>
                <template v-else-if="column.dataIndex === '${field.nameHump}'">
              <span v-for="item in ${field.enumsConst}_ARRAY" :key="item.code">
                <span v-if="item.code === record.${field.nameHump}">
                  {{item.desc}}
                </span>
              </span>
                </template>
              </#if>
            </#list>
          </template>
        </a-table>
        <#if !readOnly>
          <a-modal v-model:visible="visible" title="${tableNameCn}" @ok="handleOk"
                   ok-text="确认" cancel-text="取消">
            <a-form :model="${domain}" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
              <#list fieldList as field>
                <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
                  <a-form-item label="${field.nameCn}">
                    <#if field.enums>
                      <a-select v-model:value="${domain}.${field.nameHump}">
                        <a-select-option v-for="item in ${field.enumsConst}_ARRAY" :key="item.code" :value="item.code">
                          {{item.desc}}
                        </a-select-option>
                      </a-select>
                    <#elseif field.javaType=='Date'>
                      <#if field.type=='time'>
                        <a-time-picker v-model:value="${domain}.${field.nameHump}" valueFormat="HH:mm:ss" placeholder="请选择时间" />
                      <#elseif field.type=='date'>
                        <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
                      <#else>
                        <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD HH:mm:ss" show-time placeholder="请选择日期" />
                      </#if>
                    <#else>
                      <a-input v-model:value="${domain}.${field.nameHump}" />
                    </#if>
                  </a-form-item>
                </#if>
              </#list>
            </a-form>
          </a-modal>
        </#if>
      </template>
      
      <script>
        import { defineComponent, ref, onMounted } from 'vue';
        import {notification} from "ant-design-vue";
        import axios from "axios";
      
        export default defineComponent({
          name: "${do_main}-view",
          setup() {
            <#list fieldList as field>
            <#if field.enums>
            const ${field.enumsConst}_ARRAY = window.${field.enumsConst}_ARRAY;
            </#if>
            </#list>
            const visible = ref(false);
            let ${domain} = ref({
              <#list fieldList as field>
              ${field.nameHump}: undefined,
              </#list>
            });
            const ${domain}s = ref([]);
            // 分页的三个属性名是固定的
            const pagination = ref({
              total: 0,
              current: 1,
              pageSize: 10,
            });
            let loading = ref(false);
            const columns = [
              <#list fieldList as field>
              <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
              {
                title: '${field.nameCn}',
                dataIndex: '${field.nameHump}',
                key: '${field.nameHump}',
              },
              </#if>
              </#list>
              <#if !readOnly>
              {
                title: '操作',
                dataIndex: 'operation'
              }
              </#if>
            ];
      
            <#if !readOnly>
            const onAdd = () => {
              ${domain}.value = {};
              visible.value = true;
            };
      
            const onEdit = (record) => {
              ${domain}.value = window.Tool.copy(record);
              visible.value = true;
            };
      
            const onDelete = (record) => {
              axios.delete("/${module}/admin/${do_main}/delete/" + record.id).then((response) => {
                const data = response.data;
                if (data.success) {
                  notification.success({description: "删除成功!"});
                  handleQuery({
                    page: pagination.value.current,
                    size: pagination.value.pageSize,
                  });
                } else {
                  notification.error({description: data.message});
                }
              });
            };
      
            const handleOk = () => {
              axios.post("/${module}/admin/${do_main}/save", ${domain}.value).then((response) => {
                let data = response.data;
                if (data.success) {
                  notification.success({description: "保存成功!"});
                  visible.value = false;
                  handleQuery({
                    page: pagination.value.current,
                    size: pagination.value.pageSize
                  });
                } else {
                  notification.error({description: data.message});
                }
              });
            };
            </#if>
      
            const handleQuery = (param) => {
              if (!param) {
                param = {
                  page: 1,
                  size: pagination.value.pageSize
                };
              }
              loading.value = true;
              axios.get("/${module}/admin/${do_main}/query-list", {
                params: {
                  page: param.page,
                  size: param.size
                }
              }).then((response) => {
                loading.value = false;
                let data = response.data;
                if (data.success) {
                  ${domain}s.value = data.content.list;
                  // 设置分页控件的值
                  pagination.value.current = param.page;
                  pagination.value.total = data.content.total;
                } else {
                  notification.error({description: data.message});
                }
              });
            };
      
            const handleTableChange = (page) => {
              // console.log("看看自带的分页参数都有啥:" + JSON.stringify(page));
              pagination.value.pageSize = page.pageSize;
              handleQuery({
                page: page.current,
                size: page.pageSize
              });
            };
      
            onMounted(() => {
              handleQuery({
                page: 1,
                size: pagination.value.pageSize
              });
            });
      
            return {
              <#list fieldList as field>
              <#if field.enums>
              ${field.enumsConst}_ARRAY,
              </#if>
              </#list>
              ${domain},
              visible,
              ${domain}s,
              pagination,
              columns,
              handleTableChange,
              handleQuery,
              loading,
              <#if !readOnly>
              onAdd,
              handleOk,
              onEdit,
              onDelete
              </#if>
            };
          },
        });
      </script>
      
    • 效果:

在这里插入图片描述

  • 手动生成车次功能增加loading,防止重复点击

    • daily-train.vue

      <template>
        <p>
          <a-space>
            <a-date-picker v-model:value="params.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
            <train-select-view v-model="params.code" width="200px"></train-select-view>
            <a-button type="primary" @click="handleQuery()">查询</a-button>
            <a-button type="primary" @click="onAdd">新增</a-button>
            <a-button type="danger" @click="onClickGenDaily">手动生成车次信息</a-button>
          </a-space>
        </p>
        <a-table :dataSource="dailyTrains"
                 :columns="columns"
                 :pagination="pagination"
                 @change="handleTableChange"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
              <a-space>
                <a-popconfirm
                    title="删除后不可恢复,确认删除?"
                    @confirm="onDelete(record)"
                    ok-text="确认" cancel-text="取消">
                  <a style="color: red">删除</a>
                </a-popconfirm>
                <a @click="onEdit(record)">编辑</a>
              </a-space>
            </template>
            <template v-else-if="column.dataIndex === 'type'">
              <span v-for="item in TRAIN_TYPE_ARRAY" :key="item.code">
                <span v-if="item.code === record.type">
                  {{item.desc}}
                </span>
              </span>
            </template>
          </template>
        </a-table>
        <a-modal v-model:visible="visible" title="每日车次" @ok="handleOk"
                 ok-text="确认" cancel-text="取消">
          <a-form :model="dailyTrain" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
            <a-form-item label="日期">
              <a-date-picker v-model:value="dailyTrain.date" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
            </a-form-item>
            <a-form-item label="车次编号">
              <train-select-view v-model="dailyTrain.code" @change="onChangeCode"></train-select-view>
            </a-form-item>
            <a-form-item label="车次类型">
              <a-select v-model:value="dailyTrain.type">
                <a-select-option v-for="item in TRAIN_TYPE_ARRAY" :key="item.code" :value="item.code">
                  {{item.desc}}
                </a-select-option>
              </a-select>
            </a-form-item>
            <a-form-item label="始发站">
              <a-input v-model:value="dailyTrain.start" />
            </a-form-item>
            <a-form-item label="始发站拼音">
              <a-input v-model:value="dailyTrain.startPinyin" />
            </a-form-item>
            <a-form-item label="出发时间">
              <a-time-picker v-model:value="dailyTrain.startTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
            </a-form-item>
            <a-form-item label="终点站">
              <a-input v-model:value="dailyTrain.end" />
            </a-form-item>
            <a-form-item label="终点站拼音">
              <a-input v-model:value="dailyTrain.endPinyin" />
            </a-form-item>
            <a-form-item label="到站时间">
              <a-time-picker v-model:value="dailyTrain.endTime" valueFormat="HH:mm:ss" placeholder="请选择时间" />
            </a-form-item>
          </a-form>
        </a-modal>
        <a-modal v-model:visible="genDailyVisible" title="生成车次" @ok="handleGenDailyOk"
                 :confirm-loading="genDailyLoading" ok-text="确认" cancel-text="取消">
          <a-form :model="genDaily" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
            <a-form-item label="日期">
              <a-date-picker v-model:value="genDaily.date" placeholder="请选择日期"/>
            </a-form-item>
          </a-form>
        </a-modal>
      </template>
      
      <script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";
      import TrainSelectView from "@/components/train-select";
      import dayjs from 'dayjs';
      
      export default defineComponent({
        name: "daily-train-view",
        components: {TrainSelectView},
        setup() {
          const TRAIN_TYPE_ARRAY = window.TRAIN_TYPE_ARRAY;
          const visible = ref(false);
          let dailyTrain = ref({
            id: undefined,
            date: undefined,
            code: undefined,
            type: undefined,
            start: undefined,
            startPinyin: undefined,
            startTime: undefined,
            end: undefined,
            endPinyin: undefined,
            endTime: undefined,
            createTime: undefined,
            updateTime: undefined,
          });
          const dailyTrains = ref([]);
          // 分页的三个属性名是固定的
          const pagination = ref({
            total: 0,
            current: 1,
            pageSize: 10,
          });
          let loading = ref(false);
          let params = ref({
            code: null
          });
          const genDaily = ref({
            date: null
          });
          const genDailyVisible = ref(false);
          const genDailyLoading = ref(false);
          const columns = [
            {
              title: '日期',
              dataIndex: 'date',
              key: 'date',
            },
            {
              title: '车次编号',
              dataIndex: 'code',
              key: 'code',
            },
            {
              title: '车次类型',
              dataIndex: 'type',
              key: 'type',
            },
            {
              title: '始发站',
              dataIndex: 'start',
              key: 'start',
            },
            {
              title: '始发站拼音',
              dataIndex: 'startPinyin',
              key: 'startPinyin',
            },
            {
              title: '出发时间',
              dataIndex: 'startTime',
              key: 'startTime',
            },
            {
              title: '终点站',
              dataIndex: 'end',
              key: 'end',
            },
            {
              title: '终点站拼音',
              dataIndex: 'endPinyin',
              key: 'endPinyin',
            },
            {
              title: '到站时间',
              dataIndex: 'endTime',
              key: 'endTime',
            },
            {
              title: '操作',
              dataIndex: 'operation'
            }
          ];
      
          const onAdd = () => {
            dailyTrain.value = {};
            visible.value = true;
          };
      
          const onEdit = (record) => {
            dailyTrain.value = window.Tool.copy(record);
            visible.value = true;
          };
      
          const onDelete = (record) => {
            axios.delete("/business/admin/daily-train/delete/" + record.id).then((response) => {
              const data = response.data;
              if (data.success) {
                notification.success({description: "删除成功!"});
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize,
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleOk = () => {
            axios.post("/business/admin/daily-train/save", dailyTrain.value).then((response) => {
              let data = response.data;
              if (data.success) {
                notification.success({description: "保存成功!"});
                visible.value = false;
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleQuery = (param) => {
            if (!param) {
              param = {
                page: 1,
                size: pagination.value.pageSize
              };
            }
            loading.value = true;
            axios.get("/business/admin/daily-train/query-list", {
              params: {
                page: param.page,
                size: param.size,
                code: params.value.code,
                date: params.value.date
              }
            }).then((response) => {
              loading.value = false;
              let data = response.data;
              if (data.success) {
                dailyTrains.value = data.content.list;
                // 设置分页控件的值
                pagination.value.current = param.page;
                pagination.value.total = data.content.total;
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleTableChange = (pagination) => {
            // console.log("看看自带的分页参数都有啥:" + pagination);
            handleQuery({
              page: pagination.current,
              size: pagination.pageSize
            });
          };
      
          const onChangeCode = (train) => {
            console.log("车次下拉组件选择:", train);
            let t = Tool.copy(train);
            delete t.id;
            // 用assign可以合并
            dailyTrain.value = Object.assign(dailyTrain.value, t);
          };
      
          const onClickGenDaily = () => {
            genDailyVisible.value = true;
          };
      
          const handleGenDailyOk = () => {
            let date = dayjs(genDaily.value.date).format("YYYY-MM-DD");
            genDailyLoading.value = true;
            axios.get("/business/admin/daily-train/gen-daily/" + date).then((response) => {
              genDailyLoading.value = false;
              let data = response.data;
              if (data.success) {
                notification.success({description: "生成成功!"});
                genDailyVisible.value = false;
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          onMounted(() => {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          });
      
          return {
            TRAIN_TYPE_ARRAY,
            dailyTrain,
            visible,
            dailyTrains,
            pagination,
            columns,
            handleTableChange,
            handleQuery,
            loading,
            onAdd,
            handleOk,
            onEdit,
            onDelete,
            onChangeCode,
            params,
            genDaily,
            genDailyVisible,
            handleGenDailyOk,
            onClickGenDaily,
            genDailyLoading
          };
        },
      });
      </script>
      
    • 效果:

      转圈

在这里插入图片描述

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值