中州养老项目总结Day1-Day3

目录

一、中州养老项目介绍

1.整体业务流程

2.系统架构

3.技术架构

4.后端代码结构

二、护理项目功能开发

1.模块介绍

2.数据库表

2.1护理项目表

2.2护理计划表

2.3护理等级表

三、预约管理

1.微信小程序登录

1.1微信官方提供的登陆时序图

1.2当前业务的实现流程

1.3用户表

1.4实现过程

2.预约管理开发

2.1预约分为了两种,分别是探访预约和参观预约

2.2业务逻辑要求

2.3数据库表设计

四、定时任务框架xxl-job

1.需求说明

2.定时任务概述

3.Spring Task和xxl-job对比

3.1定位与应用场景差异

3.2XXL-JOB 的核心优势

3.3架构设计的独立性与扩展性

3.4具体例子说明

4.xxl-job入门

4.1cron 表达式基础说明

4.2源码下载

4.3初始化“调度数据库”

4.4编译源码

5. 配置部署“调度中心”

6.入门案例编写

6.1登录调度中心,点击下图所示“新建任务”按钮,新建示例任务

7.xxl-job 任务详解

7.1执行器

7.2基础配置

7.3xxl-job 案例

8.预约管理定时修改状态

8.1项目中集成xxl-job


一、中州养老项目介绍

中州养老院是一家致力于为老年人提供高质量养老服务的专业机构。拥有着多年的行业经验和深厚的服务实力,一直秉持着“以人为本、关爱生命”的核心理念,始终致力于为老年人提供最优质的养老服务,为晚年生活注入更多的快乐与温暖。

中州养老院坐落在一片幽静的绿树成荫的区域,占地面积超过30,000平方米,总建筑面积为40,000平方米。经过多年的发展,中州养老院成长为一家床位数量众多、服务项目丰富的养老机构,现有床位超过800张,员工人数达到200余人。该养老院为老年人提供舒适的住宿环境,房间宽敞明亮,家具精美,充满温馨和舒适感。床铺舒适柔软,提供优质的床垫和床上用品,确保老年人的良好睡眠。同时中州养老院注重细节,为老年人提供贴心、周到的服务。中州养老院员工经过专业培训,拥有高素质的服务意识和服务技能。他们时刻以老年人的需求和舒适为重,热情友好,愿意倾听老年人的心声,为老年人提供贴心、专业的服务和关怀。

中州养老院不断地推进创新,为老人提供更加优质的服务。还还获得了多项荣誉,包括“全国优秀养老院”、“山区老年人关爱先进集体”等。这些荣誉的获得证明了中州在养老服务领域中的领先地位,同时也是中州养老院不断努力的动力。

1.整体业务流程

中州养老系统为养老院量身定制开发专业的养老管理软件产品;涵盖来访管理、入退管理、在住管理、服务管理、财务管理等功能模块,涉及从来访参观到退住办理的完整流程。其中入住为项目的核心业务

2.系统架构

项目原型地址:https://rp-java.itheima.net/zhyl/

中州养老项目分为两端,一个是管理后台,另外一个是家属端

管理后台:养老院员工使用,入住、退住,给老人服务记录等等
家属端:养老院的老人家属使用,查看老人信息,缴费,下订单等等

3.技术架构

下图展现了中州养老项目主要使用的技术:

图中圈中的IOT目前阿里云已经停止免费使用了,无法进行实践,可采用华为云

4.后端代码结构

工程结构

我们先来熟悉下common、framework、service、web模块,这些都与我们的业务开发息息相关,后期我们会再详细讲解pay、security模块

* zzyl-common模块
  
      ├── com.zzyl                              
      │   ├── base
      │   │   ├── ResponseResult    //通用的接口返回结果类
      │   │   ├── BaseDto       //基础的DTO,所有自定义的DTO都继承它
      │   │   ├── BaseEntity    //基础的实体类,所有自定义的实体类都继承它
      │   │   ├── BaseVo        //基础的VO,所有自定义的VO都继承它
      │   │   ├── IBasicEnum    //公共的枚举类
      │   │   ├── PageResponse  //分页列表使用该对象封装       
      │   ├── constants         //这个包下存储所有的自定义常量类
      │   ├── enums             
      │   │   ├── BasicEnum     //基础枚举
      │   ├── exception         //公用异常包
      │   │   ├── BaseException  //基础异常类
      │   │   ├── GlobalExceptionHandler  //全局异常处理器
      │   ├── utils   //工具类包
      │   ├── vo      //公共的vo包
  
  > DTO(Data Transfer Object)据传输对象,主要用于外部接口参数传递封装,接口与接口进行传递使用VO(Value Object)视图对象,主要用于给前端返回页面参数使用
  
