梳理订单创建、远程检查和扣减账户余额、消息可靠性与超时未支付订单处理方案。

本篇要点

  • 串联提交订单的业务步骤
  • 检查余额并进行条件扣减
  • 区分消息可靠性与延迟取消方案

**学习提示:**笔记中列出 TTL/死信队列和延迟插件等方案;方案介绍不等于当前项目的超时取消链路已使用 RabbitMQ 延迟消息。

内容回顾

  • 登录
  • 搜索
  • 详情
  • 订单

1、跳转到结算页面

  • 订单:vip、专辑、声音
  • 都是跳转结算页面,显示vip、专辑、声音

2、生成订单接口(一部分)

  • 重复提交
  • 数据加密判断

今天内容

1、订单实现流程

image-20251107091901132

2、订单接口

OrderService订单方法

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//提交订单接口
@Override
public String submitOrder(OrderInfoVo orderInfoVo, Long userId) {
//1 校验数据签名,判断数据是否篡改
// orderInfoVo -- map
//orderInfoVo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN);
String jsonString = JSON.toJSONString(orderInfoVo);
Map map = JSON.parseObject(jsonString, Map.class);
map.put("payWay",SystemConstant.ORDER_PAY_WAY_WEIXIN);
SignHelper.checkSign(map);

//2 判断是否重复提交
//获取传递过来tradeNo值
String tradeNo = orderInfoVo.getTradeNo();
//使用lua脚本保证原子性,完成下面过程
//根据userId 查询redis,查询存储tradeNo
//比较两个值是否相同,如果相同,删除redis数据,第一次提交。
// 如果不相同,重复提交
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) " +
" then return redis.call('del', KEYS[1]) " +
" else return 0 " +
" end";
redisScript.setScriptText(script);
redisScript.setResultType(Boolean.class);
//执行
String key = "user:trade:" + userId;
Boolean flag = (Boolean) redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(key), tradeNo);
// Boolean flag = (Boolean)redisTemplate.
// execute(redisScript, Arrays.asList(key), tradeNo);
if(!flag) { //重复提交
throw new GuiguException(ResultCodeEnum.ORDER_SUBMIT_REPEAT);
}

//判断支付方式
//生成订单编号
String orderNo = UUID.randomUUID().toString().replaceAll("-", "");
//1101-微信 1102-支付宝 1103-账户余额
String payWay = orderInfoVo.getPayWay();
if(!"1103".equals(payWay)) {//微信 支付宝支付
//TODO 生成订单,未支付状态
this.saveOrder(orderInfoVo,userId,orderNo);

} else { //余额支付
//1 远程调用: 检查余额是否充足,扣减余额
//根据最终订单金额 orderAmount,到user_account表查询
AccountLockVo accountDeductVo = new AccountLockVo();
accountDeductVo.setOrderNo(orderNo);
accountDeductVo.setUserId(userId);
accountDeductVo.setAmount(orderInfoVo.getOrderAmount());
accountDeductVo.setContent(orderInfoVo.getOrderDetailVoList()
.get(0).getItemName());
//TODO 检查与扣减账户金额
Result result = userAccountFeignClient.checkAndDeduct(accountDeductVo);
if(result.getCode()!=200) { //余额不足
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}

//检查与扣减账户金额成功
//TODO 创建订单(未支付)
OrderInfo orderInfo = this.saveOrder(orderInfoVo,userId,orderNo);

//远程调用:添加购买记录
// 创建用户购买记录对象
UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
// 记录用户购买信息
userPaidRecordVo.setOrderNo(orderNo);
userPaidRecordVo.setUserId(orderInfo.getUserId());
//购买类型 :vip 专辑 声音
userPaidRecordVo.setItemType(orderInfo.getItemType());
// 购买项目的Id 专辑Id,声音Id(多个), vip的id
List<Long> itemIdList = orderInfoVo.getOrderDetailVoList()
.stream().map(OrderDetailVo::getItemId)
.collect(Collectors.toList());
userPaidRecordVo.setItemIdList(itemIdList);
//TODO 远程调用:
Result userResult = userInfoFeignClient.savePaidRecord(userPaidRecordVo);
if (200 != userResult.getCode()) {
throw new GuiguException(211, "新增购买记录异常");
}
//购买记录添加成功 ,修改订单状态(已经支付)
}
return orderNo;
}

远程调用:检查和扣减余额

分析

方法一:

1
2
3
4
5
6
7
8
9
10
# 检查
# 可用余额是否大于等于支付金额
SELECT * FROM user_account ua
WHERE ua.available_amount>=100;

# 扣减余额
UPDATE user_account ua
SET ua.available_amount=ua.available_amount-50,
ua.total_amount=ua.total_amount-50
WHERE ua.user_id=35

方法二:

1
2
3
4
UPDATE user_account ua 
SET ua.available_amount=ua.available_amount-20,
ua.total_amount=ua.total_amount-20
WHERE ua.user_id=35 and ua.available_amount>=20

image-20251107102131720

1
2
3
4
5
6
//检查和扣减余额
@PostMapping("checkAndDeduct")
Result checkAndDeduct(@RequestBody AccountLockVo accountDeductVo) {
userAccountService.checkAndDeduct(accountDeductVo);
return Result.ok();
}

编写sql实现检查并扣减余额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//检查并扣减余额
@Override
public void checkAndDeduct(AccountLockVo accountDeductVo) {
int count = userAccountMapper.checkAndDeduct(accountDeductVo.getAmount(),
accountDeductVo.getUserId());
if(count == 0) {
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}
//添加账户记录到账户详情表
UserAccountDetail userAccountDetail=new UserAccountDetail();
userAccountDetail.setUserId(accountDeductVo.getUserId());
userAccountDetail.setTitle(accountDeductVo.getContent());
userAccountDetail.setTradeType("1204");
userAccountDetail.setAmount(accountDeductVo.getAmount());
userAccountDetail.setOrderNo(accountDeductVo.getOrderNo());
userAccountDetailMapper.insert(userAccountDetail);
}
1
2
3
4
5
6
7
8
9
10
<!--//检查并扣减余额-->
<update id="checkAndDeduct">
UPDATE user_account
SET total_amount = total_amount - #{amount},
available_amount = available_amount - #{amount}
WHERE
user_id = #{userId}
AND available_amount >= #{amount}
AND is_deleted = 0
</update>

不编写语句,使用mp实现

image-20251107103935657

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
    //检查并扣减余额
@Override
public void checkAndDeduct(AccountLockVo accountDeductVo) {
//编写sql语句实现 检查并扣减余额
// int count = userAccountMapper
// .checkAndDeduct(accountDeductVo.getAmount(), accountDeductVo.getUserId());

// UPDATE user_account ua
// SET ua.available_amount=ua.available_amount-20,
// ua.total_amount=ua.total_amount-20
// WHERE ua.user_id=35
// and ua.available_amount>=20
//不编写语句实现 检查并扣减余额
LambdaQueryWrapper<UserAccount> wrapper = new LambdaQueryWrapper<>();
//WHERE ua.user_id=35
wrapper.eq(UserAccount::getUserId,accountDeductVo.getUserId());
//and ua.available_amount>=20
wrapper.ge(UserAccount::getAvailableAmount,accountDeductVo.getAmount());

//查询userId账户原始金额,减去支付金额,把最终最终金额设置到userAccount对象里面
LambdaQueryWrapper<UserAccount> wrapperOldUserAccount = new LambdaQueryWrapper<>();
wrapperOldUserAccount.eq(UserAccount::getUserId,accountDeductVo.getUserId());
UserAccount userAccount = userAccountMapper.selectOne(wrapperOldUserAccount);

userAccount.setAvailableAmount(userAccount.getAvailableAmount().subtract(accountDeductVo.getAmount()));
userAccount.setTotalAmount(userAccount.getTotalAmount().subtract(accountDeductVo.getAmount()));

//调用update方法,根据条件进行检查和扣减余额
int count = userAccountMapper.update(userAccount, wrapper);
//余额不足
if(count == 0) {
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}

//添加账户记录到账户详情表
UserAccountDetail userAccountDetail=new UserAccountDetail();
userAccountDetail.setUserId(accountDeductVo.getUserId());
userAccountDetail.setTitle(accountDeductVo.getContent());
userAccountDetail.setTradeType("1204");
userAccountDetail.setAmount(accountDeductVo.getAmount());
userAccountDetail.setOrderNo(accountDeductVo.getOrderNo());
userAccountDetailMapper.insert(userAccountDetail);
}

保存订单方法

