记录购买声音的结算页、可购买集数、订单提交前的业务检查及用户余额查询。

本篇要点

  • 进入购买声音的结算流程
  • 计算和展示可购买声音
  • 梳理提交订单与余额查询

内容回顾

1、canal工具

  • canal解决mysql和redis数据一致性问题
  • 底层机制

– 相当于mysql从机,监控主机二进制日志变化

2、订单模块

  • 跳转结算页面需求

– 购买vip

– 购买专辑

– 购买声音

  • 优惠价格计算公式

– 比如100,打八折,优惠20

– 原始价格 * (10-折扣)/ 10

  • 防止重复提交

image-20251105090103165

  • 防止数据被篡改,数据加密

image-20251105090300424

今天内容

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

1、跳转结算页面(购买声音)

1.1 实现流程

image-20251105091942256

  • 实现上面流程,编写两个接口

第一个接口,显示可以购买声音集数

第二个接口,跳转声音购买结算页面

1.2 显示可以购买声音集数接口

image-20251105103933624

image-20251105092421597

1
2
3
4
5
6
7
8
9
//显示可以购买声音集数
@GuiguLogin
@Operation(summary = "获取用户声音分集购买支付列表")
@GetMapping("/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> findUserTrackPaidList(@PathVariable Long trackId) {
List<Map<String, Object>> list =
trackInfoService.findUserTrackPaidList(trackId);
return Result.ok(list);
}
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//显示可以购买声音集数
@Override
public List<Map<String, Object>> findUserTrackPaidList(Long trackId) {
//1 根据声音id获取声音对象(本集)
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
//2 根据声音里面的专辑id获取专辑对象
AlbumInfo albumInfo =
albumInfoMapper.selectById(trackInfo.getAlbumId());

//3 远程调用:根据专辑id + 用户id 查询用户在这个购买过哪些声音
//返回用户在专辑里面购买过声音id列表
Result<List<Long>> isPaidResult =
userInfoFeignClient.findUserIsPaidTrack(trackInfo.getAlbumId());
List<Long> paidTrackIdList = isPaidResult.getData();

//4 获取当前可以购买所有声音,本集后面所有声音
//根据专辑id 查询专辑里面所有声音,条件:大于本集ordernum
LambdaQueryWrapper<TrackInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TrackInfo::getAlbumId,trackInfo.getAlbumId());
wrapper.gt(TrackInfo::getOrderNum, trackInfo.getOrderNum());
List<TrackInfo> trackInfoList = trackInfoMapper.selectList(wrapper);

//从可以购买所有声音集合获取所有声音id
List<Long> idAllList =
trackInfoList.stream().map(TrackInfo::getId)
.collect(Collectors.toList());

//5 把可以购买所有声音中,去掉购买过的声音
// idAllList去掉购买id 购买id集合paidTrackIdList
// (1 2 3 4 5) + (3 4) = 1 2 5
//创建list集合,封装没有购买过声音列表
List<Long> trackIdNoReaptList = new ArrayList<>();
if(CollectionUtils.isEmpty(paidTrackIdList)) { //用户没有购买过
trackIdNoReaptList = idAllList;
} else { ////用户购买过
trackIdNoReaptList =
idAllList.stream()
.filter(id -> !paidTrackIdList.contains(id))
.collect(Collectors.toList());
}

//6 构建显示内容,每部分显示map集合,显示多个,list集合
List<Map<String, Object>> list = new ArrayList<>();
//本集
if(trackIdNoReaptList.size() >=0) {
Map<String, Object> map = new HashMap<>();
map.put("name","本集");
map.put("price",albumInfo.getPrice());
map.put("trackCount", 0);
list.add(map);
}
// 大于0 小于等于10
if(trackIdNoReaptList.size() > 0 && trackIdNoReaptList.size() <= 10) {
Map<String, Object> map = new HashMap<>();
map.put("name","后"+trackIdNoReaptList.size()+"集");
//计算价格
BigDecimal price =
albumInfo.getPrice()
.multiply(new BigDecimal(trackIdNoReaptList.size()));
map.put("price",price);
map.put("trackCount", trackIdNoReaptList.size());
list.add(map);
}
// 19
if(trackIdNoReaptList.size() > 10) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(10));
map.put("name", "后10集");
map.put("price", price);
map.put("trackCount", 10);
list.add(map);
}
// 后20集
if(trackIdNoReaptList.size() > 10 && trackIdNoReaptList.size() <= 20) {
Map<String, Object> map = new HashMap<>();
int count = trackIdNoReaptList.size();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
map.put("name", "后"+count+"集");
map.put("price", price);
map.put("trackCount", count);
list.add(map);
}
if(trackIdNoReaptList.size() > 20) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(20));
map.put("name", "后20集");
map.put("price", price);
map.put("trackCount", 20);
list.add(map);
}