* zzyl-framework模块
  
      ├── com.zzyl                              
      │   ├── config
      │   │   ├── OssAliyunAutoConfig    //阿里OSS配置类
      │   │   ├── OSSAliyunFileStorageService    //OSS上传、删除接口封装
      │   │   ├── MybatisConfig       //mybatis自定义拦截器
      │   │   ├── SwaggerConfig    //swagger配置类,在线接口
      │   │   ├── WebMvcConfig        //mvc配置类,拦截器、映射器等
      │   ├── intercept             
      │   │   ├── AutoFillInterceptor     //自动填充字段拦截器
      │   ├── properties         
      │   │   ├── AliOssConfigProperties  //阿里OSS配置读取
      │   │   ├── JwtTokenManagerProperties  //JWT配置读取
      │   │   ├── SwaggerConfigProperties  //Swagger配置读取
  
* zzyl-service模块
  
      java
      ├── com.lunckon                              
      │   ├── dto   
      │   ├── entity             
      │   ├── enums  
      │   ├── mapper
      │   ├── service             
      │   ├── vo  
      resouces
      ├── mapper
      │   ├── xxxxx.xml
  
* zzyl-web模块
  
      java
      ├── com.lunckon                              
      │   ├── controller   
      │   │   ├── XxxController
      │   ├── zzylApplication  
      resouces
      ├── application.yml
      ├── logback.xml

二、护理项目功能开发

1.模块介绍

1.1护理等级作为护理模块的基础数据,主要是用于识别老人的身体健康状况,护理等级与护理计划进行绑定,管理员可对护理计划进行增删改、修改状态等操作;


1.2.护理计划作为护理模块的基础数据,护理计划主要是与护理等级和护理项目进行绑定,后台管理人员在创建计划时,可选择该计划要执行哪些任护理项目;后台管理人员可以护理计划进行增删改,修改状态等操作;


1.3.护理项目作为护理模块的基础数据,主要是由护理员给老年人提供护理服务,常见的有:洗头、助餐、复健运动等,护理项目分为护理计划内与护理计划外,两者之间的区别是:前者是根据护理计划产生的,后者是由家属从用户端进行下单支付产生的;

ER图

2.数据库表

2.1护理项目表

