微服務(wù)開發(fā)系列 第十一篇:XXL-JOB

總概

A、技術(shù)棧
  • 開發(fā)語言:Java 1.8
  • 數(shù)據(jù)庫:MySQL、Redis、MongoDB、Elasticsearch
  • 微服務(wù)框架:Spring Cloud Alibaba
  • 微服務(wù)網(wǎng)關(guān):Spring Cloud Gateway
  • 服務(wù)注冊和配置中心:Nacos
  • 分布式事務(wù):Seata
  • 鏈路追蹤框架:Sleuth
  • 服務(wù)降級與熔斷:Sentinel
  • ORM框架:MyBatis-Plus
  • 分布式任務(wù)調(diào)度平臺:XXL-JOB
  • 消息中間件:RocketMQ
  • 分布式鎖:Redisson
  • 權(quán)限:OAuth2
  • DevOps:Jenkins、Docker、K8S
B、源碼地址

alanchenyan/ac-mall2-cloud

C、本節(jié)實(shí)現(xiàn)目標(biāo)
  • 搭建xxl-job環(huán)境
  • xxl-job-admin平臺創(chuàng)建定時任務(wù)
  • 動態(tài)創(chuàng)建定時任務(wù),實(shí)現(xiàn)動態(tài)創(chuàng)建15分鐘未支付自動關(guān)閉訂單的定時任務(wù)
D、系列

一、部署xxl-job

1.1 下載xxl-job源碼

下載地址:https://github.com/xuxueli/xxl-job

xxl-job源碼
1.2 初始化“調(diào)度數(shù)據(jù)庫”

調(diào)度數(shù)據(jù)庫初始化SQL腳本” 位置為:/xxl-job/doc/db/tables_xxl_job.sql

調(diào)度中心支持集群部署,集群情況下各節(jié)點(diǎn)務(wù)必連接同一個MySQL實(shí)例。如果MySQL做主從,調(diào)度中心集群節(jié)點(diǎn)務(wù)必強(qiáng)制走主庫。

1.3 部署調(diào)度中心 xxl-job-admin
1.3.1 xxl-job-admin項(xiàng)目

xxl-job源碼里有3個項(xiàng)目:xxl-job-admin、xxl-job-core、xxl-job-executor-samples,

  • xxl-job-admin:調(diào)度中心
  • xxl-job-core:公共依賴
  • xxl-job-executor-samples:執(zhí)行器Sample示例(選擇合適的版本執(zhí)行器,可直接使用,也可以參考其并將現(xiàn)有項(xiàng)目改造成執(zhí)行器)

我們部署調(diào)度中心需要用到xxl-job-admin

1.3.2 啟動xxl-job-admin

xxl-job-admin調(diào)度中心項(xiàng)目用IDEA打開,配置文件地址:/xxl-job/xxl-job-admin/src/main/resources/application.properties

我們修改三個地方:

  • 端口:端口默認(rèn)是8080,這里我們將端口改成8081
  • 數(shù)據(jù)庫配置:修改數(shù)據(jù)庫地址和賬號密碼
  • accessToken:默認(rèn)是default_token

修改完后啟動xxl-job-admin

修改端口
修改數(shù)據(jù)庫地址和賬號密碼
修改accessToken
1.3.3 訪問xxl-job-admin

調(diào)度中心訪問地址:http://localhost:8081/xxl-job-admin ,該地址執(zhí)行器將會使用到,作為回調(diào)地址。

默認(rèn)登錄賬號 “admin/123456”, 登錄后運(yùn)行界面如下圖所示。

xxl-job-admin

二、配置部署“執(zhí)行器項(xiàng)目”

執(zhí)行器項(xiàng)目:xxl-job-executor-sample-springboot,可直接使用,也可以參考其并將現(xiàn)有項(xiàng)目改造成執(zhí)行器。