//后30集
if(trackIdNoReaptList.size() > 20 && trackIdNoReaptList.size() <= 30) {
Map<String, Object> map = new HashMap<>();
int count = trackIdNoReaptList.size();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
map.put("name", "后"+count+"集");
map.put("price", price);
map.put("trackCount", count);
list.add(map);
}
if(trackIdNoReaptList.size() > 30) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(30));
map.put("name", "后30集");
map.put("price", price);
map.put("trackCount", 30);
list.add(map);
}

//后50集
if(trackIdNoReaptList.size() > 30 && trackIdNoReaptList.size() <= 50) {
Map<String, Object> map = new HashMap<>();
int count = trackIdNoReaptList.size();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
map.put("name", "后"+count+"集");
map.put("price", price);
map.put("trackCount", count);
list.add(map);
}
// 最多购买50集;
if(trackIdNoReaptList.size() > 50) {
Map<String, Object> map = new HashMap<>();
BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(50));
map.put("name", "后50集");
map.put("price", price);
map.put("trackCount", 50);
list.add(map);
}

return list;
}
  • 远程调用接口
1
2
3
4
5
6
7
8
9
10
11
12
@GuiguLogin
@Operation(summary = "根据专辑id获取用户支付过的声音id列表")
@GetMapping("findUserPaidTrackList/{albumId}")
public Result findUserIsPaidTrack(@PathVariable Long albumId){
// 获取用户Id
Long userId = AuthContextHolder.getUserId();
// 根据用户Id 与 专辑Id 获取到已购买的声音Id 集合列表
List<Long> trackIdList =
this.userInfoService.findPaidUserTrackList(userId,albumId);
// 返回声音Id 集合数据
return Result.ok(trackIdList);
}
1
2
3
4
5
6
7
8
9
10
11
12
//根据专辑id + 用户id获取购买声音id列表
@Override
public List<Long> findPaidUserTrackList(Long userId, Long albumId) {
LambdaQueryWrapper<UserPaidTrack> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserPaidTrack::getUserId, userId);
wrapper.eq(UserPaidTrack::getAlbumId, albumId);
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(wrapper);
// 获取到已购买的声音集合Id 列表
List<Long> trackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
// 返回集合数据
return trackIdList;
}

1.3 购买声音跳转结算页面接口

  • 调用昨天编写接口,跳转到结算页面

image-20251105104818198

image-20251105105032641

  • 在显示购买集数页面中,点击某个买的集数

image-20251105104848965

  • 比如点击本集,前端传递本集声音id
  • 比如点击后5集,前端传递本集声音id
  • 接口中,可以根据本集id获取后多少集数据

image-20251105143421652

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
else if("1002".equals(itemType)) {
//5 购买声音
Integer trackCount = tradeVo.getTrackCount();
if(trackCount.intValue()<0) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}

//获取本集声音id
Long itemId = tradeVo.getItemId();
//远程调用:根据本集id + trackCount(购买集数)获取声音列表数据
// 过滤掉购买过声音
Result<List<TrackInfo>> result =
trackInfoFeignClient.findPaidTrackInfoList(itemId,trackCount);
List<TrackInfo> trackInfoList = result.getData();

//因为声音单价在专辑数据里面,获取专辑对象数据
Long albumId = trackInfoList.get(0).getAlbumId();
Result<AlbumInfo> albumInfoResult =
albumInfoFeignClient.getAlbumInfo(albumId);
AlbumInfo albumInfo = albumInfoResult.getData();

//购买声音没有折扣,实际金额和最终金额相同的
//trackCount=0购买本集,trackCount>0 购买很多集,单价*数量
originalAmount =
trackCount.intValue()>0
? albumInfo.getPrice().multiply(new BigDecimal(trackCount))
: albumInfo.getPrice();
//最终价格
orderAmount = originalAmount;

//封装订单明细
//List<TrackInfo> -- List<OrderDetailVo>
orderDetailVoList = trackInfoList.stream().map(trackInfo -> {
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemName(trackInfo.getTrackTitle());
orderDetailVo.setItemId(trackInfo.getId());
orderDetailVo.setItemUrl(trackInfo.getCoverUrl());
orderDetailVo.setItemPrice(albumInfo.getPrice());
return orderDetailVo;
}).collect(Collectors.toList());

}
  • 远程调用接口