1
2
3
order_info: 订单基本信息
order_detail:订单明细
order_derate:优惠明细
  • OrderServiceImpl编写
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
//创建订单
private OrderInfo saveOrder(OrderInfoVo orderInfoVo, Long userId, String orderNo) {
//order_info: 订单基本信息
OrderInfo orderInfo = new OrderInfo();
BeanUtils.copyProperties(orderInfoVo, orderInfo);
orderInfo.setOrderNo(orderNo);//订单编号
//订单名称
List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
String itemName = orderDetailVoList.get(0).getItemName();
orderInfo.setOrderTitle(itemName);
//userId
orderInfo.setUserId(userId);
//订单状态 0901-正常 0902-已支付 0903-已取消
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
//调用方法
orderInfoMapper.insert(orderInfo);

//order_detail:订单明细
if (!CollectionUtils.isEmpty(orderInfoVo.getOrderDetailVoList())) {
orderInfoVo.getOrderDetailVoList().forEach(orderDetailVo -> {
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(orderDetailVo, orderDetail);
orderDetail.setOrderId(orderInfo.getId());
orderDetailMapper.insert(orderDetail);
});
}

//order_derate:优惠明细
if (!CollectionUtils.isEmpty(orderInfoVo.getOrderDerateVoList())) {
orderInfoVo.getOrderDerateVoList().forEach(orderDerateVo -> {
OrderDerate orderDerate = new OrderDerate();
BeanUtils.copyProperties(orderDerateVo, orderDerate);
orderDerate.setOrderId(orderInfo.getId());
orderDerateMapper.insert(orderDerate);
});
}

//如果不是余额支付,发送延迟消息

//1101-微信 1102-支付宝 1103-账户余额"
String payWay = orderInfoVo.getPayWay();
if(!"1103".equals(payWay)) {
//发送延迟消息,30分钟不支付订单自动取消
Long orderId = orderInfo.getId();
this.sendDelayMessage(orderId);
}
return orderInfo;
}

发送延迟消息

  • 未支付订单30分钟没有支付自动取消

RabbitMQ消息可靠性配置

第一个方面:

  • 发送消息,首先到达mq服务里面交换机
  • 其次,由交换机把消息转发给队列
  • 第三,消费端监听队列,从队列获取消息进行消费

所以,如果mq消息出现问题,从三个环节找到问题

image-20251107141142970

第二个方面:

  • 使用确认模式判断消息是否到达交换机,如果没有到达进行重试
  • 使用返回模式确认消息是否正确到达队列,如果没有到达进行重试
  • 在消费端进行手动确认

RabbitMQ幂等性保证

  • **幂等性:**相同消息发送多次,只会消费一次

  • 如何保证?

第一种,使用数据库本身,乐观锁(版本号)

第二种,使用Redis里面setnx实现

延迟消息

image-20251107142822526

实现延迟消息方式有很多种:

第一种,使用RabbitMQ的TTL+死信队列实现

**– TTL:**消息存活时间,比如设置消息10s,过了10s消息死亡了

– 死信:

  • 消息过期了
  • 队列满了,再放消息
  • 消费端拒绝签收

– 实现基本过程:

首先,发送消息到达正常队列里面,同时设置消息过期时间,比如30分钟

其次,如果正常队列里面消息过期了,称为死信,死信进入死信队列里面

第三,专门有消费端监听死信队列,当里面有消息过来,肯定过期了,进行消费。比如过了30分钟订单,超时未支付,取消订单

image-20251107143837963

第二种,使用RabbitMQ延迟插件实现

第三种,使用Redisson框架实现

  • Redisson基于Redis封装客户端工具,使用Redisson方便实现很多功能:布隆过滤器,分布式锁,延迟队列等

使用Redisson实现订单超时未支付自动取消

  • 发送延迟消息的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
//延迟消息
private void sendDelayMessage(Long orderId) {
//1 使用redissonClient创建普通队列
RBlockingQueue<Object> blockingQueue = redissonClient.getBlockingQueue(MqConst.EXCHANGE_CANCEL_ORDER);

//2 把普通队列变成延迟队列
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);