執(zhí)行器項(xiàng)目作用:負(fù)責(zé)接收“調(diào)度中心”的調(diào)度并執(zhí)行;可直接部署執(zhí)行器,也可以將執(zhí)行器集成到現(xiàn)有業(yè)務(wù)項(xiàng)目中。

我們不用xxl-job-executor-sample-springboot,而是直接用mall-order項(xiàng)目來做為執(zhí)行器項(xiàng)目。

2.1 新增執(zhí)行管理器

先在xxl-job-admin上新增一個執(zhí)行管理器:executor-order

新增執(zhí)行管理器
新增執(zhí)行管理器
2.2 maven依賴

在mall-pom項(xiàng)目的pom.xml里引入xxl-job-core的maven依賴

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>${xxljob.version}</version>
</dependency>
xxl-job-core
2.3 執(zhí)行器組件配置

執(zhí)行器組件,配置文件地址:/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java

將該代碼復(fù)制到mall-order項(xiàng)目

XxlJobConfig
2.4 配置xxl-job連接信息

修改mall-order服務(wù)里的bootstrap-dev.yml配置信息

server:
  port: 7030

spring:
  application:
    name: mall-order

  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: dev_id
        file-extension: yml
        shared-configs:
          - data-id: common.yml
            group: DEFAULT_GROUP
            refresh: true
      discovery:
        namespace: dev_id

swagger:
  enabled: true
  title: 訂單服務(wù)
  basePackage: com.ac.order.controller
  version: 1.0
  description: 訂單服務(wù)相關(guān)接口

xxl:
  job:
    # 執(zhí)行器通訊TOKEN [選填]:非空時啟用;
    accessToken: 123456
    admin:
      # 調(diào)度中心部署跟地址 [選填]:如調(diào)度中心集群部署存在多個地址則用逗號分隔。執(zhí)行器將會使用該地址進(jìn)行"執(zhí)行器心跳注冊"和"任務(wù)結(jié)果回調(diào)";為空則關(guān)閉自動注冊;
      addresses: http://127.0.0.1:8081/xxl-job-admin
    executor:
      # 執(zhí)行器AppName [選填]:執(zhí)行器心跳注冊分組依據(jù);為空則關(guān)閉自動注冊
      app-name: executor-order
      # 執(zhí)行器注冊 [選填]:優(yōu)先使用該配置作為注冊地址,為空時使用內(nèi)嵌服務(wù) ”IP:PORT“ 作為注冊地址。從而更靈活的支持容器類型執(zhí)行器動態(tài)IP和動態(tài)映射端口問題。
      address: ''
      # 執(zhí)行器IP [選填]:默認(rèn)為空表示自動獲取IP,多網(wǎng)卡時可手動設(shè)置指定IP,該IP不會綁定Host僅作為通訊實(shí)用;地址信息用于 "執(zhí)行器注冊" 和 "調(diào)度中心請求并觸發(fā)任務(wù)";
      ip: ''
      # 執(zhí)行器端口號 [選填]:小于等于0則自動獲取;默認(rèn)端口為9999,單機(jī)部署多個執(zhí)行器時,注意要配置不同執(zhí)行器端口;
      port: -1
      # 執(zhí)行器運(yùn)行日志文件存儲磁盤路徑 [選填] :需要對該路徑擁有讀寫權(quán)限;為空則使用默認(rèn)路徑;
      log-path: /data/logs/task-log
      # 執(zhí)行器日志保存天數(shù) [選填] :值大于3時生效,啟用執(zhí)行器Log文件定期清理功能,否則不生效;
      log-retention-days: -1
修改配置
2.5 啟動mall-order服務(wù)

啟動mall-order服務(wù),mall-order服務(wù)會自動注冊到executor-order執(zhí)行器下,此時,mall-order就是一個執(zhí)行器項(xiàng)目。

executor-order執(zhí)行器

三、xxl-job-admin平臺創(chuàng)建定時任務(wù)

BEAN模式(方法形式),xxl-job-admin平臺創(chuàng)建定時任務(wù)