1
2
3
4
5
6
7
8
9
@Operation(summary = "批量获取下单付费声音列表")
@GetMapping("findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>>
findPaidTrackInfoList(@PathVariable Long trackId,
@PathVariable Integer trackCount) {
List<TrackInfo> list =
trackInfoService.findPaidTrackInfoList(trackId,trackCount);
return Result.ok(list);
}
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
//根据本集声音id + 购买集数,获取声音列表
@Override
public List<TrackInfo> findPaidTrackInfoList(Long trackId,
Integer trackCount) {
//1 获取本集声音id获取声音数据
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);

//2 根据本集声音所属专辑id + 用户id 查询用户购买这个专辑声音id列表
Result<List<Long>> userIsPaidTrackResult =
userInfoFeignClient.findUserIsPaidTrack(trackInfo.getAlbumId());
List<Long> paidTrackIdList = userIsPaidTrackResult.getData();

//3 查询声音列表
// 声明一个声音对象集合
List<TrackInfo> trackInfoList = new ArrayList<>();
//3.1 trackCount>0 购买本集后多少集数据
if(trackCount > 0) {
LambdaQueryWrapper<TrackInfo> wrapper = new LambdaQueryWrapper<>();
// 本集声音所属的专辑id
wrapper.eq(TrackInfo::getAlbumId,trackInfo.getAlbumId());

// 大于本集声音ordernum
wrapper.gt(TrackInfo::getOrderNum,trackInfo.getOrderNum());

// ordernum排序
wrapper.orderByAsc(TrackInfo::getOrderNum);

// 排除掉购买过声音
if (!CollectionUtils.isEmpty(paidTrackIdList)){
wrapper.notIn(TrackInfo::getId,paidTrackIdList);
}

// 限制查询多少集声音
wrapper.last(" limit "+trackCount);

//调用方法
trackInfoList = trackInfoMapper.selectList(wrapper);
} else {
//3.2 trackCount==0 购买本集
trackInfoList.add(trackInfo);
}
//返回集合
return trackInfoList;
}

2、提交订单接口(完成一部分)

  • 在结算页面中(购买vip、购买声音、购买专辑)
  • 在结算页面中,点击 立即结算,生成订单,后续支付一系列流程

2.1 订单提交流程

流程

1、订单包含购买vip、购买专辑和购买声音

2、购买声音:

声音只能使用余额进行支付

使用余额支付,首先创建声音订单(当前状态未支付)

​ 其次检查余额是否充足,如果余额充足进行支付

​ 第三余额支付成功之后,做几件事情:扣减余额、修改订单状态(已经支付),添加声音购买记录

3、购买vip:

购买vip可以使用余额支付,可以使用微信支付

余额支付流程和购买声音类似的

微信支付:首先创建订单(当前状态未支付)

​ 其次约定订单30分钟不支付自动取消

​ 第三如果30分钟使用微信支付了,等待微信支付成功

​ 第四微信支付成功之后,做几件事情:修改订单状态(已经支付),更新vip状态

4、购买专辑:

购买专辑可以使用余额支付,可以使用微信支付

余额支付流程和购买声音类似的

微信支付:首先创建订单(当前状态未支付)

​ 其次约定订单30分钟不支付自动取消

​ 第三如果30分钟使用微信支付了,等待微信支付成功

​ 第四微信支付成功之后,做几件事情:修改订单状态(已经支付),添加专辑购买记录

技术

  • **延迟队列:**订单30分钟不支付自动取消

– 使用RabbitMQ的TTL+死信队列实现

– 使用RabbitMQ延迟插件

– 使用Redisson实现延迟队列效果

  • 策略模式优化

  • 微信支付

2.2 订单接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//提交订单接口
@GuiguLogin
@Operation(summary = "提交订单")
@PostMapping("submitOrder")
public Result<Map<String, Object>>
submitOrder(@RequestBody @Validated OrderInfoVo orderInfoVo) {
// 获取到用户Id
Long userId = AuthContextHolder.getUserId();
String orderNo = orderInfoService.submitOrder(orderInfoVo,userId);
Map<String, Object> map = new HashMap();
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
28
29
30
31
32
33
34
35
36
37
38
39
//提交订单接口
@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);
}

//TODO


return "";
}

3、获取用户可用余额

image-20251105162424635

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 获取账户可用余额
* @return
*/
@GuiguLogin
@Operation(summary = "获取账号可用金额")
@GetMapping("getAvailableAmount")
public Result<BigDecimal> getAvailableAmount() {
// 调用服务层方法
return Result.ok(userAccountService
.getAvailableAmount(AuthContextHolder.getUserId()));
}
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public BigDecimal getAvailableAmount(Long userId) {
// 根据用户Id 获取到用户余额对象
UserAccount userAccount = this.getUserAccountByUserId(userId);
return userAccount.getAvailableAmount();
}

@Override
public UserAccount getUserAccountByUserId(Long userId) {
return this.getOne(new LambdaQueryWrapper<UserAccount>()
.eq(UserAccount::getUserId, userId));
}