CREATE TABLE "nursing_project" (
  "id" bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  "name" varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '名称',
  "order_no" int DEFAULT NULL COMMENT '排序号',
  "unit" varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '单位',
  "price" decimal(10,2) DEFAULT NULL COMMENT '价格',
  "image" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图片',
  "nursing_requirement" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '护理要求',
  "status" int NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
  "create_by" varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人',
  "update_by" varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '更新人',
  "remark" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  "update_time" datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY ("id") USING BTREE,
  UNIQUE KEY "name" ("name") USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='护理项目表';

2.2护理计划表

CREATE TABLE "nursing_plan" (
  "id" int NOT NULL AUTO_INCREMENT COMMENT '编号',
  "sort_no" int DEFAULT NULL COMMENT '排序号',
  "plan_name" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '名称',
  "status" tinyint NOT NULL DEFAULT '0' COMMENT '状态 0禁用 1启用',
  "create_time" datetime NOT NULL COMMENT '创建时间',
  "update_time" datetime DEFAULT NULL COMMENT '更新时间',
  "create_by" bigint DEFAULT NULL COMMENT '创建人id',
  "update_by" bigint DEFAULT NULL COMMENT '更新人id',
  "remark" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
  PRIMARY KEY ("id") USING BTREE,
  UNIQUE KEY "plan_name" ("plan_name") USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=96 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='护理计划表';

2.3护理等级表

CREATE TABLE "nursing_level" (
  "id" int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  "name" varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '等级名称',
  "lplan_id" int NOT NULL COMMENT '护理计划ID',
  "fee" decimal(10,2) NOT NULL COMMENT '护理费用',
  "status" tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
  "description" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '等级说明',
  "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  "create_by" bigint DEFAULT NULL COMMENT '创建人id',
  "update_by" bigint DEFAULT NULL COMMENT '更新人id',
  "remark" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
  "update_time" datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY ("id") USING BTREE,
  UNIQUE KEY "name" ("name") USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='护理等级表';

由于主要是这三张表之间的CRUD,便不再赘述

三、预约管理

从这个图上,我们可以看出来,整个业务流程的入口就是参观预约,客户可以通过小程序线上预约到访时间

1.微信小程序登录

1.1微信官方提供的登陆时序图

注意点:

①前端在小程序中集成微信相关依赖,当用户请求登录的同时,调用[wx.login()]

②获取 临时登录凭证code ,并回传到开发者服务器。
  
③开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
  
 ④自定义登录态:后台服务器验证用户成功后,使用JWT生成一个token返回给前端,前端负责把token存储到小程序端的storage中,以后,从小程序中发起其他请求的时候,携带该token到后台验证

1.2当前业务的实现流程

1.3用户表

CREATE TABLE "member" (
  "id" bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  "phone" varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
  "name" varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称',
  "avatar" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '头像',
  "open_id" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OpenID',
  "gender" int DEFAULT NULL COMMENT '性别(0:男,1:女)',
  "create_time" timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  "update_time" timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  "create_by" bigint DEFAULT NULL COMMENT '创建人',
  "update_by" bigint DEFAULT NULL COMMENT '更新人',
  "remark" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
  PRIMARY KEY ("id") USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

根据以上表结构,对应的实体类,如下:

package com.zzyl.entity;

import com.zzyl.base.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;


@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member extends BaseEntity {

    /**
     * 手机号
     */
    private String phone;

    /**
     * 名称
     */
    private String name;

    /**
     * 头像
     */
    private String avatar;

    /**
     * OpenID
     */
    private String openId;

    /**
     * 性别
     */
    private int gender;

}

1.4实现过程

①前端发起请求

②后端业务逻辑代码

package com.zzyl.service.impl;

import cn.hutool.json.JSONObject;
import com.google.common.collect.Lists;
import com.zzyl.constant.Constants;
import com.zzyl.dto.UserLoginRequestDto;
import com.zzyl.entity.Member;
import com.zzyl.exception.BaseException;
import com.zzyl.mapper.MemberMapper;
import com.zzyl.properties.JwtTokenManagerProperties;
import com.zzyl.service.MemberService;
import com.zzyl.service.WechatService;
import com.zzyl.utils.JwtUtil;
import com.zzyl.utils.ObjectUtil;
import com.zzyl.utils.StringUtils;
import com.zzyl.vo.LoginVo;
import org.aspectj.weaver.ast.Var;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author sjqn
 * @date 2023/10/27
 */
@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private WechatService wechatService;

    @Autowired
    private MemberMapper memberMapper;

    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;


    static ArrayList DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
            "生活更美好",
            "大桔大利",
            "日富一日",
            "好柿开花",
            "柿柿如意",
            "一椰暴富",
            "大柚所为",
            "杨梅吐气",
            "天生荔枝"
    );

    /**
     * 小程序端登录
     *
     * @param userLoginRequestDto
     * @return
     */
    @Override
    public LoginVo login(UserLoginRequestDto userLoginRequestDto) throws IOException {

        //1.调用微信api,根据code获取openId
        String openId = wechatService.getOpenid(userLoginRequestDto.getCode());

        //2.根据openId查询用户
        Member member = memberMapper.getByOpenId(openId);

        //3.如果用户为空,则新增
        if (ObjectUtil.isEmpty(member)) {
            member = Member.builder().openId(openId).build();
        }

        //4.调用微信api获取用户绑定的手机号
        String phone = wechatService.getPhone(userLoginRequestDto.getPhoneCode());

        //5.保存或修改用户
        saveOrUpdate(member, phone);

        //6.将用户id存入token,返回
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.JWT_USERID, member.getId());
        claims.put(Constants.JWT_USERNAME, member.getName());

        String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), claims);

        LoginVo loginVo = new LoginVo();
        loginVo.setToken(token);
        loginVo.setNickName(member.getName());
        return loginVo;

    }

    /**
     * 保存或修改客户
     *
     * @param member
     * @param phone
     */
    private void saveOrUpdate(Member member, String phone) {

        //1.判断取到的手机号与数据库中保存的手机号不一样
        if(ObjectUtil.notEqual(phone, member.getPhone())){
            //设置手机号
            member.setPhone(phone);
        }
        //2.判断id存在
        if (ObjectUtil.isNotEmpty(member.getId())) {
            memberMapper.update(member);
            return;
        }
        //3.保存新的用户
        //随机组装昵称,词组+手机号后四位
        String nickName = DEFAULT_NICKNAME_PREFIX.get((int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size()))
                + StringUtils.substring(member.getPhone(), 7);

        member.setName(nickName);
        memberMapper.save(member);
    }

}