3.1 新建定時任務(wù)執(zhí)行方法類
/**
 * @author Alan Chen
 * @description xxl-job-admin平臺創(chuàng)建定時任務(wù)
 * @date 2023/05/17
 */
@Slf4j
@Component
public class TaskByAdminCreateJob {

    @XxlJob(value = XXLJobHandlerConstant.TASK_BY_ADMIN_CREATE)
    public void doJob() {
        try {
            // 獲取任務(wù)ID
            long jobId = XxlJobHelper.getJobId();
            log.info("TaskByTimeJob doJob,jobId={},param={}", jobId, XxlJobHelper.getJobParam());

            // 獲取任務(wù)參數(shù)
            String[] params = StrUtil.splitToArray(XxlJobHelper.getJobParam(), ',');
            if (params.length <= 1) {
                String error = StrUtil.format("TaskByTimeJob.doJob,失敗, 原因: 參數(shù)缺失, 任務(wù)ID: {}", jobId);
                log.info(error);
                XxlJobHelper.handleFail(error);
                return;
            }

            // 業(yè)務(wù)邏輯
            String memberId = params[0];
            String memberName = params[1];
            String logInfo = StrUtil.format("TaskByTimeJob.doJob,成功,memberId={},memberName={}", memberId, memberName);
            log.info(logInfo);
            XxlJobHelper.handleSuccess(logInfo);
        } catch (Exception e) {
            String error = StrUtil.format("TaskByTimeJob.doJob,失敗, msg={}", e.getMessage());
            log.info(error);
            XxlJobHelper.handleFail(error);
        }
    }
}
package com.ac.common.constant;

public class XXLJobHandlerConstant {

    /**
     * xxl-job-admin平臺創(chuàng)建定時任務(wù)
     */
    public static final String TASK_BY_ADMIN_CREATE = "TASK_BY_ADMIN_CREATE";
}
定時執(zhí)行任務(wù)
3.2 新建任務(wù)管理
新建任務(wù)管理
設(shè)置執(zhí)行時間

配置JobHandler,和TaskByAdminCreateJob里配置的@XxlJob保持一致

配置

任務(wù)新建完后,需要手動啟動


啟動
3.3 執(zhí)行任務(wù)

啟動狀態(tài)下的任務(wù),可以立即執(zhí)行一次


執(zhí)行一次
執(zhí)行參數(shù)
后臺打印結(jié)果

四、代碼動態(tài)創(chuàng)建定時任務(wù)

4.1 背景說明

xxl-job-admin平臺手動創(chuàng)建定時任務(wù),使用起來雖然方便,可以有時候,我們就是需要在代碼中動態(tài)創(chuàng)建一個定時任務(wù),而不是到頁面上進(jìn)行配置。比如用戶下單后,我們需要動態(tài)創(chuàng)建一個15分鐘未支付自動關(guān)閉訂單的定時任務(wù)。

4.2 xxljob接口梳理

我們先到github上拉一份xxl-job的源碼下來,結(jié)合著文檔和代碼,先梳理一下各個模塊都是干什么的:

  • xxl-job-admin:任務(wù)調(diào)度中心,啟動后就可以訪問管理頁面,進(jìn)行執(zhí)行器和任務(wù)的注冊、以及任務(wù)調(diào)用等功能了

  • xxl-job-core:公共依賴,項(xiàng)目中使用到xxl-job時要引入的依賴包

  • xxl-job-executor-samples:執(zhí)行示例,分別包含了springboot版本和不使用框架的版本

為了弄清楚注冊和查詢executor和jobHandler調(diào)用的是哪些接口,我們先從頁面上去抓一個請求看看:

執(zhí)行管理器接口

好了,這樣就能定位到xxl-job-admin模塊中xxl-job-admin/jobgroup/pageList這個接口。

按照這個思路,可以找到下面這幾個關(guān)鍵接口:

/jobgroup/pageList:執(zhí)行器列表的條件查詢
/jobgroup/save:添加執(zhí)行器
/jobinfo/pageList:任務(wù)列表的條件查詢
/jobinfo/add:添加任務(wù)

但是如果直接調(diào)用這些接口,那么就會發(fā)現(xiàn)它會跳轉(zhuǎn)到xxl-job-admin的的登錄頁面。

其實(shí)想想也明白,出于安全性考慮,調(diào)度中心的接口也不可能允許裸調(diào)的。那么再回頭看一下剛才頁面上的請求就會發(fā)現(xiàn),它在Headers中添加了一條名為XXL_JOB_LOGIN_IDENTITY的cookie:

cookie

至于這條cookie,則是在通過用戶名和密碼調(diào)用調(diào)度中心的/login接口時返回的,在返回的response可以直接拿到。只要保存下來,并在之后每次請求時攜帶,就能夠正常訪問其他接口了。

到這里,我們需要的5個接口就基本準(zhǔn)備齊了,接下來準(zhǔn)備開始正式的改造工作。

4.3 動態(tài)創(chuàng)建定時任務(wù)實(shí)現(xiàn)
4.3.1 XxlJobInfo和XxlJobGroup類

在調(diào)用調(diào)度中心的接口前,先把xxl-job-admin模塊中的XxlJobInfo和XxlJobGroup這兩個類拿到我們的mall-common項(xiàng)目中,用于接收接口調(diào)用的結(jié)果。

XxlJobGroup
4.3.2 登錄接口

在調(diào)用業(yè)務(wù)接口前,需要通過登錄接口獲取cookie,并在獲取到cookie后,緩存到本地的Map中。

   
    private final Map<String, String> loginCookie = new HashMap<>();

    private final String adminAddresses = "http://127.0.0.1:8081/xxl-job-admin";
    private final String username = "admin";
    private final String password = "123456";

    public String login() {
        String url = adminAddresses + "/login";
        HttpResponse response = HttpRequest.post(url)
                .form("userName", username)
                .form("password", password)
                .execute();
        List<HttpCookie> cookies = response.getCookies();
        Optional<HttpCookie> cookieOpt = cookies.stream()
                .filter(cookie -> cookie.getName().equals("XXL_JOB_LOGIN_IDENTITY")).findFirst();
        if (!cookieOpt.isPresent())
            throw new RuntimeException("get xxl-job cookie error!");

        String value = cookieOpt.get().getValue();
        loginCookie.put("XXL_JOB_LOGIN_IDENTITY", value);

        log.info("XxlJobComponent.login.token={}", value);
        return value;
    }
4.3.3 獲取cookie

其他接口在調(diào)用時,直接從緩存中獲取cookie,如果緩存中不存在則調(diào)用/login接口,為了避免這一過程失敗,允許最多重試3次。

    public String getCookie() {
        for (int i = 0; i < 3; i++) {
            String cookieStr = loginCookie.get("XXL_JOB_LOGIN_IDENTITY");
            if (cookieStr != null) {
                return "XXL_JOB_LOGIN_IDENTITY=" + cookieStr;
            }
            login();
        }
        throw new RuntimeException("get xxl-job cookie error!");
    }
4.3.4 通過appName獲取執(zhí)行管理器ID
/**
     * 通過appName獲取執(zhí)行管理器ID
     *
     * @param appName
     * @return
     */
    private int getJobGroupId(String appName) {
        List<XxlJobGroup> jobGroupList = listJobGroup(appName);
        if (CollectionUtil.isEmpty(jobGroupList)) {
            return -1;
        }
        return jobGroupList.get(0).getId();
    }

    /**
     * 獲取執(zhí)行管理器列表
     *
     * @param appName
     * @return
     */
    private List<XxlJobGroup> listJobGroup(String appName) {
        String url = adminAddresses + "/jobgroup/pageList";
        HttpResponse response = HttpRequest.post(url)
                .form("appname", appName)
                .cookie(getCookie())
                .execute();

        String body = response.body();
        JSONArray array = JSONUtil.parse(body).getByPath("data", JSONArray.class);
        List<XxlJobGroup> list = array.stream()
                .map(o -> JSONUtil.toBean((JSONObject) o, XxlJobGroup.class))
                .collect(Collectors.toList());
        return list;
    }
