谷粒随享
学习目标:
专辑详情页面渲染需要以下四项数据:
因此接下来,我们需要在专辑微服务、用户微服务中补充RestFul接口实现 并且 提供远程调用Feign API接口给搜索微服务来调用获取。
在专辑搜索微服务中编写控制器汇总专辑详情所需数据:
以下是详情需要获取到的数据集
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/67
AlbumInfoApiController 控制器
/**
* 根据专辑ID查询专辑统计信息
* @param albumId
* @return
*/
@Operation(summary = "根据专辑ID查询专辑统计信息")
@GetMapping("/albumInfo/getAlbumStatVo/{albumId}")
public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId){
AlbumStatVo vo = albumInfoService.getAlbumStatVo(albumId);
return Result.ok(vo);
}
AlbumInfoService接口
/**
* 根据专辑ID查询专辑统计信息
* @param albumId
* @return
*/
AlbumStatVo getAlbumStatVo(Long albumId);
AlbumInfoServiceImpl实现类
/**
* 根据专辑ID查询专辑统计信息
* @param albumId
* @return
*/
@Override
public AlbumStatVo getAlbumStatVo(Long albumId) {
return albumInfoMapper.getAlbumStatVo(albumId);
}
albumInfoMapper.java
/**
* 根据专辑ID查询专辑统计信息
*
* @param albumId
* @return
*/
AlbumStatVo getAlbumStatVo(@Param("albumId") Long albumId);
albumInfoMapper.xml
<!--据专辑ID查询专辑统计信息-->
<select id="getAlbumStatVo" resultType="com.atguigu.tingshu.vo.album.AlbumStatVo">
select
stat.album_id,
max(if(stat.stat_type='0401', stat.stat_num, 0)) playStatNum,
max(if(stat.stat_type='0402', stat.stat_num, 0)) subscribeStatNum,
max(if(stat.stat_type='0403', stat.stat_num, 0)) buyStatNum,
max(if(stat.stat_type='0404', stat.stat_num, 0)) commentStatNum
from album_stat stat
where stat.album_id = #{albumId} and stat.is_deleted = 0
group by stat.album_id
</select>
service-album-client
模块AlbumFeignClient 接口中添加
/**
* 根据专辑ID查询专辑统计信息
* @param albumId
* @return
*/
@GetMapping("/albumInfo/getAlbumStatVo/{albumId}")
public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId);
AlbumDegradeFeignClient熔断类:
@Override
public Result<AlbumStatVo> getAlbumStatVo(Long albumId) {
log.error("远程调用专辑服务getAlbumStatVo服务降级");
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.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;
/**
* 获取渲染专辑详情页面所需数据
*
* @param albumId
* @return
*/
@Operation(summary = "获取渲染专辑详情页面所需数据")
@GetMapping("/albumInfo/{albumId}")
public Result<Map<String, Object>> getItemData(@PathVariable Long albumId) {
Map<String, Object> map = itemService.getItemData(albumId);
return Result.ok(map);
}
}
接口与实现
package com.atguigu.tingshu.search.service;
import java.util.Map;
public interface ItemService {
/**
* 获取渲染专辑详情页面所需数据
*
* @param albumId
* @return
*/
Map<String, Object> getItemData(Long albumId);
}
package com.atguigu.tingshu.search.service.impl;
import cn.hutool.core.lang.Assert;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.search.repository.AlbumInfoIndexRepository;
import com.atguigu.tingshu.search.service.ItemService;
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.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
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 {albumInfo:{},baseCategoryView:{},albumStatVo:{},announcer:{}}
*/
@Override
public Map<String, Object> getItemData(Long albumId) {
//1.创建结果Map对象 存在多线程写Map,提供线程安全聚合类ConcurrentHashMap
Map<String, Object> map = new ConcurrentHashMap<>();
//2.远程调用"专辑服务"根据ID查询专辑消息
CompletableFuture<AlbumInfo> albumInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
Assert.notNull(albumInfo, "专辑:{}不存在!", albumId);
map.put("albumInfo", albumInfo);
return albumInfo;
}, threadPoolExecutor);
//3.远程调用"专辑服务"根据专辑三级分类ID查询分类消息
CompletableFuture<Void> baseCategoryViewCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
BaseCategoryView baseCategoryView = albumFeignClient.getCategoryView(albumInfo.getCategory3Id()).getData();
Assert.notNull(baseCategoryView, "专辑分类:{}不存在!", albumId);
map.put("baseCategoryView", baseCategoryView);
}, threadPoolExecutor);
//4.远程调用"专辑服务"根据专辑ID查询统计消息
CompletableFuture<Void> albumStatVoCompletableFuture = CompletableFuture.runAsync(() -> {
AlbumStatVo albumStatVo = albumFeignClient.getAlbumStatVo(albumId).getData();
Assert.notNull(albumStatVo, "专辑统计:{}不存在!", albumId);
map.put("albumStatVo", albumStatVo);
}, threadPoolExecutor);
//5.远程调用"用户服务"根据专辑用户ID查询用户信息
CompletableFuture<Void> announcerCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData();
Assert.notNull(userInfoVo, "专辑主播:{}不存在!", albumId);
map.put("announcer", userInfoVo);
}, threadPoolExecutor);
//6.组合异步任务
CompletableFuture.allOf(
albumInfoCompletableFuture,
albumStatVoCompletableFuture,
baseCategoryViewCompletableFuture,
announcerCompletableFuture
).join();
return map;
}
}
需求:根据专辑ID分页查询声音列表,返回当前页10条记录,对每条声音付费标识处理。关键点:哪个声音需要展示付费标识。
默认每个声音付费标识为:false
判断专辑付费类型:0101-免费、0102-vip免费、0103-付费
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/87
user_paid_album
这张表记录了用户购买过的专辑
user_paid_track
这张表记录了用户购买过的声音
如果购买过,则在map 中存储数据 key=trackId value = 1 未购买value则返回0
例如:
某专辑第一页,除了试听的声音(前五)从6-10个声音需要在用户微服务中判断5个声音是否购买过
用户翻到第二页,从11-20个声音同样需要判断用户购买情况
UserInfoApiController 控制器:
/**
* 专辑详情中每页声音列表(非免费试听),对提交专辑以及声音ID进行判断得出每个声音购买情况
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param needCheckBuyStausTrackIdList 需要判断购买状态声音ID集合
* @return {20158:1,20159:1}
*/
@Operation(summary = "专辑详情中每页声音列表,对提交专辑以及声音ID进行判断得出每个声音购买情况")
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(
@PathVariable Long userId,
@PathVariable Long albumId,
@RequestBody List<Long> needCheckBuyStausTrackIdList
) {
Map<Long, Integer> map = userInfoService.getCheckBuyStausTrackIdList(userId, albumId, needCheckBuyStausTrackIdList);
return Result.ok(map);
}
UserInfoService接口:
/**
* 专辑详情中每页声音列表(非免费试听),对提交专辑以及声音ID进行判断得出每个声音购买情况
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param needCheckBuyStausTrackIdList 需要判断购买状态声音ID集合
* @return {20158:1,20159:1}
*/
Map<Long, Integer> getCheckBuyStausTrackIdList(Long userId, Long albumId, List<Long> needCheckBuyStausTrackIdList);
UserInfoServiceImpl实现类:
@Autowired
private UserPaidAlbumMapper userPaidAlbumMapper;
@Autowired
private UserPaidTrackMapper userPaidTrackMapper;
/**
* 专辑详情中每页声音列表(非免费试听),对提交专辑以及声音ID进行判断得出每个声音购买情况
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param needCheckBuyStausTrackIdList 需要判断购买状态声音ID集合
* @return {20158:1,20159:1}
*/
@Override
public Map<Long, Integer> getCheckBuyStausTrackIdList(Long userId, Long albumId, List<Long> needCheckBuyStausTrackIdList) {
Map<Long, Integer> map = new HashMap<>();
//1.判断用户是否已购买专辑
//1.1 根据用户ID+专辑ID查询专辑购买情况
LambdaQueryWrapper<UserPaidAlbum> userPaidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>();
userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getUserId, userId);
userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getAlbumId, albumId);
userPaidAlbumLambdaQueryWrapper.last("limit 1");
Long count = userPaidAlbumMapper.selectCount(userPaidAlbumLambdaQueryWrapper);
//1.2 如果购买专辑,将提交所有声音购买情况设置为:1 返回
if (count > 0) {
for (Long trackId : needCheckBuyStausTrackIdList) {
map.put(trackId, 1);
}
return map;
}
//2.判断用户购买声音情况
//2.1 根据用户ID+专辑ID查询已购声音列表
LambdaQueryWrapper<UserPaidTrack> userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>();
userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getUserId, userId);
userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getAlbumId, albumId);
userPaidTrackLambdaQueryWrapper.select(UserPaidTrack::getTrackId);
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(userPaidTrackLambdaQueryWrapper);
//2.2 如果不存在已购声音,将提交所有声音购买情况设置为:0 返回
if (CollectionUtil.isEmpty(userPaidTrackList)) {
for (Long trackId : needCheckBuyStausTrackIdList) {
map.put(trackId, 0);
}
return map;
}
//2.3 如果存在已购声音,动态判断声音购买情况(遍历待检查声音购买情况ID列表,判断ID是否包含在已购声音ID列表中)
List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
for (Long trackId : needCheckBuyStausTrackIdList) {
if (userPaidTrackIdList.contains(trackId)) {
map.put(trackId, 1);
} else {
map.put(trackId, 0);
}
}
return map;
}
service-user-client
模块中UserFeignClient 远程调用接口中添加:
/**
* 专辑详情中每页声音列表(非免费试听),对提交专辑以及声音ID进行判断得出每个声音购买情况
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param needCheckBuyStausTrackIdList 需要判断购买状态声音ID集合(除去掉免费试听)
* @return {20158:1,20159:1}
*/
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(
@PathVariable Long userId,
@PathVariable Long albumId,
@RequestBody List<Long> needCheckBuyStausTrackIdList
);
UserDegradeFeignClient熔断类:
@Override
public Result<Map<Long, Integer>> userIsPaidTrack(Long userId, Long albumId, List<Long> needCheckBuyStausTrackIdList) {
log.error("[用户服务]提供远程调用userIsPaidTrack服务降级");
return null;
}
在service-album
微服务中添加控制器. 获取专辑声音列表时,我们将数据都统一封装到AlbumTrackListVo实体类中
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/89
TrackInfoApiController控制器
/**
* 分页展示专辑下声音列表-动态显示付费标识
* 根据用户登录状态、身份、专辑付费类型、购买情况综合判断付费标识
*
* @param albumId
* @param page
* @param limit
* @return 返回每一页声音列表(动态判断付费标识)
*/
@GuiGuLogin(required = false)
@Operation(summary = "分页展示专辑下声音列表-动态显示付费标识")
@GetMapping("/trackInfo/findAlbumTrackPage/{albumId}/{page}/{limit}")
public Result<Page<AlbumTrackListVo>> getAlbumTrackPage(@PathVariable Long albumId, @PathVariable int page, @PathVariable int limit) {
//1.尝试获取用户ID
Long userId = AuthContextHolder.getUserId();
//2.创建MP分页对象
Page<AlbumTrackListVo> pageInfo = new Page<>(page, limit);
//3.调用业务层查询声音分页列表
pageInfo = trackInfoService.getAlbumTrackPage(pageInfo, userId, albumId);
return Result.ok(pageInfo);
}
TrackInfoService接口:
/**
* 分页展示专辑下声音列表-动态显示付费标识
* 根据用户登录状态、身份、专辑付费类型、购买情况综合判断付费标识
*
* @param pageInfo
* @param userId
* @param albumId
* @return
*/
Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, Long userId, Long albumId);
TrackInfoServiceImpl实现类:
付费类型: 0101-免费 0102-vip付费 0103-付费
获取到声音Id列表集合 与 用户购买声音Id集合进行比较 将用户购买的声音存储到map中,key=trackId value = 1或0; 1:表示购买过,0:表示没有购买过
如果声音列表不包含,则将显示为付费,否则判断用户是否购买过声音,没有购买过设置为付费
@Autowired
private UserFeignClient userFeignClient;
/**
* 分页展示专辑下声音列表-动态显示付费标识
* 根据用户登录状态、身份、专辑付费类型、购买情况综合判断付费标识
*
* @param pageInfo 分页对象
* @param userId 用户ID
* @param albumId 专辑ID
* @return
*/
@Override
public Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, Long userId, Long albumId) {
//1.获取指定专辑下包含声音分页列表(不考虑付费标识 AlbumTrackListVo中付费标识:false)
pageInfo = trackInfoMapper.getAlbumTrackPage(pageInfo, albumId);
//TODO 根据用户登录状态、身份、专辑付费类型、购买情况综合判断付费标识
//2. 根据专辑ID查询专辑信息-获取专辑付费类型-付费类型: 0101-免费、0102-vip免费、0103-付费
AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
//2.1 获取专辑付费类型
String payType = albumInfo.getPayType();
//2.2 获取专辑免费试听集数
Integer tracksForFreeCount = albumInfo.getTracksForFree();
//3. 处理用户未登录情况
if (userId == null) {
//3.1 判断专辑付费类型是否为VIP免费或者付费
if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(payType) || SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(payType)) {
//3.2 将本页中声音列表除去免费试听声音将声音付费标识设置为:true
pageInfo.getRecords()
.stream()
.filter(albumTrackListVo -> albumTrackListVo.getOrderNum() > tracksForFreeCount) //去除免费试听声音
.forEach(albumTrackListVo -> {
albumTrackListVo.setIsShowPaidMark(true);
});
}
} else {
//4. 处理用户已登录情况
//4.1 远程调用用户服务获取用户信息,得到用户身份
UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData();
Assert.notNull(userInfoVo, "用户:{}不存在", userId);
Boolean isVIP = false;
if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(new Date())) {
isVIP = true;
}
//4.2 判断专辑付费类型-是否需要进一步判断用户购买情况
Boolean isNeedCheck = false;
//4.2.1 判断专辑付费类型是否为:VIP免费
if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(payType)) {
if (!isVIP) {
//当前用户为普通用户 专辑类型为VIP免费 需要进一步判断购买情况
isNeedCheck = true;
}
}
//4.2.2 判断专辑付费类型是否为:付费-所有用户都需要进一步判断购买情况
if (SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(payType)) {
isNeedCheck = true;
}
//4.3 需要进一步判断购买情况
if (isNeedCheck) {
//4.3.1 将免费试听声音排除掉得到需要判断购买情况声音ID集合
List<Long> needCheckBuyStausTrackIdList = pageInfo
.getRecords()
.stream()
.filter(albumTrackListVo -> albumTrackListVo.getOrderNum() > tracksForFreeCount) //去除免费试听声音
.map(AlbumTrackListVo::getTrackId) //映射获取声音ID
.collect(Collectors.toList()); //收集得到需要判断购买情况声音ID集合
//4.3.2 远程调用用户服务判断本页中声音ID列表购买情况 {声音ID:1/0}
Map<Long, Integer> buyStatusMap = userFeignClient.userIsPaidTrack(userId, albumId, needCheckBuyStausTrackIdList).getData();
//4.3.3 根据返回购买情况Map,判断是否需要将标识改为true(未购买声音)
pageInfo
.getRecords()
.stream()
.filter(albumTrackListVo -> albumTrackListVo.getOrderNum() > tracksForFreeCount)
.forEach(albumTrackListVo -> {
//找出本页中未购买声音ID,将付费标识设置:true
if (buyStatusMap.get(albumTrackListVo.getTrackId()).intValue() == 0) {
albumTrackListVo.setIsShowPaidMark(true);
}
});
}
}
return pageInfo;
}
TrackInfoMapper接口:条件必须是当前已经开放并且是审核通过状态的数据,并且还需要获取到声音的播放量以及评论数量
/**
* 分页展示专辑下声音列表
* @param pageInfo 分页对象 MP会自动进行分页
* @param albumId 专辑ID
* @return
*/
Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, @Param("albumId") Long albumId);
TrackInfoMapper.xml 映射文件
动态SQL
#分页查询指定专辑下包含声音列表(包含统计信息)
select * from track_info where album_id = 307;
select * from track_info where album_id = 307 and id = 16289;
select * from track_stat where track_id = 16289;
select
ti.id trackId,
ti.track_title trackTitle,
ti.media_duration mediaDuration,
ti.order_num orderNum,
ti.create_time createTime,
max(if(ts.stat_type='0701', ts.stat_num, 0)) playStatNum,
max(if(ts.stat_type='0702', ts.stat_num, 0)) collectStatNum,
max(if(ts.stat_type='0703', ts.stat_num, 0)) praiseStatNum,
max(if(ts.stat_type='0704', ts.stat_num, 0)) commentStatNum
from track_info ti left join track_stat ts
on ts.track_id = ti.id
where ti.album_id = 307 and ti.is_deleted = 0
group by ti.id
order by ti.order_num
<!--分页展示专辑下声音列表-->
<select id="getAlbumTrackPage" resultType="com.atguigu.tingshu.vo.album.AlbumTrackListVo">
select
ti.id trackId,
ti.track_title,
ti.cover_url,
ti.media_duration,
ti.order_num,
ti.create_time,
max(if(ts.stat_type='0701', ts.stat_num, 0)) playStatNum,
max(if(ts.stat_type='0702', ts.stat_num, 0)) collectStatNum,
max(if(ts.stat_type='0703', ts.stat_num, 0)) praiseStatNum,
max(if(ts.stat_type='0704', ts.stat_num, 0)) commentStatNum
from track_info ti inner join track_stat ts
on ts.track_id = ti.id
where ti.album_id = #{albumId} and ti.is_deleted = 0
group by ti.id
order by order_num asc
</select>
测试:
情况一:未登录情况,专辑付费类型:VIP免费 付费 查看声音列表->试听声音免费+其余都需要展示付费标识
情况二:登录情况
详情见:第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(required = false)
@Operation(summary = "查询当前用户声音播放进度")
@GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}")
public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) {
//1.获取用户ID
Long userId = AuthContextHolder.getUserId();
//2.获取声音播放进度
BigDecimal breakSecond = userListenProcessService.getTrackBreakSecond(userId, trackId);
return Result.ok(breakSecond);
}
UserListenProcessService接口:
/**
* 查询当前用户声音播放进度
*
* @param userId
* @param trackId
* @return
*/
BigDecimal getTrackBreakSecond(Long userId, Long trackId);
UserListenProcessServiceImpl实现类:
package com.atguigu.tingshu.user.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.service.KafkaService;
import com.atguigu.tingshu.common.util.MongoUtil;
import com.atguigu.tingshu.model.user.UserListenProcess;
import com.atguigu.tingshu.user.service.UserListenProcessService;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.atguigu.tingshu.vo.user.UserListenProcessVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Service
@SuppressWarnings({"all"})
public class UserListenProcessServiceImpl implements UserListenProcessService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 查询当前用户声音播放进度
*
* @param userId
* @param trackId
* @return
*/
@Override
public BigDecimal getTrackBreakSecond(Long userId, Long trackId) {
//1.用户未登录,返回0
if (userId == null) {
return new BigDecimal("0.00");
}
//2.根据用户ID+声音ID查询播放进度记录
//2.1 创建查询对象-封装查询条件
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(userId).and("trackId").is(trackId));
query.with(Sort.by(Sort.Direction.DESC, "updateTime"));
query.limit(1);
//2.2 动态构建用户播放进度集合 形式:前缀_用户ID
String collName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collName);
if (userListenProcess != null) {
return userListenProcess.getBreakSecond();
}
return new BigDecimal("0.00");
}
}
页面每隔10秒左右更新播放进度.
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/73
在 UserListenProcessApiController 控制器中添加
/**
* 更新当前用户声音播放进度
*
* @param userListenProcessVo
* @return
*/
@GuiGuLogin(required = false)
@Operation(summary = "更新当前用户声音播放进度")
@PostMapping("/userListenProcess/updateListenProcess")
public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo) {
//1.获取用户ID
Long userId = AuthContextHolder.getUserId();
if (userId != null) {
userListenProcessService.updateListenProcess(userId, userListenProcessVo);
}
return Result.ok();
}
UserListenProcessService接口:
/**
* 更新当前用户声音播放进度
*
* @param userListenProcessVo
* @return
*/
void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo);
UserListenProcessServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private KafkaService kafkaService;
/**
* 更新当前用户声音播放进度
*
* @param userListenProcessVo
* @return
*/
@Override
public void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo) {
//1.根据用户ID+专辑ID+声音ID查询播放进度记录
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(userId).and("albumId").is(userListenProcessVo.getAlbumId()).and("trackId").is(userListenProcessVo.getTrackId()));
query.limit(1);
String collName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collName);
//2.如果播放进度存在则更新(时长,更新时间)
BigDecimal breakSeckond = userListenProcessVo.getBreakSecond().setScale(2, RoundingMode.HALF_UP);
if (userListenProcess != null) {
userListenProcess.setBreakSecond(breakSeckond);
userListenProcess.setUpdateTime(new Date());
} else {
//3.如果播放进度不存在则新增
userListenProcess = new UserListenProcess();
userListenProcess.setUserId(userId);
userListenProcess.setAlbumId(userListenProcessVo.getAlbumId());
userListenProcess.setTrackId(userListenProcessVo.getTrackId());
userListenProcess.setBreakSecond(breakSeckond);
userListenProcess.setCreateTime(new Date());
userListenProcess.setUpdateTime(new Date());
}
mongoTemplate.save(userListenProcess, collName);
//3.TODO 更新声音已经专辑播放统计数量
//3.1 避免规定时间内多次重复更新统计数值 采用redis的set k v [ex] [nx]命令实现
//3.1.1 创建避免多次重复更新Key 形式:包含用户ID,专辑ID,声音ID
String key = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + userId + "_" + userListenProcessVo.getAlbumId() + "_" + userListenProcessVo.getTrackId();
//3.1.2 计算key的过期时间 当日结束时间-当前系统时间
long ttl = DateUtil.endOfDay(new Date()).getTime() - System.currentTimeMillis();
//3.1.2 调用redis的set nx存入Key,写入数据失败说明在重复操作
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, null, ttl, TimeUnit.MILLISECONDS);
//3.2 发送Kafka消息进行异步更新统计数值
if (flag) {
//3.1 构建更新声音统计消息VO对象
TrackStatMqVo mqVo = new TrackStatMqVo();
mqVo.setBusinessNo(IdUtil.randomUUID());
mqVo.setAlbumId(userListenProcessVo.getAlbumId());
mqVo.setTrackId(userListenProcessVo.getTrackId());
mqVo.setStatType(SystemConstant.TRACK_STAT_PLAY);
mqVo.setCount(1);
kafkaService.sendMessage(KafkaConstant.QUEUE_TRACK_STAT_UPDATE, JSON.toJSONString(mqVo));
}
}
在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.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* @author: atguigu
* @create: 2024-04-16 10:38
*/
@Slf4j
@Component
public class AlbumReceiver {
@Autowired
private TrackInfoService trackInfoService;
/**
* 监听到声音统计消息
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_TRACK_STAT_UPDATE)
public void updateTrackStat(ConsumerRecord<String, String> record) {
String value = record.value();
if (StringUtils.isNotBlank(value)) {
log.info("[专辑服务]监听到更新声音统计信息消息:{}", value);
TrackStatMqVo trackStatMqVo = JSON.parseObject(value, TrackStatMqVo.class);
trackInfoService.updateTrackStat(trackStatMqVo);
}
}
}
在TrackInfoService 中添加接口
/**
* 更新声音统计信息
* @param trackStatMqVo
*/
void updateTrackStat(TrackStatMqVo trackStatMqVo);
在TrackInfoServiceImpl 中添加实现
@Autowired
private RedisTemplate redisTemplate;
/**
* 更新声音统计信息
* 1.避免同一个统计消息被重复消息
* 2.事务管理
*
* @param trackStatMqVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTrackStat(TrackStatMqVo trackStatMqVo) {
//1.避免同一个统计消息被重复消息,利用Redis命令set k v [ex][nx]某次统计消息只有第一次能存入成功
//1.1 创建key
String key = RedisConstant.BUSINESS_PREFIX + "album:" + trackStatMqVo.getBusinessNo();
//1.2 尝试存入redis
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.MINUTES);
if (flag) {
try {
//2.更新声音统计信息
trackInfoMapper.updateStat(trackStatMqVo.getTrackId(), trackStatMqVo.getStatType(), trackStatMqVo.getCount());
//3.更新专辑统计信息 当前方法当声音统计操作发生后才会被调用,专辑中存在跟声音相同统计类型:播放量,评论量
if(SystemConstant.TRACK_STAT_PLAY.equals(trackStatMqVo.getStatType())){
//3.1 同时更新专辑播放量
albumInfoMapper.updateStat(trackStatMqVo.getAlbumId(), SystemConstant.ALBUM_STAT_PLAY, trackStatMqVo.getCount());
}
if(SystemConstant.TRACK_STAT_COMMENT.equals(trackStatMqVo.getStatType())){
//3.2 同时更新专辑评论量
albumInfoMapper.updateStat(trackStatMqVo.getAlbumId(), SystemConstant.ALBUM_STAT_COMMENT, trackStatMqVo.getCount());
}
} catch (Exception e) {
//4.如果更新统计发生异常,确保消费者重试能够再次setnx到Redis成功
redisTemplate.delete(key);
throw new RuntimeException(e);
}
}
}
TrackStatMapper.java 添加方法
/**
* 更新声音统计信息
* @param trackId
* @param statType
* @param count
*/
void updateStat(@Param("trackId") Long trackId, @Param("statType") String statType, @Param("count") Integer count);
映射文件
<update id="updateStat">
update album_stat set stat_num = stat_num + #{count} where album_id = #{albumId} and stat_type = #{statType}
</update>
AlbumStatMapper.java 接口添加
/**
* 更新专辑统计信息
* @param albumId 专辑ID
* @param statType 统计类型
* @param count 数值
*/
void updateStat(@Param("albumId") Long albumId, @Param("statType") String statType, @Param("count") Integer count);
映射文件
<!--更新声音统计信息-->
<update id="updateStat">
UPDATE track_stat
SET stat_num = stat_num + #{count}
WHERE
track_id = ${trackId}
AND stat_type = ${statType}
AND is_deleted = 0
</update>
我们需要根据用户Id 来获取播放记录 ,需要获取到专辑Id 与声音Id 封装到map中然后返回数据即可!
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/83
控制器 UserListenProcessApiController
/**
* 获取当前用户最近播放专辑及声音ID
* @return
*/
@GuiGuLogin
@Operation(summary = "获取当前用户最近播放专辑及声音ID")
@GetMapping("/userListenProcess/getLatelyTrack")
public Result<Map<String, Long>> getLatelyTrack(){
Map<String, Long> map = userListenProcessService.getLatelyTrack();
return Result.ok(map);
}
UserListenProcessService接口:
/**
* 获取当前用户最近播放专辑及声音ID
* @return
*/
Map<String, Long> getLatelyTrack();
UserListenProcessServiceImpl实现类:
/**
* 获取当前用户最近播放专辑及声音ID
*
* @return
*/
@Override
public Map<String, Long> getLatelyTrack() {
//1.获取当前用户ID
Long userId = AuthContextHolder.getUserId();
//2.根据用户ID+更新时间倒序获取最近一条播放进度
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(userId));
query.with(Sort.by(Sort.Direction.DESC, "updateTime"));
query.limit(1);
UserListenProcess listenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
if (listenProcess != null) {
//3.从播放进度中获取专辑ID,声音ID
Map<String, Long> map = new HashMap<>();
map.put("albumId", listenProcess.getAlbumId());
map.put("trackId", listenProcess.getTrackId());
return map;
}
return null;
}
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 控制器中添加
/**
* 根据声音ID查询声音统计信息
* @param trackId
* @return
*/
@Operation(summary = "根据声音ID查询声音统计信息")
@GetMapping("/trackInfo/getTrackStatVo/{trackId}")
public Result<TrackStatVo> getTrackStatVo(@PathVariable Long trackId){
TrackStatVo trackStatVo = trackInfoService.getTrackStatVo(trackId);
return Result.ok(trackStatVo);
}
TrackInfoService接口:
/**
* 根据声音ID查询声音统计信息
* @param trackId
* @return
*/
TrackStatVo getTrackStatVo(Long trackId);
TrackInfoServiceImpl实现类:
/**
* 根据声音ID查询声音统计信息
* @param trackId
* @return
*/
@Override
public TrackStatVo getTrackStatVo(Long trackId) {
return trackInfoMapper.getTrackStatVo(trackId);
}
TrackInfoMapper.java
/**
* 根据声音ID查询声音统计信息
* @param trackId
* @return
*/
TrackStatVo getTrackStatVo(@Param("trackId") Long trackId);
SQL
# 根据声音ID查询指定声音统计信息 playStatNum collectStatNum praiseStatNum commentStatNum
select
track_id,
max(if(stat_type='0701', stat_num, 0)) playStatNum,
max(if(stat_type='0702', stat_num, 0)) collectStatNum,
max(if(stat_type='0703', stat_num, 0)) praiseStatNum,
max(if(stat_type='0704', stat_num, 0)) commentStatNum
from track_stat where track_id = 49162 and is_deleted=0
group by track_id
映射文件:
<select id="getTrackStatVo" resultType="com.atguigu.tingshu.vo.album.TrackStatVo">
select
track_id,
max(if(stat_type='0701', stat_num, 0)) playStatNum,
max(if(stat_type='0702', stat_num, 0)) collectStatNum,
max(if(stat_type='0703', stat_num, 0)) praiseStatNum,
max(if(stat_type='0704', stat_num, 0)) commentStatNum
from track_stat
where track_id = #{trackId} and is_deleted = 0
</select>
手动调用一次更新,查看排行榜。后续会整合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() {
LambdaQueryWrapper<BaseCategory1> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.select(BaseCategory1::getId);
List<BaseCategory1> list = baseCategoryService.list(queryWrapper);
return Result.ok(list);
}
AlbumFeignClient
/**
* 查询所有一级分类列表
*
* @return
*/
@GetMapping("/category/findAllCategory1")
public Result<List<BaseCategory1>> getAllCategory1();
AlbumDegradeFeignClient熔断类:
@Override
public Result<List<BaseCategory1>> getAllCategory1() {
log.error("远程调用专辑服务getAllCategory1服务降级");
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/79
在SearchApiController 中添加控制器
/**
* 更新Redis缓冲中不同分类下不同排序方式TOP10列表
* @return
*/
@Operation(summary = "更新Redis缓冲中不同分类下不同排序方式TOP10列表")
@GetMapping("/albumInfo/updateLatelyAlbumRanking")
public Result updateLatelyAlbumRanking(){
searchService.updateLatelyAlbumRanking();
return Result.ok();
}
SearchService接口:
/**
* 更新Redis缓冲中不同分类下不同排序方式TOP10列表
*/
void updateLatelyAlbumRanking();
SearchServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
/**
* 更新Redis缓冲中不同分类下不同排序方式TOP10列表 作业:改为多线程版本
*/
@Override
public void updateLatelyAlbumRanking() {
try {
//1.远程调用专辑服务获取所有1级分类ID
List<BaseCategory1> baseCategory1List = albumFeignClient.getAllCategory1().getData();
if (CollectionUtil.isNotEmpty(baseCategory1List)) {
List<Long> baseCategory1IdList = baseCategory1List.
stream()
.map(BaseCategory1::getId)
.collect(Collectors.toList());
//2.遍历1级分类列表
String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"};
for (Long baseCategoryId : baseCategory1IdList) {
//3.遍历不同排序方式:每个分类下五种排序字段TOP10列表,放入Redis
for (String dimension : rankingDimensionArray) {
//3.1 查询指定1级下某个排序方式TOP10列表
SearchResponse<AlbumInfoIndex> searchResponse = elasticsearchClient
.search(
s -> s.index(INDEX_NAME)
.query(q -> q.term(t -> t.field("category1Id").value(baseCategoryId)))
.size(10)
.sort(sort -> sort.field(f -> f.field(dimension).order(SortOrder.Desc)))
.source(source -> source.filter(f -> f.excludes(Arrays.asList("isFinished", "category1Id", "category2Id", "category3Id", "hotScore", "attributeValueIndexList.attributeId", "attributeValueIndexList.valueId")))),
AlbumInfoIndex.class
);
//3.2 解析ES响应结果
List<Hit<AlbumInfoIndex>> hitList = searchResponse.hits().hits();
if (CollectionUtil.isNotEmpty(hitList)) {
List<AlbumInfoIndex> top10List = hitList.stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
//3.3 将查询到列表数据放入到Redis中Hash
//3.3.1 构建排行榜Hash的KEY
String key = RedisConstant.RANKING_KEY_PREFIX + baseCategoryId;
//3.3.2 构建排行榜Hash的field(hash key)
String field = dimension;
//3.3.3 将TOP10列表放入Hash中Value
redisTemplate.opsForHash().put(key, field, top10List);
}
}
}
}
} catch (IOException e) {
log.error("[搜索服务]查询ES更新Redis中TOP10排行榜异常:", e);
throw new RuntimeException(e);
}
}
点击排行榜的时候,能看到获取排行榜的地址
排行榜:key=ranking:category1Id field = hotScore 或 playStatNum 或 subscribeStatNum 或 buyStatNum 或albumCommentStatNum value=List
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/81
SearchApiController 控制器中添加
/**
* 获取某个下某种排序方式TOP10排行榜
* @param category1Id
* @param dimension
* @return
*/
@Operation(summary = "获取某个下某种排序方式TOP10排行榜")
@GetMapping("/albumInfo/findRankingList/{category1Id}/{dimension}")
public Result<List<AlbumInfoIndex>> getRankingList(@PathVariable Long category1Id, @PathVariable String dimension){
List<AlbumInfoIndex> list = searchService.getRankingList(category1Id, dimension);
return Result.ok(list);
}
SearchService接口:
/**
* 获取某个下某种排序方式TOP10排行榜
* @param category1Id
* @param dimension
* @return
*/
List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension);
SearchServiceImpl实现类:
/**
* 获取某个下某种排序方式TOP10排行榜
*
* @param category1Id
* @param dimension
* @return
*/
@Override
public List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension) {
//3.3.1 构建排行榜Hash的KEY
String key = RedisConstant.RANKING_KEY_PREFIX + category1Id;
//3.3.2 构建排行榜Hash的field(hash key)
String field = dimension;
return (List<AlbumInfoIndex>) redisTemplate.opsForHash().get(key, field);
}