③微信接口方法

package com.zzyl.service.impl;

import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zzyl.exception.BaseException;
import com.zzyl.service.WechatService;
import com.zzyl.utils.ObjectUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author sjqn
 * @date 2023/10/27
 */
@Service
public class WechatServiceImpl implements WechatService {


    // 登录
    private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code";

    // 获取token
    private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential";

    // 获取手机号
    private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";

    @Value("${zzyl.wechat.appId}")
    private String appId;

    @Value("${zzyl.wechat.appSecret}")
    private String secret;


    /**
     * 获取openid
     *
     * @param code 登录凭证
     * @return
     * @throws IOException
     */
    @Override
    public String getOpenid(String code) throws IOException {

        //封装参数
        Map<String,Object> requestUrlParam = getAppConfig();
        requestUrlParam.put("js_code",code);

        String result = HttpUtil.get(REQUEST_URL, requestUrlParam);

        JSONObject jsonObject = JSONUtil.parseObj(result);
        // 若code不正确,则获取不到openid,响应失败
        if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
            throw new BaseException(jsonObject.getStr("errmsg"));
        }

        return jsonObject.getStr("openid");
    }

    /**
     * 封装公共参数
     * @return
     */
    private Map<String, Object> getAppConfig() {

        Map<String, Object> requestUrlParam = new HashMap<>();
        requestUrlParam.put("appid",appId);
        requestUrlParam.put("secret",secret);
        return requestUrlParam;
    }

    /**
     * 获取手机号
     *
     * @param code 手机号凭证
     * @return
     * @throws IOException
     */
    @Override
    public String getPhone(String code) throws IOException {

        //获取access_token
        String token = getToken();
        //拼接请求路径
        String url = PHONE_REQUEST_URL + token;

        Map<String,Object> param = new HashMap<>();
        param.put("code",code);

        String result = HttpUtil.post(url, JSONUtil.toJsonStr(param));
        JSONObject jsonObject = JSONUtil.parseObj(result);
        if (jsonObject.getInt("errcode") != 0) {
            //若code不正确,则获取不到phone,响应失败
            throw new BaseException(jsonObject.getStr("errmsg"));

        }
        return jsonObject.getJSONObject("phone_info").getStr("purePhoneNumber");

    }

    public String getToken(){
        Map<String, Object> requestUrlParam = getAppConfig();

        String result = HttpUtil.get(TOKEN_URL, requestUrlParam);
        //解析
        JSONObject jsonObject = JSONUtil.parseObj(result);
        //如果code不正确,则失败
        if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){
            throw new BaseException(jsonObject.getStr("errmsg"));
        }
        return jsonObject.getStr("access_token");

    }
}

2.预约管理开发

2.1预约分为了两种,分别是探访预约和参观预约

* 探访预约是指家里有老人已经入住了养老院,家人可以点击探访预约,看望绑定的老人
* 参观预约是指想要参观一下养老院,了解养老院的环境、设施及服务情况

参观预约

2.2业务逻辑要求

首页

预约设置页