4.3.5 創(chuàng)建&啟動定時任務(wù)
 /**
     * 啟動定時任務(wù)
     *
     * @param jobId
     * @return
     */
    private boolean startJob(Integer jobId) {
        String url = adminAddresses + "/jobinfo/start";
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("id", jobId);

        HttpResponse response = HttpRequest.post(url)
                .form(paramMap)
                .cookie(getCookie())
                .execute();

        JSON json = JSONUtil.parse(response.body());
        Object code = json.getByPath("code");
        return code.equals(200);
    }

    /**
     * 創(chuàng)建定時任務(wù)
     *
     * @param xxlJobInfo
     * @return
     */
    private Integer addJobInfo(XxlJobInfo xxlJobInfo) {
        String url = adminAddresses + "/jobinfo/add";
        Map<String, Object> paramMap = BeanUtil.beanToMap(xxlJobInfo);
        HttpResponse response = HttpRequest.post(url)
                .form(paramMap)
                .cookie(getCookie())
                .execute();

        JSON json = JSONUtil.parse(response.body());
        Object code = json.getByPath("code");
        if (code.equals(200)) {
            Object content = json.getByPath("content");
            if (content == null) {
                return -1;
            }
            return Integer.valueOf((String) content);
        }
        log.info("創(chuàng)建定時任務(wù)失敗");
        return -1;
    }
4.3.5 XxlJobComponent完整代碼
package com.ac.order.component;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.ac.common.xxljob.XxlJobGroup;
import com.ac.common.xxljob.XxlJobInfo;
import com.ac.order.cmd.AddDefaultXxlJobCmd;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.HttpCookie;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Component
public class XxlJobComponent {

    private final Map<String, String> loginCookie = new HashMap<>();

    private final String adminAddresses = "http://127.0.0.1:8081/xxl-job-admin";
    private final String username = "admin";
    private final String password = "123456";

    /**
     * 創(chuàng)建定時定時任務(wù)并啟動
     *
     * @param cmd
     * @return
     */
    public boolean addAndStartJob(AddDefaultXxlJobCmd cmd) {
        int jobGroup = getJobGroupId(cmd.getAppName());
        if (jobGroup == -1) {
            log.error("獲取執(zhí)行管理器ID失敗,appName={}", cmd.getAppName());
            return false;
        }

        cmd.setJobGroup(jobGroup);
        XxlJobInfo jobInfo = convertDefaultJobInfo(cmd);

        //創(chuàng)建定時任務(wù)
        Integer id = this.addJobInfo(jobInfo);
        if (id == -1) {
            log.error("創(chuàng)建定時任務(wù)失敗,cmd={}", cmd);
            return false;
        }
        //啟動定時任務(wù)
        return this.startJob(id);
    }

    /**
     * xxl-job登錄
     *
     * @return
     */
    public String login() {
        String url = adminAddresses + "/login";
        HttpResponse response = HttpRequest.post(url)
                .form("userName", username)
                .form("password", password)
                .execute();
        List<HttpCookie> cookies = response.getCookies();
        Optional<HttpCookie> cookieOpt = cookies.stream()
                .filter(cookie -> cookie.getName().equals("XXL_JOB_LOGIN_IDENTITY")).findFirst();
        if (!cookieOpt.isPresent())
            throw new RuntimeException("get xxl-job cookie error!");

        String value = cookieOpt.get().getValue();
        loginCookie.put("XXL_JOB_LOGIN_IDENTITY", value);

        log.info("XxlJobComponent.login.token={}", value);
        return value;
    }

