目录
6.1登录调度中心,点击下图所示“新建任务”按钮,新建示例任务
一、中州养老项目介绍
中州养老院是一家致力于为老年人提供高质量养老服务的专业机构。拥有着多年的行业经验和深厚的服务实力,一直秉持着“以人为本、关爱生命”的核心理念,始终致力于为老年人提供最优质的养老服务,为晚年生活注入更多的快乐与温暖。
中州养老院坐落在一片幽静的绿树成荫的区域,占地面积超过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 Task | XXL-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
注解直接标注在业务方法上),代码复用性差,分布式场景下难以管理。
- 对比:Spring Task 的调度逻辑与业务代码紧耦合(通过
- 插件化扩展机制
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上拉取它的开源代码
平台类型 | 仓库地址 | 说明 |
---|---|---|
GitHub | GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB) | 国际知名代码托管平台,可获取最新代码、参与开源协作,查看海外开发者提交的 Issue 与 PR |
Gitee | xxl-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分钟执行一次,手动在数据库改一些数据
看到这如果有用的话记得点赞关注哦,后续会更新更多内容的!!