2.3数据库表设计

通过需求分析,我们可以确定的字段有:

预约人姓名、手机号、家人姓名、预约时间

根据我们的经验来看,一个表中必要的字段有6个,所以现在的字段有

主键、 预约人姓名、手机号、家人姓名、预约时间、创建时间、修改时间、创建人、修改人、备注

由于当前预约表是可以表示**探访预约**和**参观预约**的,我们可以再加一个字段`type`来区分

预约也有状态,会根据预约探访的情况设置状态,具体如下:

* 待报道 预约之后,到院之前
* 已完成 按时到院
* 取消 在小程序端取消预约
* 过期 过期未到

所以,预约表中最终的表字段为:

主键、 预约人姓名、手机号、家人姓名、类型、状态、预约时间、创建时间、修改时间、创建人、修改人、备注

由于一个手机号在同一个时间段内只能预约一次,我们也可以在表结构中设置唯一约束(手机号+时间),其中的手机号和手机号做了唯一约束,代表一个手机号同一个时间段端只能预约一次

建表语句:

CREATE TABLE `reservation` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '预约人姓名',
  `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '预约人手机号',
  `time` datetime NOT NULL COMMENT '预约时间',
  `visitor` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '探访人',
  `type` int NOT NULL COMMENT '预约类型,0:参观预约,1:探访预约',
  `status` int NOT NULL COMMENT '预约状态,0:待报道,1:已完成,2:取消,3:过期',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_by` bigint DEFAULT NULL COMMENT '创建人id',
  `update_by` bigint DEFAULT NULL COMMENT '更新人id',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `name_mobile_time_visitor` (`mobile`,`time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=93 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='预约信息表';

这个接口所有的接口也是常规上的业务CRUD,不再赘述

四、定时任务框架xxl-job

1.需求说明

当用户预约之后,假如没有在预约的时间来访,那这次预约就会失效。那我们如何设置失效的状态呢?

肯定不能人工操作,更加简便的方式就是自动完成。那如何自动完成,就要使用到定时任务

2.定时任务概述

任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人力,而是由系统自动去执行任务。

常用业务场景案例:

某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
12306会根据车次的不同,设置某几个时间点进行分批放票。

如何实现任务调度?

多线程方式,结合sleep
JDK提供的API,例如:Timer、ScheduledExecutor
框架,例如Quartz ,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求
spring task
分布式任务调度框架(例如:xxl-job

这里结合我是使用过的两个定时任务调度框架Spring Task和xxl-job来对比一下为啥使用xxl-job更为广泛

3.Spring Task和xxl-job对比

在分布式系统和微服务架构日益普及的背景下,XXL-JOB 相比 Spring Task 应用更为广泛,主要因其在功能定位、架构设计、运维能力等方面更贴合现代企业级开发需求。以下从多个维度对比分析两者的差异及 XXL-JOB 的优势:

3.1定位与应用场景差异

维度Spring TaskXXL-JOB
本质Spring 框架内置的单机任务调度工具独立开源的分布式任务调度平台
核心场景单体应用、简单定时任务(如日志清理、定时通知)分布式系统、微服务架构、复杂任务调度(如分布式事务补偿、大数据批处理)
设计目标轻量级集成,快速实现简单调度逻辑解决分布式场景下的任务调度难题,强调高可用、可扩展、易运维

3.2XXL-JOB 的核心优势

①. 分布式调度能力

  • 多节点负载均衡
    XXL-JOB 支持将任务分配到多个执行器节点并行处理,避免单机瓶颈,提升吞吐量。例如,在电商订单批量处理场景中,可通过多节点同时处理不同批次订单,缩短整体耗时。
  • 故障转移与容灾
    当某个执行器节点故障时,任务会自动路由到其他健康节点执行,确保调度稳定性。而 Spring Task 仅支持单机执行,故障时任务会直接中断。
  • 跨应用 / 跨服务调度
    在微服务架构中,不同服务的任务可通过 XXL-JOB 统一管理,无需在每个服务中独立实现调度逻辑(如用户服务、库存服务的定时任务统一由 XXL-JOB 触发)。

②. 可视化管理与运维能力

  • 图形化操作界面
    XXL-JOB 提供直观的控制台,支持任务增删改查、状态监控、日志查看等功能(如下图示例),大幅降低运维成本。而 Spring Task 需通过代码或配置文件管理任务,调试和监控依赖日志或自定义工具.

  • 丰富的任务控制功能
    • 动态调整调度策略:实时修改任务的执行时间(如将每天 1 次调度改为每小时 1 次),无需重启应用。
    • 手动触发与重试:支持手动触发任务执行,或对失败任务进行重试(可配置重试次数、间隔),适合需要人工介入的异常处理场景。
    • 报警机制:任务失败时可通过邮件、短信等方式通知运维人员,而 Spring Task 需自行实现报警逻辑。

3.3架构设计的独立性与扩展性

  • 解耦调度逻辑与业务代码
    XXL-JOB 采用 “调度中心 + 执行器” 的架构,调度逻辑集中在调度中心,业务代码通过执行器封装,两者通过接口通信。这种设计避免了调度逻辑侵入业务代码,便于独立升级和维护。
    • 对比:Spring Task 的调度逻辑与业务代码紧耦合(通过@Scheduled注解直接标注在业务方法上),代码复用性差,分布式场景下难以管理。
  • 插件化扩展机制
    XXL-JOB 支持通过插件扩展功能(如自定义路由策略、日志存储方式),例如:
    • 使用 Redis 实现调度中心的高可用(替换默认的数据库锁机制);
    • 集成 Kafka 实现任务事件的异步通知。

3.4具体例子说明

大家来看下面这个图:

上面是一个发送优惠券的定时任务

如果只是单体项目的话,定时任务执行是不会有任何问题的
如果后期业务量较大,单体项目做了集群部署,那集群中每一台服务的代码都是一样的,都会按照规定的时间来执行任务,这样就会造成优惠券重复发放。

所以,由此以上分析,我们要解决就是,即使是单体项目,如果做集群,同样要考虑任务重复执行的问题,那xxl-job就可以解决这些问题,当然不仅仅如此

官网地址:https://www.xuxueli.com/xxl-job/

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

4.xxl-job入门

4.1cron 表达式基础说明

cron 表达式是用于设置定时规则的字符串,由 7 部分 组成(年可选 ),各部分用空格隔开,含义及取值范围如下:

组成部分含义取值范围
Seconds(秒)秒数0-59
Minutes(分)分钟数0-59
Hours(时)小时数0 - 23
Day-of-Month(天)月份中的第几天1 - 31
Month(月)月份0 - 11 或 JAN - DEC(英文缩写)
Day-of-Week(星期)星期几1 - 7(1 表示星期日 )或 SUN - SAT(英文缩写 )
Year(年) (可选)年份1970 - 2099(可不填 )

cron 表达式特殊符号含义

可通过特殊符号实现更灵活的定时规则,符号及含义:

符号含义
?表示不确定值,用于避免子表达式冲突(如每月 20 日触发,不管星期几,写 0 0 0 20 * ? )
*代表所有可能值,比如秒位用 * 就是 “每秒”
,设置多个值,如 26,29,33 表示 “26 分、29 分、33 分各运行一次任务”
-设置取值范围,如 5-20 表示 “从 5 分到 20 分,每分钟运行一次任务”
/设置频率 / 间隔,如 1/15(分位 )表示 “从 1 分开始,每隔 15 分钟运行一次任务”
L用于每月 / 每周,表 “每月最后一天” 或 “每月最后星期几”,如 6L 是 “每月最后一个星期五”
W表离给定日期最近的工作日,如 15W(天位 )是 “离本月 15 日最近的工作日”
#表 “该月第几个周 X”,如 6#3 是 “该月第 3 个周五”(6 代表周五,#3 表第 3 个 )

cron 表达式示例

常见场景的 cron 表达式及含义:

cron 表达式含义
*/5 * * * * ?每隔 5 秒运行一次任务
0 0 23 * * ?每天 23 点(晚上 11 点 )运行一次任务
0 0 1 1 * ?每月 1 号凌晨 1 点运行一次任务
0 0 23 L * ?每月最后一天 23 点运行一次任务
0 26,29,33 * * * ?在 26 分、29 分、33 分各运行一次任务
0 0/30 9-17 * * ?朝九晚五(9 - 17 点 )期间,每半小时运行一次
0 15 10 ? * 6#3每月第 3 个周五上午 10:15 运行一次任务

实际使用时,结合调度框架(如 XXL-JOB、Quartz 等 )的语法支持,就能精准控制任务触发时机啦,有需要可以直接复制对应表达式到框架配置里测试效果~

4.2源码下载

想要快速使用xxl-job,我们可以首先到gitee或者是github上拉取它的开源代码

平台类型仓库地址说明
GitHubGitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB)国际知名代码托管平台,可获取最新代码、参与开源协作,查看海外开发者提交的 Issue 与 PR
Giteexxl-job: 一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。国内代码托管平台,访问速度更稳定,适合国内开发者下载、查看文档与进行社区交流

4.3初始化“调度数据库”

请下载项目源码并解压,获取 “调度数据库初始化SQL脚本” 并执行即可。

位置:`/xxl-job/doc/db/tables_xxl_job.sql` 共8张表

- xxl_job_lock:任务调度锁表;
- xxl_job_group:执行器信息表,维护任务执行器信息;
- xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
- xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
- xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
- xxl_job_user:系统用户表;

调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例;

如果mysql做主从,调度中心集群节点务必强制走主库;

4.4编译源码

解压源码,按照maven格式将源码导入IDE, 使用maven进行编译即可,源码结构如下:

5. 配置部署“调度中心”

调度中心项目:xxl-job-admin

作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。

步骤一:调度中心配置

调度中心配置文件地址:`/xxl-job/xxl-job-admin/src/main/resources/application.properties`

数据库的连接信息修改为自己的数据库

启动调度中心,访问http://localhost:8080/xxl-job-admin 默认账户admin,密码123456, 登录后运行界面如下图所示。

6.入门案例编写

6.1登录调度中心,点击下图所示“新建任务”按钮,新建示例任务

6.2创建xxljob-demo项目,导入依赖

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

    <!--xxl-job-->
    <dependency>
        <groupId>com.xuxueli</groupId>
        <artifactId>xxl-job-core</artifactId>
        <version>2.3.0</version>
    </dependency>
</dependencies>

6.3application.yml配置

server:
  port: ${port:8881}



xxl:
  job:
    executor:
      port: ${executor.port:9999}
      appname: zz-126-executor
    admin:
      addresses: http://192.168.200.146:8888/xxl-job-admin

6.4新建配置类(参考xxl-job源码-样例)

package com.heima.xxljob.config;

import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * xxl-job config
 *
 * @author xuxueli 2017-04-28
 */
@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Value("${xxl.job.executor.port}")
    private int port;


    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setPort(port);
        return xxlJobSpringExecutor;
    }


}

7.xxl-job 任务详解

7.1执行器

执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能;

另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器

以下是执行器的属性说明:

属性名称说明
AppName每个执行器集群的唯一标识,执行器会周期性以 AppName 为对象进行自动注册,可通过该配置自动发现注册成功的执行器,供任务调度时使用
名称执行器的名称,因 AppName 限制由字母数字等组成、可读性不强,此名称用于提高执行器的可读性
排序执行器的排序,系统中需要执行器的地方(如任务新增),将按照该排序读取可用的执行器列表
注册方式调度中心获取执行器地址的方式
机器地址注册方式为 “手动录入” 时有效,支持人工维护执行器的地址信息

7.2基础配置

在我们新建任务的时候,里面有很多的配置项,下面我们就来介绍下里面具体的作用

基础配置

* 执行器:每个任务必须绑定一个执行器, 方便给任务进行分组
  
* 任务描述:任务的描述信息,便于任务管理;
  
* 负责人:任务的负责人;
  
* 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔

调度配置

调度类型:
  * 无:该类型不会主动触发调度;
  * CRON:该类型将会通过CRON,触发任务调度;
  * 固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发;

任务配置

运行模式:

BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;

 JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;
  执行参数:任务执行所需的参数;

阻塞处理策略

阻塞处理策略:调度过于密集执行器来不及处理时的处理策略;

* 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO(First Input First Output)队列并以串行方式运行;


* 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
  
* 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;

路由策略

当执行器集群部署时,提供丰富的路由策略,包括;

* FIRST(第一个):固定选择第一个机器;
  
* LAST(最后一个):固定选择最后一个机器;
  
* **ROUND(轮询)**
  
* RANDOM(随机):随机选择在线的机器;
  
* CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
  
* LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
  
* LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
  
* FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
  
* BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
  
* **SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

7.3xxl-job 案例

默认测试一遍

①默认路由策略为第一个

②点击执行一次

③查看日志,成功了

④控制台输出了

轮询测试一遍

①.将路由策略变为轮询

②.启动多个微服务

③点击两次执行

控制台两个服务各有一次执行

分片广播

执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务

执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务

①例如我们现在本地有两份执行器的服务,相当于执行器集群

②将刚才的任务改成分片广播,执行的任务注解也修改一下,和要执行的任务上的注解相匹配

分片广播代码

分片参数

​ index:当前分片序号(从0开始),执行器集群列表中当前执行器的序号;

​ total:总分片数,执行器集群的总机器数量;

③执行一次任务,观察两个执行器项目的控制台

可见分片广播成功了,每个执行器都执行了任务

8.预约管理定时修改状态

依据我们之前分析的需求,当用户没有按时来访之后,会把预约的状态修改为**已过期**

我们可以每隔半个小时查询一次数据,如果状态是未到访并且已经过了预约时间,则设置为已过期

8.1项目中集成xxl-job

1)调度中心搭建

关于调度中心的环境,黑马提供的虚拟机中已安装了该服务,大家可以直接使用。

地址:http://192.168.200.146:8888/xxl-job-admin

用户名密码:admin/123456

2)创建执行器和任务

登录调度中心后,手动创建执行器和任务

执行器,一般一个项目只需要创建一个执行器即可,在执行器下可以创建多个任务

创建任务

3)项目中集成

拷贝测试案例中的config到我们的项目中,

模块名:zzyl-framework 核心技术模块

包名:com.zzyl.config

package com.zzyl.config;

import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 *
 * @author xuxueli 2017-04-28
 */
@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Value("${xxl.job.executor.port}")
    private int port;


    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setPort(port);
        return xxlJobSpringExecutor;
    }


}

在application.yml文件中的配置也要拷贝过来,修改为自己定义的执行器

xxl:
  job:
    admin:
      addresses: http://192.168.200.146:8888/xxl-job-admin
    executor:
      appname: zzyl-dev-executor
      port: 9999

在ReservationMapper文件中新增一个方法(根据时间修改状态),用于修改预约的状态

    @Update("update reservation set status = 3 where status = 0 and time < #{now}")
    void updateReservationStatus(LocalDateTime now);

在ReservationService业务层代码中新增一个方法(根据时间修改状态),用于修改预约的状态

 /**
         * 过期状态修改
         * @param now
         */
    void updateReservationStatus(LocalDateTime now);

实现类:

 /**
         * 过期状态修改
         * @param now
         */
    @Override
    public void updateReservationStatus(LocalDateTime now) {
        reservationMapper.updateReservationStatus(now);
    }

在zzyl-service模块下创建任务类:

package com.zzyl.job;
    
    import com.zzyl.service.ReservationService;
    import com.zzyl.vo.ReservationVo;
    import com.xxl.job.core.handler.annotation.XxlJob;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalDateTime;
    import java.util.List;
    
    /**
     * @author sjqn
     * @date 2023/8/20
     */
    @Component
    @Slf4j
    public class ReservationJob {
    
        @Autowired
        private ReservationService reservationService;
    
        @XxlJob("reservationStatusToExpired")
        public void updateReservationStatus() {
            log.info("预约状态-过期修改-begin");
            reservationService.updateReservationStatus(LocalDateTime.now());
            log.info("预约状态-过期修改-end");
        }
    }

启动项目测试,在测试的时候,大家可以先把cron表达式修改为1分钟执行一次,手动在数据库改一些数据

看到这如果有用的话记得点赞关注哦,后续会更新更多内容的!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

让我上个超影吧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值