    /**
     * 其他接口在調(diào)用時,直接從緩存中獲取cookie,如果緩存中不存在則調(diào)用/login接口,為了避免這一過程失敗,允許最多重試3次
     *
     * @return
     */
    public String getCookie() {
        for (int i = 0; i < 3; i++) {
            String cookieStr = loginCookie.get("XXL_JOB_LOGIN_IDENTITY");
            if (cookieStr != null) {
                return "XXL_JOB_LOGIN_IDENTITY=" + cookieStr;
            }
            login();
        }
        throw new RuntimeException("get xxl-job cookie error!");
    }

    /**
     * 通過appName獲取執(zhí)行管理器ID
     *
     * @param appName
     * @return
     */
    private int getJobGroupId(String appName) {
        List<XxlJobGroup> jobGroupList = listJobGroup(appName);
        if (CollectionUtil.isEmpty(jobGroupList)) {
            return -1;
        }
        return jobGroupList.get(0).getId();
    }

    /**
     * 獲取執(zhí)行管理器列表
     *
     * @param appName
     * @return
     */
    private List<XxlJobGroup> listJobGroup(String appName) {
        String url = adminAddresses + "/jobgroup/pageList";
        HttpResponse response = HttpRequest.post(url)
                .form("appname", appName)
                .cookie(getCookie())
                .execute();

        String body = response.body();
        JSONArray array = JSONUtil.parse(body).getByPath("data", JSONArray.class);
        List<XxlJobGroup> list = array.stream()
                .map(o -> JSONUtil.toBean((JSONObject) o, XxlJobGroup.class))
                .collect(Collectors.toList());
        return list;
    }

    /**
     * 啟動定時任務(wù)
     *
     * @param jobId
     * @return
     */
    private boolean startJob(Integer jobId) {
        String url = adminAddresses + "/jobinfo/start";
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("id", jobId);

        HttpResponse response = HttpRequest.post(url)
                .form(paramMap)
                .cookie(getCookie())
                .execute();

        JSON json = JSONUtil.parse(response.body());
        Object code = json.getByPath("code");
        return code.equals(200);
    }

    /**
     * 創(chuàng)建定時任務(wù)
     *
     * @param xxlJobInfo
     * @return
     */
    private Integer addJobInfo(XxlJobInfo xxlJobInfo) {
        String url = adminAddresses + "/jobinfo/add";
        Map<String, Object> paramMap = BeanUtil.beanToMap(xxlJobInfo);
        HttpResponse response = HttpRequest.post(url)
                .form(paramMap)
                .cookie(getCookie())
                .execute();

        JSON json = JSONUtil.parse(response.body());
        Object code = json.getByPath("code");
        if (code.equals(200)) {
            Object content = json.getByPath("content");
            if (content == null) {
                return -1;
            }
            return Integer.valueOf((String) content);
        }
        log.info("創(chuàng)建定時任務(wù)失敗");
        return -1;
    }

    /**
     * 定時任務(wù)對象轉(zhuǎn)換
     *
     * @param cmd
     * @return
     */
    private XxlJobInfo convertDefaultJobInfo(AddDefaultXxlJobCmd cmd) {
        XxlJobInfo jobInfo = new XxlJobInfo();
        /*基礎(chǔ)配置*/
        jobInfo.setJobGroup(cmd.getJobGroup());
        jobInfo.setJobDesc(cmd.getJobDesc());
        jobInfo.setAuthor("SYSTEM");
        jobInfo.setAlarmEmail("test.126.com");
        //調(diào)度配置
        jobInfo.setScheduleType("CRON");
        jobInfo.setScheduleConf(cmd.getScheduleConf());
        //任務(wù)配置
        jobInfo.setGlueType("BEAN");
        jobInfo.setExecutorHandler(cmd.getExecutorHandler());
        jobInfo.setExecutorParam(cmd.getExecutorParam());

        /*高級配置*/
        //路由策略
        jobInfo.setExecutorRouteStrategy("CONSISTENT_HASH");
        //調(diào)度過期策略 DO_NOTHING忽略 FIRE_ONCE_NOW立即執(zhí)行一次
        jobInfo.setMisfireStrategy("FIRE_ONCE_NOW");
        //阻塞處理策略
        jobInfo.setExecutorBlockStrategy("SERIAL_EXECUTION");

        return jobInfo;
    }
}
4.4 訂單未付款自動關(guān)閉15分鐘倒計(jì)時
4.4.1 訂單任務(wù)類
package com.ac.order.task;

