谷粒随享
学习目标:
专辑详情页面渲染需要以下四项数据:
因此接下来,我们需要在专辑微服务、用户微服务中补充RestFul接口实现 并且 提供远程调用Feign API接口给专辑搜索微服务来调用获取。
在专辑检索微服务中编写控制器汇总专辑详情所需数据:
以下是详情需要获取到的数据集
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/67
AlbumInfoApiController 控制器
/**
* 根据专辑Id 获取到统计信息
* @param albumId
* @return
*/
@Operation(summary = "获取到专辑统计信息")
@GetMapping("/albumInfo/getAlbumStatVo/{albumId}")
public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId){
// 获取服务层方法
AlbumStatVo albumStatVo = albumInfoService.getAlbumStatVoByAlbumId(albumId);
return Result.ok(albumStatVo);
}
AlbumInfoService接口
/**
* 根据专辑Id 获取到统计信息
* @param albumId
* @return
*/
AlbumStatVo getAlbumStatVoByAlbumId(Long albumId);
AlbumInfoServiceImpl实现类
@Override
public AlbumStatVo getAlbumStatVoByAlbumId(Long albumId) {
// 调用mapper 层方法
return albumInfoMapper.getAlbumStatVoByAlbumId(albumId);
}
albumInfoMapper.java
/**
* 根据专辑Id 获取到统计信息
* @param albumId
* @return
*/
AlbumStatVo getAlbumStatVoByAlbumId(@Param("albumId") Long albumId);
albumInfoMapper.xml
<!--根据专辑Id 获取到统计数据-->
<select id="getAlbumStatVoByAlbumId" resultType="com.atguigu.tingshu.vo.album.AlbumStatVo">
select
info.album_id,
max(if(info.stat_type = '0401', info.stat_num, 0)) play_stat_num,
max(if(info.stat_type = '0402', info.stat_num, 0)) subscribe_stat_num,
max(if(info.stat_type = '0403', info.stat_num, 0)) buy_stat_num,
max(if(info.stat_type = '0404', info.stat_num, 0)) comment_stat_num
from (select stat.album_id,
stat.stat_type,
stat.stat_num
from album_stat stat
where stat.album_id = #{albumId}) info
group by info.album_id
</select>
service-album-client
模块AlbumFeignClient 接口中添加
/**
* 根据专辑Id 获取到统计信息
* @param albumId
* @return
*/
@GetMapping("/albumInfo/getAlbumStatVo/{albumId}")
public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId);
AlbumDegradeFeignClient熔断类:
@Component
public class AlbumDegradeFeignClient implements AlbumFeignClient {
@Override
public Result<AlbumStatVo> getAlbumStatVo(Long albumId) {
return null;
}
}
回显时,后台需要提供将数据封装到map集合中;
result.put("albumInfo", albumInfo); 获取专辑信息
result.put("albumStatVo", albumStatVo); 获取专辑统计信息
result.put("baseCategoryView", baseCategoryView); 获取分类信息
result.put("announcer", userInfoVo); 获取主播信息
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/69
在service-search
微服务itemApiController 控制器中添加
package com.atguigu.tingshu.search.api;
import com.atguigu.tingshu.common.login.GuiguLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.search.service.ItemService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@Tag(name = "专辑详情管理")
@RestController
@RequestMapping("api/search")
@SuppressWarnings({"all"})
public class itemApiController {
@Autowired
private ItemService itemService;
/**
* 根据专辑Id 获取详情数据
* @param albumId
* @return
*/
@GuiguLogin(required = false)
@Operation(summary = "专辑详情")
@GetMapping("/albumInfo/{albumId}")
public Result getItem(@PathVariable Long albumId){
// 获取到专辑详情数据
Map<String,Object> result = this.itemService.getItem(albumId);
// 返回数据
return Result.ok(result);
}
}
service-util
模块中提供线程池配置类:
package com.atguigu.tingshu.common.thread;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author: atguigu
* @create: 2023-09-25 10:09
*/
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(){
return new ThreadPoolExecutor(
17,
17,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
接口与实现
package com.atguigu.tingshu.search.service;
import java.util.Map;
public interface ItemService {
/**
* 根据专辑Id 获取数据
* @param albumId
* @return
*/
Map<String, Object> getItem(Long albumId);
}
package com.atguigu.tingshu.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.search.service.ItemService;
import com.atguigu.tingshu.user.client.AlbumFeignClient;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class ItemServiceImpl implements ItemService {
@Autowired
private AlbumFeignClient albumFeignClient;
@Autowired
private UserFeignClient userFeignClient;
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
/**
* 汇总专辑详情相关信息
*
* @param albumId
* @return
*/
@Override
public Map<String, Object> getItem(Long albumId) {
Map<String, Object> mapResult = new HashMap<>();
CompletableFuture<AlbumInfo> albumCompletableFuture = CompletableFuture.supplyAsync(() -> {
AlbumInfo albumInfo = albumFeignClient.getAlbumInfoById(albumId).getData();
// 封装albumInfo专辑信息
mapResult.put("albumInfo", albumInfo);
return albumInfo;
}, threadPoolExecutor);
CompletableFuture<Void> albumStatCompletableFuture = CompletableFuture.runAsync(() -> {
AlbumStatVo albumStatVo = albumFeignClient.getAlbumStatVo(albumId).getData();
mapResult.put("albumStatVo", albumStatVo);
}, threadPoolExecutor);
CompletableFuture<Void> baseCategoryViewCompletableFuture = albumCompletableFuture.thenAcceptAsync(albumInfo -> {
BaseCategoryView baseCategoryView = albumFeignClient.getCategoryView(albumInfo.getCategory3Id()).getData();
mapResult.put("baseCategoryView", baseCategoryView);
log.info("baseCategoryView:{}", JSON.toJSONString(baseCategoryView));
}, threadPoolExecutor);
CompletableFuture<Void> announcerCompletableFuture = albumCompletableFuture.thenAcceptAsync(albumInfo -> {
UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData();
mapResult.put("announcer", userInfoVo);
}, threadPoolExecutor);
CompletableFuture.allOf(albumCompletableFuture, albumStatCompletableFuture, baseCategoryViewCompletableFuture, announcerCompletableFuture).join();
return mapResult;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/87
user_paid_album
这张表记录了用户购买过的专辑
如果购买过,则在map 中存储数据 key=trackId value = 1
UserInfoApiController 控制器:
/**
* 判断用户是否购买声音列表
*
* @param albumId
* @param trackIdList
* @return
*/
@Operation(summary = "判断用户是否购买声音列表")
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(@PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> trackIdList) {
// 调用服务层方法
Map<Long, Integer> map = userInfoService.userIsPaidTrack(userId, albumId, trackIdList);
// 返回map 集合数据
return Result.ok(map);
}
UserInfoService接口:
/**
* 判断用户是否购买过声音列表
* @param userId
* @param albumId
* @param trackIdList
* @return
*/
Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> trackIdList);
UserInfoServiceImpl实现类:
@Autowired
private UserPaidAlbumMapper userPaidAlbumMapper;
@Autowired
private UserPaidTrackMapper userPaidTrackMapper;
/**
* 判断用户是否已购买专辑或者专辑下声音
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param trackIdList 专辑下声音ID列表
* @return
*/
@Override
public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> trackIdList) {
//根据用户userId+专辑albumId 获取专辑付费记录
LambdaQueryWrapper<UserPaidAlbum> paidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>();
paidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getUserId, userId)
.eq(UserPaidAlbum::getAlbumId, albumId);
UserPaidAlbum userPaidAlbum = userPaidAlbumMapper.selectOne(paidAlbumLambdaQueryWrapper);
if (userPaidAlbum != null) {
Map<Long, Integer> map = new HashMap<>();
for (Long trackId : trackIdList) {
map.put(trackId, 1);
}
return map;
}
//根据用户ID+声音ID查询声音付费记录
LambdaQueryWrapper<UserPaidTrack> paidTrackQueryWrapper = new LambdaQueryWrapper<>();
paidTrackQueryWrapper.eq(UserPaidTrack::getUserId, userId)
.in(UserPaidTrack::getTrackId, trackIdList);
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(paidTrackQueryWrapper);
if (CollectionUtil.isNotEmpty(userPaidTrackList)) {
//获取已购买声音ID集合
List<Long> paidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
//遍历入参中声音ID判断是否已支付
Map<Long, Integer> map = new HashMap<>();
for (Long trackId : trackIdList) {
if (paidTrackIdList.contains(trackId)) {
map.put(trackId, 1);
} else {
map.put(trackId, 0);
}
}
return map;
} else {
//如果无声音付费记录 则将所有声音标识为未付费
Map<Long, Integer> map = new HashMap<>();
for (Long trackId : trackIdList) {
map.put(trackId, 0);
}
return map;
}
}
service-user-client
模块中UserFeignClient 远程调用接口中添加:
/**
* 判断用户是否购买声音列表
*
* @param albumId
* @param trackIdList
* @return
*/
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(@PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> trackIdList)
UserDegradeFeignClient熔断类:
@Component
public class UserDegradeFeignClient implements UserInfoFeignClient {
@Override
public Result<Map<Long, Integer>> userIsPaidTrack(Long userId, Long albumId, List<Long> trackIdList) {
return null;
}
}
在service-album
微服务中添加控制器. 获取专辑声音列表时,我们将数据都统一封装到AlbumTrackListVo实体类中
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/89
TrackInfoApiController控制器
/**
* 根据专辑Id获取声音列表
*
* @param albumId
* @param page
* @param limit
* @return
*/
@GuiguLogin(required = false)
@Operation(summary = "获取专辑声音分页列表")
@GetMapping("/trackInfo/findAlbumTrackPage/{albumId}/{page}/{limit}")
public Result<IPage<AlbumTrackListVo>> getAlbumTrackPage(
@PathVariable Long albumId,
@PathVariable Long page,
@PathVariable Long limit) {
// 获取用户Id
Long userId = AuthContextHolder.getUserId();
// 构建分页对象
Page<AlbumTrackListVo> pageInfo = new Page<>(page, limit);
// 调用服务层方法
pageInfo = trackInfoService.getAlbumTrackPage(pageInfo, albumId, userId);
// 返回数据
return Result.ok(pageInfo);
}
TrackInfoService接口:
public interface TrackInfoService extends IService<TrackInfo> {
IPage<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageParam, Long albumId, Long userId);
}
TrackInfoServiceImpl实现类:
付费类型: 0101-免费 0102-vip付费 0103-付费
获取到声音Id列表集合 与 用户购买声音Id集合进行比较 将用户购买的声音存储到map中,key=trackId value = 1或0; 1:表示购买过,0:表示没有购买过
如果声音列表不包含,则将显示为付费,否则判断用户是否购买过声音,没有购买过设置为付费
@Autowired
private UserFeignClient userFeignClient;
/**
* 分页获取专辑声音列表
* @param pageParam
* @param albumId
* @param userId
* @return
*/
@Override
public Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageParam, Long albumId, Long userId) {
// 根据专辑Id 获取到专辑
Page<AlbumTrackListVo> pageInfo = trackInfoMapper.getAlbumTrackPage(pageParam, albumId);
// 判断用户是否需要付费:0101-免费 0102-vip付费 0103-付费
AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
Assert.notNull(albumInfo, "专辑对象不能为空");
// 判断用户是否登录
if (null == userId) {
// 除免费的都需要显示付费标识
if (!SystemConstant.ALBUM_PAY_TYPE_FREE.equals(albumInfo.getPayType())) {
//处理试听声音,获取需要付费的声音列表
pageInfo.getRecords().stream().filter(albumTrackListVo ->
//获取付费专辑编号大于5的(非免费声音)
albumTrackListVo.getOrderNum().intValue() > albumInfo.getTracksForFree()
).collect(Collectors.toList()).stream().forEach(albumTrackListVo -> {
// 显示付费通知
albumTrackListVo.setIsShowPaidMark(true);
});
}
} else {
// 用户已登录
// 声明变量是否需要付费,默认不需要付费
boolean isNeedPaid = false;
// vip 付费情况
if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(albumInfo.getPayType())) {
// 获取用户信息
UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData();
// 1. VIP 免费,如果不是vip则需要付费,将这个变量设置为true,需要购买
if (0 == userInfoVo.getIsVip().intValue()) {
isNeedPaid = true;
}
//1.1 如果是vip但是vip过期了(定时任务还为更新状态)
if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().before(new Date())) {
isNeedPaid = true;
} else if (SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(albumInfo.getPayType())) {
// 2. 付费
isNeedPaid = true;
}
}
//需要付费,判断用户是否购买过专辑或声音
if (isNeedPaid) {
List<AlbumTrackListVo> albumTrackNeedPaidListVoList = pageInfo.getRecords().stream().filter(albumTrackListVo -> albumTrackListVo.getOrderNum().intValue() > albumInfo.getTracksForFree()).collect(Collectors.toList());
// 判断
if (!CollectionUtils.isEmpty(albumTrackNeedPaidListVoList)) {
// 判断用户是否购买该声音
// 获取到声音Id 集合列表
List<Long> trackIdList = albumTrackNeedPaidListVoList.stream().map(AlbumTrackListVo::getTrackId).collect(Collectors.toList());
// 获取用户购买的声音列表
Result<Map<Long, Integer>> mapResult = userFeignClient.userIsPaidTrack(userId, albumId, trackIdList);
Assert.notNull(mapResult, "声音集合不能为空.");
Map<Long, Integer> map = mapResult.getData();
Assert.notNull(map, "map集合不能为空.");
albumTrackNeedPaidListVoList.forEach(albumTrackListVo -> {
if (!map.containsKey(albumTrackListVo.getTrackId())) {
// 显示付费
albumTrackListVo.setIsShowPaidMark(false);
} else {
// 如果map.get(albumTrackLis tVo.getTrackId()) == 1 已经购买过,则不显示付费标识;
boolean isBuy = map.get(albumTrackListVo.getTrackId()) == 1 ? false : true;
albumTrackListVo.setIsShowPaidMark(isBuy);
}
});
}
}
}
return pageInfo;
}
TrackInfoMapper接口:条件必须是当前已经开放并且是审核通过状态的数据,并且还需要获取到声音的播放量以及评论数量
@Mapper
public interface TrackInfoMapper extends BaseMapper<TrackInfo> {
/**
* 获取专辑声音列表
* @param pageParam
* @param albumId
* @return
*/
Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageParam, @Param("albumId") Long albumId);
}
TrackInfoMapper.xml 映射文件
<!--根据专辑 Id 获取到声音列表-->
<select id="getAlbumTrackPage" resultType="com.atguigu.tingshu.vo.album.AlbumTrackListVo">
select
info.trackId,
trackTitle,
info.mediaDuration,
info.orderNum,
info.createTime,
MAX(IF(info.statType = '0701', info.statNum, 0)) as playStatNum,
MAX(IF(info.statType = '0704', info.statNum, 0)) as commentStatNum
from
(select
track.id as trackId,
track.track_title as trackTitle,
track.media_duration as mediaDuration,
track.order_num as orderNum,
track.create_time as createTime,
stat.stat_type as statType,
stat.stat_num as statNum
from track_info track
left join track_stat stat on stat.track_id = track.id
where track.album_id = #{albumId} and track.is_open = '1' and track.status = '0501') info
group by info.trackId
order by info.orderNum asc
</select>
详情见:第5章 MongoDB入门.md
播放进度对应的实体类:
@Data
@Schema(description = "UserListenProcess")
@Document
public class UserListenProcess {
@Schema(description = "id")
@Id
private String id;
@Schema(description = "用户id")
private Long userId;
@Schema(description = "专辑id")
private Long albumId;
@Schema(description = "声音id,声音id为0时,浏览的是专辑")
private Long trackId;
@Schema(description = "相对于音频开始位置的播放跳出位置,单位为秒。比如当前音频总时长60s,本次播放到音频第25s处就退出或者切到下一首,那么break_second就是25")
private BigDecimal breakSecond;
@Schema(description = "是否显示")
private Integer isShow;
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "更新时间")
private Date updateTime;
}
在播放声音的时候,会有触发一个获取播放进度的控制器!因为页面每隔10s会自动触发一次保存功能,会将数据写入MongoDB中。所以我们直接从MongoDB中获取到上一次声音的播放时间即可!
YAPI接口:http://192.168.200.6:3000/project/11/interface/api/71
在 service-user
微服务的 UserListenProcessApiController 控制器中添加
/**
* 获取声音播放的时间
* @param trackId
* @return
*/
@GuiguLogin
@Operation(summary = "获取声音的上次跳出时间")
@GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}")
public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) {
// 获取用户Id
Long userId = AuthContextHolder.getUserId();
// 调用服务层方法
BigDecimal trackBreakSecond = userListenProcessService.getTrackBreakSecond(userId, trackId);
// 返回数据
return Result.ok(trackBreakSecond);
}
UserListenProcessService接口:
/**
* 根据用户Id,声音Id 获取到播放进度
* @param userId
* @param trackId
* @return
*/
BigDecimal getTrackBreakSecond(Long userId, Long trackId);
UserListenProcessServiceImpl实现类:
@Override
public BigDecimal getTrackBreakSecond(Long userId, Long trackId) {
// 根据用户Id,声音Id获取播放进度对象
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");
}
页面每隔10秒左右更新播放进度.
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/73
在 UserListenProcessApiController 控制器中添加
/**
* 更新播放进度
* @param userListenProcessVo
* @return
*/
@GuiguLogin
@Operation(summary = "更新播放进度")
@PostMapping("/userListenProcess/updateListenProcess")
public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo) {
// 获取用户Id
Long userId = AuthContextHolder.getUserId();
// 调用服务层方法
userListenProcessService.updateListenProcess(userId, userListenProcessVo);
return Result.ok();
}
UserListenProcessService接口:
/**
* 更新播放进度
* @param userId
* @param userListenProcessVo
*/
void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo);
UserListenProcessServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private KafkaService kafkaService;
@Override
public void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo) {
//1.根据用户ID+声音ID查询声音播放进度
Query query = Query.query(Criteria.where("userId").is(userId).and("trackId").is(userListenProcessVo.getTrackId()));
UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
//2.如果存在进度更新播放时间
if (userListenProcess != null) {
// 设置更新时间
userListenProcess.setUpdateTime(new Date());
// 设置跳出时间
userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond());
// 存储数据
mongoTemplate.save(userListenProcess, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
} else {
//3.如果不存在则创建进度对象新增播放进度对象-第一次播放该声音,更新声音统计信息
// 创建对象
userListenProcess = new UserListenProcess();
// 进行属性拷贝
BeanUtils.copyProperties(userListenProcessVo, userListenProcess);
// 设置Id
userListenProcess.setId(ObjectId.get().toString());
// 设置用户Id
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));
}
//4.避免一天内多次更新统计信息;基于Kafka消息更新专辑统计信息
String ifRepeatStatKey = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + DateUtil.today() + ":" + userId + ":" + userListenProcessVo.getTrackId();
Boolean ifRepeatStat = redisTemplate.opsForValue().setIfAbsent(ifRepeatStatKey, userId, 24, TimeUnit.HOURS);
if (ifRepeatStat) {
//一天内当前用户首次播放
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);
// 发送消息
kafkaService.sendMessage(KafkaConstant.QUEUE_TRACK_STAT_UPDATE, JSON.toJSONString(trackStatMqVo));
}
}
在service-album
微服务中添加监听消息:
package com.atguigu.tingshu.album.receiver;
import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.album.service.TrackInfoService;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class TrackReceiver {
@Autowired
private TrackInfoService trackInfoService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 监听更新统计状态
*
* @param consumerRecord
*/
@KafkaListener(topics = KafkaConstant.QUEUE_TRACK_STAT_UPDATE)
public void updateStat(ConsumerRecord<String, String> consumerRecord) {
// 获取发送的数据
TrackStatMqVo trackStatMqVo = JSON.parseObject(consumerRecord.value(), TrackStatMqVo.class);
log.info("更新声音统计:" + JSON.toJSONString(trackStatMqVo));
// 业务去重
String key = trackStatMqVo.getBusinessNo();
boolean isExist = redisTemplate.opsForValue().setIfAbsent(key, 1, 1, TimeUnit.HOURS);
// 如果key存入成功
if (isExist) {
// 调用服务层方法
trackInfoService.updateStat(trackStatMqVo.getAlbumId(), trackStatMqVo.getTrackId(), trackStatMqVo.getStatType(), trackStatMqVo.getCount());
}
}
}
在TrackInfoService 中添加接口
/**
* 更新统计方法
* @param albumId
* @param trackId
* @param statType
* @param count
*/
void updateStat(Long albumId, Long trackId, String statType,Integer count);
在TrackInfoServiceImpl 中添加实现
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStat(Long albumId, Long trackId, String statType, Integer count) {
// 更新统计数据
trackInfoMapper.updateStat(trackId, statType, count);
// 更新评论数
if(statType.equals(SystemConstant.TRACK_STAT_COMMENT)) {
albumInfoMapper.updateStat(albumId, SystemConstant.ALBUM_STAT_COMMENT, count);
}
// 更新播放量
if(statType.equals(SystemConstant.TRACK_STAT_PLAY)) {
albumInfoMapper.updateStat(albumId, SystemConstant.ALBUM_STAT_PLAY, count);
}
}
TrackInfoMapper.java 添加方法
/**
* 更新评论数据
* @param trackId
* @param statType
* @param count
*/
Integer updateStat(@Param("trackId") Long trackId, @Param("statType") String statType, @Param("count") Integer count);
TrackInfoMapper.xml 实现
<!--更新统计状态-->
<update id="updateStat">
update track_stat
set stat_num = stat_num + #{count}
where track_id = #{trackId} and stat_type = #{statType}
</update>
AlbumInfoMapper.java 接口添加
/**
* 更新评论与播放量
* @param albumId
* @param statType
* @param count
*/
Integer updateStat(@Param("albumId") Long albumId, @Param("statType") String statType, @Param("count") Integer count);
AlbumInfoMapper.xml 实现
<update id="updateStat">
update album_stat
set stat_num = stat_num + #{count}
where album_id = #{albumId} and stat_type = #{statType}
</update>
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/75
统计声音需要更新的数据如下,我们将数据封装到一个实体类中便于操作
@Data
@Schema(description = "用户声音统计信息")
public class TrackStatVo {
@Schema(description = "播放量")
private Integer playStatNum;
@Schema(description = "订阅量")
private Integer collectStatNum;
@Schema(description = "点赞量")
private Integer praiseStatNum;
@Schema(description = "评论数")
private Integer commentStatNum;
}
在TrackInfoApiController 控制器中添加
/**
* 获取声音统计接口
* @param trackId
* @return
*/
@Operation(summary = "获取声音统计信息")
@GetMapping("/trackInfo/getTrackStatVo/{trackId}")
public Result<TrackStatVo> getTrackStatVo(@PathVariable Long trackId) {
// 调用服务层方法
TrackStatVo trackStatVo = trackInfoService.getTrackStatVoByTrackId(trackId);
// 返回对象
return Result.ok(trackStatVo);
}
TrackInfoService接口:
/**
* 根据声音Id 获取统计信息
* @param trackId
* @return
*/
TrackStatVo getTrackStatVoByTrackId(Long trackId);
TrackInfoServiceImpl实现类:
@Override
public TrackStatVo getTrackStatVoByTrackId(Long trackId) {
// 调用mapper 层方法
return trackInfoMapper.getTrackStat(trackId);
}
TrackInfoMapper.java
/**
* 根据声音Id 获取到声音统计
* @param trackId
* @return
*/
TrackStatVo getTrackStat(@Param("trackId") Long trackId);
TrackInfoMapper.xml
<!-- 根据声音Id统计声音状态. -->
<select id="getTrackStat" resultType="com.atguigu.tingshu.vo.album.TrackStatVo">
select
MAX(IF(info.statType = '0701', info.statNum, 0)) as playStatNum,
MAX(IF(info.statType = '0702', info.statNum, 0)) as collectStatNum,
MAX(IF(info.statType = '0703', info.statNum, 0)) as praiseStatNum,
MAX(IF(info.statType = '0704', info.statNum, 0)) as commentStatNum
from (
select
stat.track_id as trackId,
stat.stat_type as statType,
stat.stat_num as statNum
from track_stat stat
where track_id = #{trackId}
) info
group by info.trackId
</select>
我们需要根据用户Id 来获取播放记录 ,需要获取到专辑Id 与声音Id 封装到map中然后返回数据即可!
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/83
控制器 UserListenProcessApiController
/**
* 获取最近一次播放声音
* @return
*/
@GuiguLogin
@Operation(summary = "获取最近一次播放声音")
@GetMapping("/userListenProcess/getLatelyTrack")
public Result<Map<String,Object>> getLatelyTrack() {
// 获取用户Id
Long userId = AuthContextHolder.getUserId();
// 获取播放记录
Map<String,Object> map = userListenProcessService.getLatelyTrack(userId);
// 返回数据
return Result.ok(map);
}
UserListenProcessService接口:
/**
* 获取播放记录
* @param userId
* @return
*/
Map<String, Object> getLatelyTrack(Long userId);
UserListenProcessServiceImpl实现类:
@Override
public Map<String, Object> getLatelyTrack(Long userId) {
Query query = Query.query(Criteria.where("userId").is(userId));
Sort sort = Sort.by(Sort.Direction.DESC, "updateTime");
query.with(sort);
UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
if(null == userListenProcess) {
return null;
}
Map<String, Object> map = new HashMap<>();
map.put("albumId", userListenProcess.getAlbumId());
map.put("trackId", userListenProcess.getTrackId());
return map;
}
手动调用一次更新,查看排行榜。后续会整合xxl-job 分布式定时任务调度框架做定时调用。
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/77
service-album
微服务中BaseCategoryApiController控制器中添加
/**
* 查询所有的一级分类数据
* @return
*/
@Operation(summary = "查询所有的一级分类信息")
@GetMapping("/category/findAllCategory1")
public Result<List<BaseCategory1>> getAllCategory1() {
// 获取所有的一级分类数据
List<BaseCategory1> baseCategory1List = baseCategoryService.getAllCategory1();
return Result.ok(baseCategory1List);
}
BaseCategoryService接口:
/**
* 获取所有一级分类数据
* @return
*/
List<BaseCategory1> getAllCategory1();
BaseCategoryServiceImpl实现类:
@Override
public List<BaseCategory1> getAllCategory1() {
return baseCategory1Mapper.selectList(new LambdaQueryWrapper<BaseCategory1>().orderByAsc(BaseCategory1::getOrderNum));
}
AlbumFeignClient
/**
* 查询所有的一级分类数据
* @return
*/
@GetMapping("/category/findAllCategory1")
public Result<List<BaseCategory1>> getAllCategory1();
AlbumDegradeFeignClient熔断类:
@Override
public Result<List<BaseCategory1>> getAllCategory1() {
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/79
在SearchApiController 中添加控制器
/**
* 更新排行榜
*
* @return
*/
@SneakyThrows
@Operation(summary = "更新排行榜")
@GetMapping("/albumInfo/updateLatelyAlbumRanking")
public Result updateLatelyAlbumRanking() {
// 调用服务层方法
searchService.updateLatelyAlbumRanking();
// 默认返回
return Result.ok();
}
SearchService接口:
/**
* 更新排行榜
*/
void updateLatelyAlbumRanking();
SearchServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
@Override
public void updateLatelyAlbumRanking() {
// 排行榜,按分类维度统计, 先获取分类数据
Result<List<BaseCategory1>> baseCategory1Result = albumFeignClient.getAllCategory1();
Assert.notNull(baseCategory1Result,"对象不能为空");
List<BaseCategory1> baseCategory1List = baseCategory1Result.getData();
if (!CollectionUtils.isEmpty(baseCategory1List)){
// 循环遍历
for (BaseCategory1 baseCategory1 : baseCategory1List) {
// 统计维度:热度:hotScore、播放量:playStatNum、订阅量:subscribeStatNum、购买量:buyStatNum、评论数:albumCommentStatNum
String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"};
for (String rankingDimension : rankingDimensionArray) {
SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(s -> s
.index("albuminfo")
.size(10)
.sort(t -> t.field(f -> f.field(rankingDimension).order(SortOrder.Desc))), AlbumInfoIndex.class);
// 解析查询列表
List<Hit<AlbumInfoIndex>> albumInfoIndexHitList = response.hits().hits();
List<AlbumInfoIndex> albumInfoIndexList = albumInfoIndexHitList.stream().map(hit -> hit.source()).collect(Collectors.toList());
// 将排行榜数据更新到缓存中
redisTemplate.boundHashOps(RedisConstant.RANKING_KEY_PREFIX+baseCategory1.getId()).put(rankingDimension,albumInfoIndexList);
}
}
}
}
点击排行榜的时候,能看到获取排行榜的地址
排行榜:key=ranking:category1Id field = hotScore 或 playStatNum 或 subscribeStatNum 或 buyStatNum 或albumCommentStatNum value=List
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/81
SearchApiController 控制器中添加
/**
* 获取排行榜列表
* @param category1Id
* @param dimension
* @return
*/
@Operation(summary = "获取排行榜列表")
@GetMapping("/albumInfo/findRankingList/{category1Id}/{dimension}")
public Result<List<AlbumInfoIndexVo>> getRankingList(@PathVariable Long category1Id, @PathVariable String dimension) {
// 调用服务层方法
List<AlbumInfoIndexVo> infoIndexVoList = searchService.getRankingList(category1Id, dimension);
// 返回结果集
return Result.ok(infoIndexVoList);
}
SearchService接口:
/**
* 获取排行榜列表
* @param category1Id
* @param dimension
* @return
*/
List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension);
SearchServiceImpl实现类:
@Override
public List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension) {
return (List<AlbumInfoIndexVo>) redisTemplate.boundHashOps(RedisConstant.RANKING_KEY_PREFIX + category1Id).get(dimension);
}