//3 向延迟队列添加消息,设置消息过期时间
//delayedQueue.offer(orderId.toString(),30,TimeUnit.MINUTES);
//TODO 为了测试 10s
delayedQueue.offer(orderId.toString(),10,TimeUnit.SECONDS);
}
  • 接收消息(监听队列)方法
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
@Component
public class RedisDelayHandle {

@Autowired
private RedissonClient redissonClient;

@Autowired
private OrderInfoService orderInfoService;
//监听方法
@PostConstruct
public void listener() {
//创建新线程一直监听延迟队列
new Thread(()->{
while(true) {
try {
//从延迟队列获取消息
RBlockingQueue<String> blockingQueue =
redissonClient.getBlockingQueue(MqConst.EXCHANGE_CANCEL_ORDER);

//如果可以获取到,根据消息取消订单
String orderId = blockingQueue.take();

if(StringUtils.hasText(orderId)) {
//调用方法,根据orderId取消订单
orderInfoService.cancelOrder(Long.parseLong(orderId));
}

} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
  • 取消订单的方法
1
2
3
4
5
6
7
8
9
//调用方法,根据orderId取消订单
@Override
public void cancelOrder(Long orderId) {
OrderInfo orderInfo = orderInfoMapper.selectById(orderId);
if(orderInfo.getOrderStatus().equals("0901")) {
orderInfo.setOrderStatus("0903");
orderInfoMapper.updateById(orderInfo);
}
}

远程调用:添加购买记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserInfoApiController {

@Autowired
private UserInfoService userInfoService;

//添加购买记录
/**
* 处理用户购买记录
* @param userPaidRecordVo
* @return
*/
@Operation(summary = "处理用户购买记录")
@PostMapping("/savePaidRecord")
public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo){
userInfoService.savePaidRecord(userPaidRecordVo);
return Result.ok();
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//添加购买记录
@Autowired
private TrackInfoFeignClient trackInfoFeignClient;

@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;

@Autowired
private UserVipServiceMapper userVipServiceMapper;
@Override
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
//获取购买类型 1001-专辑 1002-声音 1003-vip会员
String itemType = userPaidRecordVo.getItemType();
if("1001".equals(itemType)) { //购买专辑
//TODO 判断,购买记录不能重复添加

UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
//用户id
userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
//订单变化
userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
// 专辑Id
Long albumId = userPaidRecordVo.getItemIdList().get(0);
userPaidAlbum.setAlbumId(albumId);
//调用方法
userPaidAlbumMapper.insert(userPaidAlbum);

} else if("1002".equals(itemType)) { //购买声音
//TODO 判断,购买记录不能重复添加

//远程调用:根据声音id获取声音对象,从声音对象里面获取专辑id
Long id = userPaidRecordVo.getItemIdList().get(0);
Result<TrackInfo> result = trackInfoFeignClient.getTrackInfo(id);
TrackInfo trackInfo = result.getData();

List<Long> itemIdList = userPaidRecordVo.getItemIdList();
itemIdList.forEach(trackId->{
UserPaidTrack userPaidTrack = new UserPaidTrack();
userPaidTrack.setTrackId(trackId);
userPaidTrack.setUserId(userPaidRecordVo.getUserId());
userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
//设置专辑id
Long albumId = trackInfo.getAlbumId();
userPaidTrack.setAlbumId(albumId);
userPaidTrackMapper.insert(userPaidTrack);
});
} else { //购买vip
Long userId = userPaidRecordVo.getUserId();
//根据userId获取用户信息
UserInfo userInfo = userInfoMapper.selectById(userId);

//计算vip过期时间
Date currentDate = new Date();

////如果用户是没有过期vip,过期时间=原有过期时间+开通月份
if(userInfo.getIsVip().intValue()==1
&& userInfo.getVipExpireTime().after(new Date())) {
currentDate = userInfo.getVipExpireTime();
}

//如果用户不是vip, 过期时间=当前时间+开通月份
Long vipId = userPaidRecordVo.getItemIdList().get(0);
//根据vipid查询开通月份
VipServiceConfig vipServiceConfig =
vipServiceConfigMapper.selectById(vipId);
Integer serviceMonth = vipServiceConfig.getServiceMonth();
//过期时间= 当前时间+开通月份
Date expireDate =
new LocalDateTime(currentDate)
.plusMonths(serviceMonth).toDate();

//user_vip_service
UserVipService userVipService = new UserVipService();
userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
userVipService.setUserId(userPaidRecordVo.getUserId());
userVipService.setStartTime(new Date());
userVipService.setExpireTime(expireDate);
// 保存
userVipServiceMapper.insert(userVipService);

// 修改user_info.is_vip=1;
userInfo.setIsVip(1);
// 设置过期时间
userInfo.setVipExpireTime(expireDate);
// 修改
this.userInfoMapper.updateById(userInfo);
}
}