import cn.hutool.core.util.StrUtil;
import com.ac.common.constant.XXLJobHandlerConstant;
import com.ac.core.util.DateUtil;
import com.ac.order.cmd.AddDefaultXxlJobCmd;
import com.ac.order.component.XxlJobComponent;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

/**
 * @author Alan Chen
 * @description 訂單未支付倒計(jì)時關(guān)閉
 * @date 2023/05/17
 */
@Slf4j
@Component
public class AutoCancelOrderJob {

    @Resource
    private XxlJobComponent xxlJobComponent;

    /**
     * 訂單未付款自動關(guān)閉15分鐘倒計(jì)時
     *
     * @param orderNo
     */
    public void addJob(String orderNo) {
        try {
            log.info("AutoCancelOrderJob.addJob,orderNo={}", orderNo);

            String executorParam = orderNo;

            LocalDateTime now = LocalDateTime.now();
            //6小時后執(zhí)行
            LocalDateTime offset = DateUtil.offset(now, 15, ChronoUnit.MINUTES);
            String scheduleConf = DateUtil.getCron(cn.hutool.core.date.DateUtil.date(offset));

            AddDefaultXxlJobCmd cmd = AddDefaultXxlJobCmd.builder()
                    .appName("executor-order")
                    .jobDesc("訂單未付款自動關(guān)閉15分鐘倒計(jì)時")
                    .scheduleConf(scheduleConf)
                    .executorHandler(XXLJobHandlerConstant.AUTO_CANCEL_ORDER)
                    .executorParam(executorParam)
                    .build();
            xxlJobComponent.addAndStartJob(cmd);
        } catch (Exception e) {
            log.info("AutoCancelOrderJob.addJob,啟動任務(wù)失敗,msg={}", e.getMessage());
        }
    }

    @XxlJob(value = XXLJobHandlerConstant.AUTO_CANCEL_ORDER)
    public void doJob() {
        try {
            // 獲取任務(wù)ID
            long jobId = XxlJobHelper.getJobId();
            log.info("AutoCancelOrderJob.doJob,jobId={},param={}", jobId, XxlJobHelper.getJobParam());

            // 獲取任務(wù)參數(shù)
            String orderNo = XxlJobHelper.getJobParam();
            // 業(yè)務(wù)邏輯

            log.info("模擬業(yè)務(wù)邏輯,AutoCancelOrderJob.doJob,關(guān)閉訂單,orderNo={}", orderNo);

            String logInfo = StrUtil.format("AutoCancelOrderJob.doJob,成功關(guān)閉訂單,orderNo={}", orderNo);
            log.info(logInfo);
            XxlJobHelper.handleSuccess(logInfo);
        } catch (Exception e) {
            String error = StrUtil.format("AutoCancelOrderJob.doJob,失敗, msg={}", e.getMessage());
            log.info(error);
            XxlJobHelper.handleFail(error);
        }
    }
}
4.4.2 Controller
   @ApiOperation(value = "訂單未付款自動關(guān)閉15分鐘倒計(jì)時")
    @GetMapping("autoCancelOrder")
    public boolean autoCancelOrder(@RequestParam String orderNo) {
        autoCancelOrderJob.addJob(orderNo);
        return true;
    }
4.4.3 測試
Postman
xxljob-admin
執(zhí)行一次
參數(shù)自動填充
控制臺日志
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容