记录专辑声音列表、声音详情与播放进度处理,梳理跨服务调用和用户身份传递的问题。
本篇要点
- 查询专辑下的声音列表
- 分析声音详情数据来源
- 记录播放进度与远程调用边界
内容回顾
1、根据一级分类Id获取全部分类
2、关键字自动补全功能
- completion :数据加载到内存中,通过前缀匹配
- 自动补全接口
3、logstash
ELK
- 使用logstash收集项目日志信息
- 把收集到日志信息存储到es里面
- 使用kibana分析es日志信息
4、专辑详情
今天内容
1、根据专辑id查询声音列表(复杂)
流程:
1、根据专辑id获取专辑下面所有声音列表,但是声音包含免费和收费的
2、判断用户是否登录状态,如果用户没有登录:
– 判断如果专辑不是免费的,去掉试看的集数,其他是收费的
3、如果用户当前是登录状态:根据专辑类型判断
– 如果vip免费专辑:判断当前登录用户是否是vip用户,如果不是vip收费,如果是vip但是vip过期了也是收费
– 如果付费专辑:需要收费,但是查询当前用户是否购买专辑或者声音
— 如果用户购买过专辑,专辑里面声音可以免费看
— 如果用户购买过专辑里面某些声音,购买的声音可以免费看
实现
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
|
if(userId == null) { String payType = albumInfo.getPayType(); if(!"0101".equals(payType)) { List<AlbumTrackListVo> allAlbumTrackList = pageInfo.getRecords(); Integer tracksForFree = albumInfo.getTracksForFree(); List<AlbumTrackListVo> trackListVoList = allAlbumTrackList.stream().filter(albumTrackListVo -> albumTrackListVo.getOrderNum().intValue()>tracksForFree) .collect(Collectors.toList()); if(!CollectionUtils.isEmpty(trackListVoList)) { trackListVoList.forEach(albumTrackListVo -> { albumTrackListVo.setIsShowPaidMark(true); }); } } }
|
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
| } else { boolean isNeedPaid = false;
if("0102".equals(payType)) { Result<UserInfoVo> userInfoVoResult = userInfoFeignClient.getUserInfoVo(userId); UserInfoVo userInfoVo = userInfoVoResult.getData(); Assert.notNull(userInfoVo,"用户信息为空"); Integer isVip = userInfoVo.getIsVip(); if (isVip.intValue() == 0){ isNeedPaid = true; }
Date vipExpireTime = userInfoVo.getVipExpireTime(); if(isVip.intValue() == 1 && vipExpireTime.before(new Date())) { isNeedPaid = true; } } else if("0103".equals(payType)){ isNeedPaid = true; } }
|
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
| if(isNeedPaid) { List<AlbumTrackListVo> albumTrackNeedPaidListVoList = pageInfo.getRecords().stream() .filter(albumTrackListVo -> albumTrackListVo.getOrderNum().intValue() > albumInfo.getTracksForFree()) .collect(Collectors.toList());
if(!CollectionUtils.isEmpty(albumTrackNeedPaidListVoList)) { List<Long> trackIdList = albumTrackNeedPaidListVoList.stream() .map(AlbumTrackListVo::getTrackId).collect(Collectors.toList());
Result<Map<Long, Integer>> mapResult = userInfoFeignClient.userIsPaidTrack(albumId,trackIdList); Map<Long, Integer> map = mapResult.getData(); Assert.notNull(map,"为空"); albumTrackNeedPaidListVoList.forEach(albumTrackListVo -> { Long trackId = albumTrackListVo.getTrackId();
boolean isMark = map.get(trackId)==1?false:true; albumTrackListVo.setIsShowPaidMark(isMark); }); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @FeignClient(value = "service-user", fallback = UserInfoDegradeFeignClient.class) public interface UserInfoFeignClient {
@GetMapping("api/user/userInfo/getUserInfoVo/{userId}") Result<UserInfoVo> getUserInfoVo(@PathVariable Long userId);
@PostMapping("api/user/userInfo/userIsPaidTrack/{albumId}") Result<Map<Long, Integer>> userIsPaidTrack(@PathVariable("albumId") Long albumId, @RequestBody List<Long> trackIdList);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @GuiguLogin(required = false) @Operation(summary = "判断用户是否购买声音列表") @PostMapping("userIsPaidTrack/{albumId}") public Result<Map<Long, Integer>> userIsPaidTrack(@PathVariable Long albumId, @RequestBody List<Long> trackIdList) { Long userId = AuthContextHolder.getUserId(); Map<Long, Integer> map = userInfoService.userIsPaidTrack(userId, albumId, trackIdList); 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 40 41 42 43 44 45 46 47 48 49 50
| @Override public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> trackIdList) { LambdaQueryWrapper<UserPaidAlbum> wrapper01 = new LambdaQueryWrapper<>(); wrapper01.eq(UserPaidAlbum::getUserId, userId); wrapper01.eq(UserPaidAlbum::getAlbumId, albumId); UserPaidAlbum userPaidAlbum = userPaidAlbumMapper.selectOne(wrapper01);
if(userPaidAlbum != null) { Map<Long, Integer> map = new HashMap<>(); trackIdList.forEach(trackId->{ map.put(trackId,1); }); return map; } else { LambdaQueryWrapper<UserPaidTrack> wrapper02 = new LambdaQueryWrapper<>(); wrapper02.eq(UserPaidTrack::getUserId, userId); wrapper02.in(UserPaidTrack::getTrackId, trackIdList); List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(wrapper02);
List<Long> userPaidTrackIdList = userPaidTrackList.stream() .map(UserPaidTrack::getTrackId) .collect(Collectors.toList());
Map<Long, Integer> map = new HashMap<>();
trackIdList.forEach(trackId->{ boolean contains = userPaidTrackIdList.contains(trackId); if(contains) { map.put(trackId,1); } else { map.put(trackId,0); } }); return map; } }
|
问题
- 远程调用时候,请求头不会传递token值,造成远程调用时候,无法获取当前userId
- 第一种:远程调用时候在方法里面直接传递userId
- 第二种:使用远程调用专用的拦截器,每次远程调用时候,手动把token放到请求头传递

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Component public class FeignInterceptor implements RequestInterceptor {
public void apply(RequestTemplate requestTemplate){ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if(null != requestAttributes) { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)requestAttributes; HttpServletRequest request = servletRequestAttributes.getRequest(); String token = request.getHeader("token"); requestTemplate.header("token", token); } } }
|
2、声音详情
分析
在专辑详情页面,点击某个声音,进入声音详情界面,播放声音文件(存储到腾讯云里面)
记录当前声音播放进度,比如这一次播放到第10s,下次再播放这个声音从第10s向后播放
首先,使用MongoDB存储声音播放进度
其次,前端每隔10s会一次我们后端接口,记录一次当前播放进度
接口1:获取声音上一次播放记录


更新(添加)声音播放记录
- 前端每隔10s调用一次接口,更新当前播放进度
- 在service-user模块编写接口
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Tag(name = "用户声音播放进度管理接口") @RestController @RequestMapping("api/user/userListenProcess") @SuppressWarnings({"all"}) public class UserListenProcessApiController {
@Autowired private UserListenProcessService userListenProcessService;
@GuiguLogin @Operation(summary = "更新播放进度") @PostMapping("/updateListenProcess") public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo) { Long userId = AuthContextHolder.getUserId(); userListenProcessService.saveOrUpdateListenProcess(userId,userListenProcessVo); return Result.ok(); } }
|

service
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
| @Override public void saveOrUpdateListenProcess(Long userId, UserListenProcessVo userListenProcessVo) { Criteria criteria = Criteria.where("userId").is(userId) .and("trackId").is(userListenProcessVo.getTrackId()); Query query = new Query(criteria); UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName( MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
if (userListenProcess != null) { userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond()); userListenProcess.setUpdateTime(new Date());
mongoTemplate.save(userListenProcess, MongoUtil.getCollectionName( MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
} else { userListenProcess = new UserListenProcess(); BeanUtils.copyProperties(userListenProcessVo, userListenProcess); userListenProcess.setId(ObjectId.get().toString()); userListenProcess.setUserId(userId); userListenProcess.setIsShow(1); userListenProcess.setCreateTime(new Date()); userListenProcess.setUpdateTime(new Date()); mongoTemplate.save(userListenProcess, MongoUtil.getCollectionName( MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); }
String key = "user:track:"+userListenProcessVo.getTrackId() + userId; Boolean isExist = redisTemplate.opsForValue().getBit(key, userListenProcessVo.getTrackId()); if (!isExist) { redisTemplate.opsForValue().setBit(key, userListenProcessVo.getTrackId(), true); redisTemplate.expire(key,24*60*60,TimeUnit.SECONDS);
TrackStatMqVo trackStatMqVo = new TrackStatMqVo(); trackStatMqVo.setBusinessNo(UUID.randomUUID().toString().replaceAll("-","")); trackStatMqVo.setAlbumId(userListenProcessVo.getAlbumId()); trackStatMqVo.setTrackId(userListenProcessVo.getTrackId()); trackStatMqVo.setStatType(SystemConstant.TRACK_STAT_PLAY); trackStatMqVo.setCount(1); rabbitService.sendMessage(MqConst.EXCHANGE_TRACK, MqConst.ROUTING_TRACK_STAT_UPDATE, JSON.toJSONString(trackStatMqVo)); } }
|
MQ接收端更新播放量
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
| @Component public class TrackReceiver {
@Autowired private TrackInfoService trackInfoService;
@Autowired private RedisTemplate redisTemplate;
@SneakyThrows @RabbitListener(bindings = @QueueBinding( exchange = @Exchange(value = MqConst.EXCHANGE_TRACK, durable = "true"), value = @Queue(value = MqConst.QUEUE_TRACK_STAT_UPDATE, durable = "true"), key = {MqConst.ROUTING_TRACK_STAT_UPDATE} )) public void updateStat(String content, Message message, Channel channel) { if(!StringUtils.isEmpty(content)) { TrackStatMqVo trackStatMqVo = JSON.parseObject(content, TrackStatMqVo.class); if(trackStatMqVo != null) { trackInfoService.updateStat(trackStatMqVo); } }
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override public void updateStat(TrackStatMqVo trackStatMqVo) { Long trackId = trackStatMqVo.getTrackId(); Integer count = trackStatMqVo.getCount(); String statType = trackStatMqVo.getStatType();
LambdaQueryWrapper<TrackStat> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(TrackStat::getTrackId, trackId); wrapper.eq(TrackStat::getStatType, statType); TrackStat trackStat = trackStatMapper.selectOne(wrapper);
trackStat.setStatNum(trackStat.getStatNum()+count);
trackStatMapper.updateById(trackStat); }
|
获取声音播放进度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@GuiguLogin @Operation(summary = "获取声音的上次跳出时间") @GetMapping("/getTrackBreakSecond/{trackId}") public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) { Long userId = AuthContextHolder.getUserId(); BigDecimal trackBreakSecond = userListenProcessService.getTrackBreakSecond(userId, trackId); return Result.ok(trackBreakSecond); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Override public BigDecimal getTrackBreakSecond(Long userId, Long trackId) { Query query = Query.query(Criteria.where("userId") .is(userId).and("trackId").is(trackId)); UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName( MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); if (null != userListenProcess){ return userListenProcess.getBreakSecond(); } return new BigDecimal("0"); }
|
完善两个地方
问题一
- 代码中,实现同一个用户对于同一个声音,24小时只是计算一次播放量
- 一个用户播放不同声音,redis里面使用相同key,造成过期时间混乱
- 解决: redis里面的key唯一的,比如根据用户id+声音id构建key
1
| String key = "user:track:"+userListenProcessVo.getTrackId() + userId;
|
问题二
保证RabbitMQ消息幂等性
相同消息发送多次,只是消费一次

第一步 通过setnx向redis加数据,key是发送端传递过来业务编号
第二步 如果可以添加成功,证明第一次消费,更新播放量
第三步 如果添加失败,证明消息重复发送了,不进行消费
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
| @SneakyThrows @RabbitListener(bindings = @QueueBinding( exchange = @Exchange(value = MqConst.EXCHANGE_TRACK, durable = "true"), value = @Queue(value = MqConst.QUEUE_TRACK_STAT_UPDATE, durable = "true"), key = {MqConst.ROUTING_TRACK_STAT_UPDATE} )) public void updateStat(String content, Message message, Channel channel) { if(!StringUtils.isEmpty(content)) { TrackStatMqVo trackStatMqVo = JSON.parseObject(content, TrackStatMqVo.class); if(trackStatMqVo != null) { String businessNo = trackStatMqVo.getBusinessNo(); Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(businessNo, 1, 1, TimeUnit.HOURS); if(setIfAbsent) { trackInfoService.updateStat(trackStatMqVo); } } }
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); }
|