记录充值订单、支付结果处理和 XXL-JOB 的部署与接入,并复盘订单模块涉及的异步处理。

本篇要点

  • 梳理充值订单与支付记录
  • 学习 XXL-JOB 调度中心和执行器
  • 复盘订单异步处理

**学习提示:**课程中的支付、延迟方案与调度配置应按实际代码和环境验证;不要把示例配置视为生产部署结果。

内容回顾

1、设计模式优化

你熟悉设计模式?

你项目中是否使用过设计模式?

你项目做过哪些代码级别优化?

– 使用策略模式优化添加购买记录

– 使用aop + 注解方式实现专辑缓存

  • 策略模式:使用的就是面向对象的多态机制,从而实现同一行为在不同场景下有不同实现

  • 场景:添加购买记录

    订单包含vip、声音和专辑,购买这些都需要添加购买记录,买内容不同添加记录不同

    这个场景使用策略模式进行优化

– 第一步 创建接口,定义添加购买记录方法

– 第二步 根据不同购买类型比较多个实现类,创建vip、声音和专辑实现类,分别实现方法

– 第三步 创建环境类,根据不同购买类型返回不同实现类对象

2、微信支付

  • 发起微信支付请求
  • 查询微信支付状态

项目中,对于微信支付如何判断成功或者失败?

1 发起微信支付请求之后,前端每隔5s查询一次查询支付状态接口看微信是否成功,如果支付中状态等待,如果支付成功更新状态

2 等待微信支付成功回调判断是否支付成功

3 如果微信支付失败,微信端抛出异常,捕获微信返回异常返回给用户

今天内容

1、充值

需求

image-20251110094748180

实现

提交充值订单数据接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Tag(name = "充值管理")
@RestController
@RequestMapping("api/account/rechargeInfo")
@SuppressWarnings({"all"})
public class RechargeInfoApiController {

@Autowired
private RechargeInfoService rechargeInfoService;

/**
* 给用户充值
* @param rechargeInfoVo
* @return
*/
@GuiguLogin
@Operation(summary = "充值")
@PostMapping("submitRecharge")
public Result submitRecharge(@RequestBody RechargeInfoVo rechargeInfoVo){
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
// 调用充值方法
String orderNo =
this.rechargeInfoService.submitRecharge(rechargeInfoVo,userId);
// 创建map 集合对象
HashMap<String, Object> map = new HashMap<>();
// 存储订单Id
map.put("orderNo",orderNo);
// 返回数据
return Result.ok(map);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
@SuppressWarnings({"all"})
public class RechargeInfoServiceImpl extends ServiceImpl<RechargeInfoMapper, RechargeInfo> implements RechargeInfoService {

@Autowired
private RechargeInfoMapper rechargeInfoMapper;

@Override
public String submitRecharge(RechargeInfoVo rechargeInfoVo, Long userId) {
// 创建对象
RechargeInfo rechargeInfo = new RechargeInfo();
// 赋值操作
rechargeInfo.setUserId(userId);
// 0901-未支付
rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_UNPAID);
//充值金额
rechargeInfo.setRechargeAmount(rechargeInfoVo.getAmount());
//充值方式 微信
rechargeInfo.setPayWay(rechargeInfoVo.getPayWay());
//充值订单编号
rechargeInfo.setOrderNo(UUID.randomUUID().toString().replaceAll("-",""));
// 插入数据库
rechargeInfoMapper.insert(rechargeInfo);
// 返回订单编号
return rechargeInfo.getOrderNo();
}
}

保存充值支付记录

1
2
3
4
5
6
7
8
9
10
11
else { //1302-充值
// 充值信息
Result<RechargeInfo> rechargeInfoResult =
rechargeInfoFeignClient.getRechargeInfo(orderNo);
Assert.notNull(rechargeInfoResult,"返回充值对象不能不为空");
RechargeInfo rechargeInfo = rechargeInfoResult.getData();
Assert.notNull(rechargeInfo,"返回充值对象不能不为空");
paymentInfo.setUserId(rechargeInfo.getUserId());
paymentInfo.setContent("充值");
paymentInfo.setAmount(rechargeInfo.getRechargeAmount());
}

支付成功更新

  • 更新充值订单状态(已经支付)
  • 更新账户余额
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public void rechargePaySuccess(String orderNo) {
// 获取到充值信息对象
RechargeInfo rechargeInfo = this.getRechargeInfoByOrderNo(orderNo);
// 如果当前状态是已支付,则直接返回
if (SystemConstant.ORDER_STATUS_PAID
.equals(rechargeInfo.getRechargeStatus())) return;
// 否则赋值为已支付状态
rechargeInfo.setRechargeStatus(SystemConstant.ORDER_STATUS_PAID);
// 更新数据
this.updateById(rechargeInfo);

//更新账户金额
//获取充值金额
BigDecimal rechargeAmount = rechargeInfo.getRechargeAmount();
//获取账户原始金额
Long userId = rechargeInfo.getUserId();
LambdaQueryWrapper<UserAccount> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserAccount::getUserId, userId);

UserAccount userAccount = userAccountService.getOne(wrapper);
//调用
userAccount.setTotalAmount(userAccount.getTotalAmount()
.add(rechargeAmount));
userAccount.setAvailableAmount(userAccount.getAvailableAmount()
.add(rechargeAmount));

LambdaQueryWrapper<UserAccount> wrapper1 = new LambdaQueryWrapper<>();
wrapper1.eq(UserAccount::getUserId, userId);
userAccountService.update(userAccount,wrapper1);
}

2、任务调度框架XXL-JOB

概述

  • 单机版定时任务,使用SpringTask实现

注解+cron表达式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@EnableScheduling //开启定时任务
public class ScheduledTask {

@Autowired
private RabbitService rabbitService;

//每隔5秒执行一次
@Scheduled(cron = "0/5 * * * * ?")
public void task1() {
System.out.println(new Date().toLocaleString());
}
}
  • 分布式定时任务框架

xxl-job: 是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展

XXL-JOB使用

第一步 部署XXL-JOB调度中心

  • 使用调度中心,配置任务执行规则

  • xxl-job调度中心就是springboot项目,启动这个springboot项目有图形化界面,操作都可以在图形化界面进行

  • 首先,从xxl-job官网下载调度中心springboot项目

– 调度中心springboot工程使用jdk1.8环境

image-20251110112443064

  • 其次,修改调度中心配置文件和创建数据库

– 创建数据库

image-20251110112604425

– 修改配置文件

image-20251110112743712

  • 第三,启动调度中心springboot工程

image-20251110112849692

  • 访问调度中心

http://localhost:8080/xxl-job-admin admin/123456

image-20251110113014102

image-20251110113357819

第二步 创建执行器项目,注册到调度中心

首先,修改执行器项目配置文件,添加调度中心地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# web port
server.port=8281

# log config
logging.config=classpath:logback.xml


### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=

### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=C:\\tingshu
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30

其次,创建配置类,XxlJobSpringExecutor对象初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

return xxlJobSpringExecutor;
}

第三步 编写执行器项目具体业务

  • 编写具体业务代码
  • 给业务代码方法起名字 ,使用注解 @XxlJob(“demoJobHandler”)
1
2
3
4
5
6
7
8
9
10
11
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");

for (int i = 0; i < 5; i++) {
XxlJobHelper.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"atguigu....");
}
// default success
}

第四步 在调度中心配置执行器任务

  • 执行器项目注册到调度中心

image-20251110141455910

  • 任务管理配置业务方法执行规则,比如每隔10s执行一次

image-20251110141837745

遇到问题

  • 执行器项目无法注册到调度中心里面

**解决一:**检查调度中心配置文件数据库路径,用户名和密码

​ 检查执行器项目调度中心地址是否正确

**解决二:**在调度中心界面操作(bug)

image-20251110142539228

项目整合XXL-JOB

xxl-job项目应用场景

  • 定期更新排行榜
  • 定期修改过期vip用户的状态

整合XXL-JOB

  • 引入依赖
1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
  • 修改配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xxl:
job:
admin:
# 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册
addresses: http://localhost:8080/xxl-job-admin
# 执行器通讯TOKEN [选填]:非空时启用
accessToken:

executor:
# 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
appname: xxl-job-executor-sample
# 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
address:
# 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
ip:
# 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
port: 9999
# 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
logpath: C:\\tingshu
# 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
logretentiondays: 30
  • 创建配置类,初始化XxlJobSpringExecutor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.atguigu.tingshu.dispatch.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
*
*/
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

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

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

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

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

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

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

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

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

@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

return xxlJobSpringExecutor;
}
}
  • 创建方法具体业务,给方法起名
1
2
3
4
5
6
7
8
9
10
@Slf4j
@Component
public class DispatchJobHandler {

@XxlJob("firstJobHandler")
public void firstJobHandler() {
log.info("xxl-job项目集成测试");
}

}

功能一:定期更新排行榜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Slf4j
@Component
public class DispatchJobHandler {

@Autowired
private SearchFeignClient searchFeignClient;

@Autowired
private XxlJobLogMapper xxlJobLogMapper;

@XxlJob("firstJobHandler")
public void firstJobHandler() {
log.info("xxl-job项目集成测试");
}

//更新排行榜
@XxlJob("updateLatelyAlbumRanking")
public void updateLatelyAlbumRanking() {
// 封装日志对象
XxlJobLog xxlJobLog = new XxlJobLog();
xxlJobLog.setJobId(XxlJobHelper.getJobId());
long startTime = System.currentTimeMillis();
try {
//远程调用,更新排行榜
searchFeignClient.updateLatelyAlbumRanking();
} catch (Exception e) {
xxlJobLog.setStatus(0);//失败
xxlJobLog.setError(ExceptionUtil.getErrorMessage(e));
log.error("定时任务执行失败,任务id为:{}", XxlJobHelper.getJobId());
e.printStackTrace();
} finally {
//耗时
int times = (int) (System.currentTimeMillis() - startTime);
xxlJobLog.setTimes(times);
xxlJobLogMapper.insert(xxlJobLog);
}
}
}

image-20251110144511619

功能二:更新过期vip状态

  • 在service-user创建接口
1
2
3
4
5
6
@Operation(summary = "更新Vip到期失效状态")
@GetMapping("updateVipExpireStatus")
public Result updateVipExpireStatus() {
userInfoService.updateVipExpireStatus();
return Result.ok();
}
1
2
3
4
5
6
7
8
9
10
<!--更新过期vip用户状态-->
<update id="updateVipExpireStatus">
update user_info
set is_vip = 0
where is_vip = 1

and vip_expire_time &lt; now()

and is_deleted = 0
</update>
1
2
3
4
<   转义  &lt;  
<= 转义 &le;
> 转义 &gt;
>= 转义 &le;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//更新过期vip用户状态
@XxlJob("updateVipExpireStatus")
public void updateVipExpireStatus() {
// 封装日志对象
XxlJobLog xxlJobLog = new XxlJobLog();
xxlJobLog.setJobId(XxlJobHelper.getJobId());
long startTime = System.currentTimeMillis();
try {
//远程调用
userInfoFeignClient.updateVipExpireStatus();

} catch (Exception e) {
xxlJobLog.setStatus(0);//失败
xxlJobLog.setError(ExceptionUtil.getErrorMessage(e));
log.error("定时任务执行失败,任务id为:{}", XxlJobHelper.getJobId());
e.printStackTrace();
} finally {
//耗时
int times = (int) (System.currentTimeMillis() - startTime);
xxlJobLog.setTimes(times);
xxlJobLogMapper.insert(xxlJobLog);
}
}

3、总结订单

订单模块回顾

image-20251110154158967

订单模块话术

==订单========
订单流程
根据购买不同类型:vip、专辑、声音,基于优惠折扣计算结算价格
提交订单,首先考虑数据安全,进行签名校验,并且判断是否重复提交,提交订单添加订单数据,包含订单基本信息,详情信息和优惠信息

订单技术:

  • redis判断是否重复提交
  • 策略模式优化添加购买记录(vip、声音、专辑),根据不同的购买类型做不同的操作
  • 延迟消息实现订单超时不支付自动取消,延迟消息使用RabbitMQ+TTL或者延迟插件或者Redisson

==订单========

订单模块我们主要使用了Redis用来防止订单的重复提交,就是在我们生成订单时会生成一个流水号,并把它放入到Redis中,其中key是userId,Value是这个流水号,然后在提交订单时会根据这个key去查询,如果查到了就说明没有重复提交,并把这个值删除,如果没有查询到就说名重复提交了。以及订单支付的延迟消息投递,我们使用的RabbitMq的死信队列+ttl或者Rabbitmq的延迟插件。最后我们还通过策略模式对购买功能进行了优化。
策略模式就是在同一个行为在不同场景的不同操作,项目中实现为:购买的物品(vip,声音、专辑)不同来修改不同的数据库。

可能涉及问题

1、Redis用来防止订单的重复提交

  • **流程:**结算页面向redis放数据,提交订单传递数据,订单比较redis数据和传递数据是否一样,如果相同,删除redis数据,比较删除过程使用lua脚步实现(原子性)

  • **重复提交,**redis数据不会变化吗?

– 结算接口时候向redis里面放数据

– 点击立即结算只是调用提交订单接口,不会调用结算页面接口,redis不会变化

image-20251110160219925

  • redis在项目中还有其他应用场景?

– 使用bitmap计算播放量,同一个用户对于同一个声音24小时计算一次播放量

– 使用redis的hash类型存储排行榜数据,使用任务调度框架实现定期更新排行榜

– 详情添加redis缓存,解决缓存四个问题

…………

2、RabbitMq相关问题

  • 订单超时未支付自动取消。ttl+死信队列和 延迟插件

  • RabbitMQ消息失败?

– 确认模式:到达交换机

– 返回模式:到达队列

– 消费端确认

– 失败重试,使用redis对消息持久化

  • 使用Redisson实现延迟消息

  • Redisson实现分布式锁

– 两个机制:重试机制 和 看门狗机制

  • Redisson实现布隆过滤器

– 使用n个哈希函数计算数据在数组位置,把位置值1

– 获取:使用n个哈希函数计算数据在数组位置,如果有任何一个位置值是0肯定不存在,如果都是1可能存在,因为hash冲突问题

– 存在误判率

3、策略模式

  • 策略模式就是在同一个行为在不同场景的不同操作,面向对象多态机制
  • 添加购买记录使用策略模式优化

4、延迟消息

  • 订单超时未支付自动取消