谷粒随享
学习目标:
专辑详情页面渲染需要以下四项数据:
因此接下来,我们需要在专辑微服务、用户微服务中补充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 albumStatVo = albumInfoService.getAlbumStatVo(albumId);
return Result.ok(albumStatVo);
}
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
album_id,
max(if(stat_type='0401', stat_num, 0)) playStatNum,
max(if(stat_type='0402', stat_num, 0)) subscribeStatNum,
max(if(stat_type='0403', stat_num, 0)) buyStatNum,
max(if(stat_type='0404', stat_num, 0)) commentStatNum
from album_stat
where album_id = #{albumId} and is_deleted = 0
</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;
/**
* 根据专辑ID汇总详情页所需参数
*
* @param albumId
* @return
*/
@Operation(summary = "根据专辑ID汇总详情页所需参数")
@GetMapping("/albumInfo/{albumId}")
public Result<Map<String, Object>> getItem(@PathVariable Long albumId) {
Map<String, Object> map = itemService.getItem(albumId);
return Result.ok(map);
}
}
接口与实现
package com.atguigu.tingshu.search.service;
import java.util.Map;
public interface ItemService {
/**
* 根据专辑ID汇总详情页所需参数
*
* @param albumId
* @return
*/
//ItemVo getItem(Long albumId);
Map<String, Object> getItem(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.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.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class ItemServiceImpl implements ItemService {
@Autowired
private AlbumFeignClient albumFeignClient;
@Autowired
private UserFeignClient userFeignClient;
@Autowired
@Qualifier("threadPoolTaskExecutor")
private Executor executor;
/**
* 根据专辑ID汇总详情页所需参数
*
* @param albumId
* @return
*/
@Override
public Map<String, Object> getItem(Long albumId) {
//0.TODO 采用布隆过滤器解决缓存穿透问题
//1.创建Map对象用于封装详情页数据
Map<String, Object> map = new ConcurrentHashMap<>();
//2.远程调用专辑服务:根据专辑ID查询专辑信息
CompletableFuture<AlbumInfo> albumInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
log.info("获取专辑信息异步任务:{}", albumId);
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
Assert.notNull(albumInfo, "专辑{}不存在", albumId);
map.put("albumInfo", albumInfo);
return albumInfo;
}, executor);
//3.远程调用专辑服务:根据专辑所属分类ID查询分类信息
CompletableFuture<Void> baseCategoryCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
BaseCategoryView categoryView = albumFeignClient.getCategoryView(albumInfo.getCategory3Id()).getData();
Assert.notNull(categoryView, "分类{}不存在", albumInfo.getCategory3Id());
map.put("baseCategoryView", categoryView);
}, executor);
//4.远程调用用户服务:根据专辑所属主播ID查询主播信息
CompletableFuture<Void> announcerCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData();
Assert.notNull(userInfoVo, "用户{}不存在", albumInfo.getUserId());
map.put("announcer", userInfoVo);
}, executor);
//5.远程调用专辑服务:根据专辑ID查询专辑统计信息
CompletableFuture<Void> albumStatVoCompletableFuture = CompletableFuture.runAsync(() -> {
log.info("获取专辑统计异步任务:{}", albumId);
AlbumStatVo albumStatVo = albumFeignClient.getAlbumStatVo(albumId).getData();
Assert.notNull(albumStatVo, "专辑统计{}不存在", albumId);
map.put("albumStatVo", albumStatVo);
}, executor);
//6.组合异步任务
CompletableFuture.allOf(
albumInfoCompletableFuture,
baseCategoryCompletableFuture,
announcerCompletableFuture,
albumStatVoCompletableFuture
).orTimeout(1, TimeUnit.SECONDS)
.join();
//7.响应Map对象
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
* @param albumId
* @param needCheckPayStatusTrackIdList 待检查购买状态声音ID列表
* @return
*/
@Operation(summary = "提交需要检查购买状态声音ID列表,响应每个声音购买状态")
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(
@PathVariable Long userId,
@PathVariable Long albumId,
@RequestBody List<Long> needCheckPayStatusTrackIdList
) {
Map<Long, Integer> map = userInfoService.userIsPaidTrack(userId, albumId, needCheckPayStatusTrackIdList);
return Result.ok(map);
}
UserInfoService接口:
/**
* 提交需要检查购买状态声音ID列表,响应每个声音购买状态
*
* @param userId
* @param albumId
* @param needCheckPayStatusTrackIdList
* @return
*/
Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needCheckPayStatusTrackIdList);
UserInfoServiceImpl实现类:
/**
* 提交需要检查购买状态声音ID列表,响应每个声音购买状态
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param needCheckPayStatusTrackIdList 待检查购买状态声音ID列表
* @return
*/
@Override
public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needCheckPayStatusTrackIdList) {
Map<Long, Integer> map = new HashMap<>();
//1.根据用户ID+专辑ID查询专辑购买记录
Long count = userPaidAlbumMapper.selectCount(
new LambdaQueryWrapper<UserPaidAlbum>()
.eq(UserPaidAlbum::getUserId, userId)
.eq(UserPaidAlbum::getAlbumId, albumId)
);
//2. 如果已购买专辑,将所有待检查购买状态声音 购买状态设置为 1 响应
if (count > 0) {
for (Long trackId : needCheckPayStatusTrackIdList) {
map.put(trackId, 1);
}
return map;
}
//3. 根据用户ID+专辑ID查询已购声音记录
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(
new LambdaQueryWrapper<UserPaidTrack>()
.eq(UserPaidTrack::getUserId, userId)
.eq(UserPaidTrack::getAlbumId, albumId)
.select(UserPaidTrack::getTrackId)
);
//4. 如果不存再已购声音,将所有待检查购买状态声音 购买状态设置为 0 响应
if (CollUtil.isEmpty(userPaidTrackList)) {
for (Long trackId : needCheckPayStatusTrackIdList) {
map.put(trackId, 0);
}
return map;
}
//5.如果存在已购声音,将提交检查声音ID列表中,已购声购买状态设置为:1。未购买设置为0
List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
for (Long trackId : needCheckPayStatusTrackIdList) {
if (userPaidTrackIdList.contains(trackId)) {
map.put(trackId, 1);
} else {
map.put(trackId, 0);
}
}
return map;
}
service-user-client
模块中UserFeignClient 远程调用接口中添加:
/**
* 提交需要检查购买状态声音ID列表,响应每个声音购买状态
* @param userId
* @param albumId
* @param needCheckPayStatusTrackIdList 待检查购买状态声音ID列表
* @return
*/
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(
@PathVariable Long userId,
@PathVariable Long albumId,
@RequestBody List<Long> needCheckPayStatusTrackIdList
);
UserDegradeFeignClient熔断类:
@Override
public Result<Map<Long, Integer>> userIsPaidTrack(Long userId, Long albumId, List<Long> needCheckPayStatusTrackIdList) {
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<IPage<AlbumTrackListVo>> findAlbumTrackPage(
@PathVariable Long albumId,
@PathVariable Long page,
@PathVariable Long limit) {
//1.尝试获取用户ID
Long userId = AuthContextHolder.getUserId();
//2.构建分页对象:封装当前页码、每页记录数
IPage<AlbumTrackListVo> pageInfo = new Page<>(page, limit);
//3.调用业务逻辑层->持久层:封装:总记录数,总页数,当前页数据
pageInfo = trackInfoService.findAlbumTrackPage(pageInfo, albumId, userId);
//4.响应分页对象
return Result.ok(pageInfo);
}
TrackInfoService接口:
/**
* 需求:用户未登录,可以给用户展示声音列表;用户已登录,可以给用户展示声音列表,并动态渲染付费标识
* 分页查询专辑下声音列表(动态渲染付费标识)
*
* @param pageInfo MP分页对象
* @param albumId 专辑ID
* @param userId 用户ID
* @return
*/
IPage<AlbumTrackListVo> findAlbumTrackPage(IPage<AlbumTrackListVo> pageInfo, Long albumId, Long userId);
TrackInfoServiceImpl实现类:
付费类型: 0101-免费 0102-vip付费 0103-付费
获取到声音Id列表集合 与 用户购买声音Id集合进行比较 将用户购买的声音存储到map中,key=trackId value = 1或0; 1:表示购买过,0:表示没有购买过
如果声音列表不包含,则将显示为付费,否则判断用户是否购买过声音,没有购买过设置为付费
@Autowired
private UserFeignClient userFeignClient;
/**
* 需求:用户未登录,可以给用户展示声音列表;用户已登录,可以给用户展示声音列表,并动态渲染付费标识
* 分页查询专辑下声音列表(动态渲染付费标识)
*
* @param pageInfo MP分页对象
* @param albumId 专辑ID
* @param userId 用户ID
* @return
*/
@Override
public IPage<AlbumTrackListVo> findAlbumTrackPage(IPage<AlbumTrackListVo> pageInfo, Long albumId, Long userId) {
//1.分页获取声音列表(包含统计数值) isShowPaidMark默认为false
pageInfo = trackInfoMapper.findAlbumTrackPage(pageInfo, albumId);
//动态渲染付费标识
//2.根据专辑ID查询专辑信息,得到专辑付费类型以及免费试听的集数
AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
Assert.notNull(albumInfo, "专辑{}不存在", albumId);
//2.1 付费类型:0101-免费、0102-vip免费、0103-付费
String payType = albumInfo.getPayType();
//2.2 免费实体集数
Integer tracksForFree = albumInfo.getTracksForFree();
//3.如果用户未登录 情况一:且专辑付费类型是:VIP免费或付费,将除试听以外其他声音付费标识设置true
if (userId == null) {
if (ALBUM_PAY_TYPE_VIPFREE.equals(payType) || ALBUM_PAY_TYPE_REQUIRE.equals(payType)) {
pageInfo.getRecords()
.stream()
.filter(t -> t.getOrderNum() > tracksForFree)
.forEach(t -> t.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 情况二:如果专辑 付费类型:VIP免费 且 当前用户是普通用户,满足进一步查询当前页声音购买状态
Boolean isNeedCheckPayStatus = false;
if (!isVIP && ALBUM_PAY_TYPE_VIPFREE.equals(payType)) {
isNeedCheckPayStatus = true;
}
//4.3 情况三:如果专辑 付费类型:付费,无论什么用户,满足进一步查询当前页声音购买状态
if (ALBUM_PAY_TYPE_REQUIRE.equals(payType)) {
isNeedCheckPayStatus = true;
}
//4.4 如果满足情况二或情况三,远程调用用户服务,得到当前页除试听部分每个声音购买状态
if (isNeedCheckPayStatus) {
//4.4.1 获取当前页中除试听以外其他声音ID列表
List<Long> needCheckPayStatusTrackIdList = pageInfo
.getRecords()
.stream()
.filter(t -> t.getOrderNum() > tracksForFree)
.map(AlbumTrackListVo::getTrackId)
.collect(Collectors.toList());
//4.4.2 远程调用用户服务,获取当前页中声音购买状态Map<Long-专辑ID,Integer-购买状态>
Map<Long, Integer> payStatusMap =
userFeignClient.userIsPaidTrack(userId, albumId, needCheckPayStatusTrackIdList).getData();
//4.5 根据响应声音购买状态,动态修改付费标识。购买状态为0的声音付费标识isShowPaidMark全部设置:true
pageInfo
.getRecords()
.stream()
.filter(t -> t.getOrderNum() > tracksForFree)
.forEach(t -> t.setIsShowPaidMark(payStatusMap.get(t.getTrackId()) == 0));
}
}
return pageInfo;
}
TrackInfoMapper接口:条件必须是当前已经开放并且是审核通过状态的数据,并且还需要获取到声音的播放量以及评论数量
/**
* 分页获取声音列表(包含统计数值)
* @param pageInfo 分页对象,框架自动SQL后面拼接limit部分
* @param albumId
* @return
*/
IPage<AlbumTrackListVo> findAlbumTrackPage(IPage<AlbumTrackListVo> pageInfo, @Param("albumId") Long albumId);
TrackInfoMapper.xml 映射文件
动态SQL
#需求:分页查询指定专辑下包含声音列表(展示声音统计信息) 按照声音序号升序排列
select
ti.id trackId,
ti.track_title,
ti.media_duration,
ti.order_num,
ti.create_time,
max(if(stat_type='0701', stat_num, 0)) playStatNum,
max(if(stat_type='0704', stat_num, 0)) commentStatNum
from track_info ti inner join track_stat stat
on stat.track_id = ti.id
where ti.album_id = 1 and ti.status = '0501' and ti.is_deleted = 0
group by ti.id
order by ti.order_num asc
limit 0,10;
<!--分页获取声音列表(包含统计数值)-->
<select id="findAlbumTrackPage" resultType="com.atguigu.tingshu.vo.album.AlbumTrackListVo">
select
ti.id as track_id,
ti.track_title,
ti.media_duration,
ti.order_num,
ti.create_time,
max(if(stat.stat_type='0701', stat.stat_num, 0)) playStatNum,
max(if(stat.stat_type='0702', stat.stat_num, 0)) collectStatNum,
max(if(stat.stat_type='0703', stat.stat_num, 0)) praiseStatNum,
max(if(stat.stat_type='0704', stat.stat_num, 0)) commentStatNum
from track_info ti inner join track_stat stat on stat.track_id = ti.id and stat.is_deleted = 0
where album_id = #{albumId} and ti.status = '0501' and ti.is_deleted = 0
group by ti.id
order by ti.order_num asc
</select>
测试:
情况一:未登录情况,专辑付费类型:VIP免费 付费 查看声音列表->试听声音免费+其余都需要展示付费标识
情况二:登录情况
详情见:第5章 MongoDB入门.md
播放进度对应的实体类 UserListenProcess
在播放声音的时候,会有触发一个获取播放进度的控制器!因为页面每隔10s会自动触发一次保存功能,会将数据写入MongoDB中。所以我们直接从MongoDB中获取到上一次声音的播放时间即可!
YAPI接口:http://192.168.200.6:3000/project/11/interface/api/71
在 service-user
微服务的 UserListenProcessApiController 控制器中添加
/**
* 查询当前用户指定声音播放进度
*
* @param trackId 声音ID
* @return 前端必须返回具体数值,返回null导致前端无法触发更新播放进度定时任务
*/
@GuiGuLogin(required = false)
@GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}")
public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) {
//1.获取当前用户ID
Long userId = AuthContextHolder.getUserId();
if (userId != null) {
//2.根据用户ID+声音ID查询播放进度
BigDecimal bigDecimal = userListenProcessService.getTrackBreakSecond(userId, trackId);
return Result.ok(bigDecimal);
}
return Result.ok(new BigDecimal("0.00"));
}
UserListenProcessService接口:
/**
* 根据用户ID及声音ID查询播放进度
* @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;
/**
* 根据用户ID及声音ID查询播放进度
*
* @param userId
* @param trackId
* @return
*/
@Override
public BigDecimal getTrackBreakSecond(Long userId, Long trackId) {
//1.创建查询对象:封装查询条件
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(userId).and("trackId").is(trackId));
//2.执行查询:注意每个用户都有自己播放进度集合
String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
UserListenProcess listenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);
if (listenProcess != null) {
return listenProcess.getBreakSecond();
}
return new BigDecimal("0.00");
}
}
页面每隔10秒左右更新播放进度.
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/73
在 UserListenProcessApiController 控制器中添加
/**
* 保存更新声音播放进度
* @param userListenProcessVo
* @return
*/
@Operation(summary = "保存更新声音播放进度")
@GuiGuLogin(required = false)
@PostMapping("/userListenProcess/updateListenProcess")
public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo) {
//1.获取当前用户ID
Long userId = AuthContextHolder.getUserId();
if (userId != null) {
//2.新增或修改播放进度
userListenProcessService.updateListenProcess(userId, userListenProcessVo);
}
return Result.ok();
}
UserListenProcessService接口:
/**
* 保存更新声音播放进度
* @param userListenProcessVo
* @return
*/
void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo);
UserListenProcessServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RabbitService rabbitService;
/**
* 保存更新声音播放进度
*
* @param userListenProcessVo
* @return
*/
@Override
public void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo) {
//1.查询当前用户某个声音播放进度
//1.1 构建查询条件
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(userId).and("trackId").is(userListenProcessVo.getTrackId()));
//1.2 执行查询:注意每个用户都有自己播放进度集合
String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
UserListenProcess listenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);
//2.如果不存在构建声音播放进度保存到MongoDB中
BigDecimal breakSecond = userListenProcessVo.getBreakSecond().setScale(0, RoundingMode.HALF_UP);
if (listenProcess == null) {
listenProcess = new UserListenProcess();
listenProcess.setUserId(userId);
listenProcess.setTrackId(userListenProcessVo.getTrackId());
listenProcess.setAlbumId(userListenProcessVo.getAlbumId());
listenProcess.setBreakSecond(breakSecond);
listenProcess.setCreateTime(new Date());
listenProcess.setUpdateTime(new Date());
} else {
//3.如果已存在播放进度,更新播放进度时间及更新时间 包含文档注解ID
listenProcess.setBreakSecond(breakSecond);
listenProcess.setUpdateTime(new Date());
}
mongoTemplate.save(listenProcess, collectionName);
//4.采用MQ异步更新数据库及索引库中统计信息(播放量)
//4.1 确保某个用户当日内只能更新一次播放统计量-生产者幂等性(由于播放进度更新会定时调用)
//4.1.1 构建生产者幂等性Key 形式:前缀:userId_albumId_trackId
String key = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + userId + "_" + userListenProcessVo.getAlbumId() + "_" + userListenProcessVo.getTrackId();
//4.1.2 计算过期时间 要求:当前日内一次统计有效
long ttl = DateUtil.endOfDay(new Date()).getTime() - System.currentTimeMillis();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "", ttl, TimeUnit.MILLISECONDS);
//4.2 发送更新声音统计VO消息到RabbitMQ
if (flag) {
//4.2.1 创建更新统“计消息VO对象
TrackStatMqVo mqVo = new TrackStatMqVo();
mqVo.setAlbumId(userListenProcessVo.getAlbumId());
mqVo.setTrackId(userListenProcessVo.getTrackId());
mqVo.setStatType(SystemConstant.TRACK_STAT_PLAY);
mqVo.setCount(1);
mqVo.setBusinessNo(IdUtil.randomUUID());
//4.2.2 发送MQ消息
rabbitService.sendMessage(MqConst.EXCHANGE_TRACK, MqConst.ROUTING_TRACK_STAT_UPDATE, mqVo);
}
}
注意:要修改TrackStatMqVo实现序列化接口,否则会导致发送MQ消息失败
在service-album
微服务中添加监听消息:
package com.atguigu.tingshu.album;
import com.atguigu.tingshu.album.service.TrackInfoService;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.rabbitmq.client.Channel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* @author: atguigu
* @create: 2024-08-14 14:23
*/
@Slf4j
@Component
public class AlbumReceiver {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private TrackInfoService trackInfoService;
/**
* 监听到更新声音统计消息,更新MySQL中统计数值
*
* @param mqVo
* @param message
* @param channel
*/
@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(TrackStatMqVo mqVo, Message message, Channel channel) {
String key = "";
try {
if (mqVo != null) {
log.info("监听到更新声音统计消息:{}", mqVo);
//1. 处理消费者幂等性问题(一个消息被多次消费带来数据不一致问题)
//1.1 基于MQ消息对象中唯一标识作为Redis中Key 采用set k v ex nx命令写入Redis
key = RedisConstant.BUSINESS_PREFIX + mqVo.getBusinessNo();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.MINUTES);
if (flag) {
//2. 更新统计数值 业务处理可能抛出异常
trackInfoService.updateStat(mqVo);
}
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("[专辑服务]监听更新声音统计消息异常:{}", e);
//3.捕获到业务处理异常后,将消息再次进行入队,RabbitMQ再次投递消息
redisTemplate.delete(key);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
在TrackInfoService 中添加接口
/**
* 更新声音统计数值
* @param mqVo
*/
void updateStat(TrackStatMqVo mqVo);
在TrackInfoServiceImpl 中添加实现
@Autowired
private AlbumStatMapper albumStatMapper;
/**
* 更新声音统计数值
* 注意:如果声音被播放,被评论 所属专辑也需要更新统计数值
*
* @param mqVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStat(TrackStatMqVo mqVo) {
//1.更新声音统计数值
LambdaUpdateWrapper<TrackStat> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(TrackStat::getTrackId, mqVo.getTrackId());
updateWrapper.eq(TrackStat::getStatType, mqVo.getStatType());
updateWrapper.setSql("stat_num=stat_num+" + mqVo.getCount());
trackStatMapper.update(null, updateWrapper);
//2.如果统计类型是:声音播放 所属专辑统计信息也一并修改
if (SystemConstant.TRACK_STAT_PLAY.equals(mqVo.getStatType())) {
LambdaUpdateWrapper<AlbumStat> albumStatLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
albumStatLambdaUpdateWrapper.eq(AlbumStat::getAlbumId, mqVo.getAlbumId());
albumStatLambdaUpdateWrapper.eq(AlbumStat::getStatType, SystemConstant.ALBUM_STAT_PLAY);
albumStatLambdaUpdateWrapper.setSql("stat_num = stat_num+" + mqVo.getCount());
albumStatMapper.update(null, albumStatLambdaUpdateWrapper);
}
//3.如果统计类型是:声音评论 所属专辑统计信息也一并修改
if (SystemConstant.TRACK_STAT_COMMENT.equals(mqVo.getStatType())) {
LambdaUpdateWrapper<AlbumStat> albumStatLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
albumStatLambdaUpdateWrapper.eq(AlbumStat::getAlbumId, mqVo.getAlbumId());
albumStatLambdaUpdateWrapper.eq(AlbumStat::getStatType, SystemConstant.ALBUM_STAT_COMMENT);
albumStatLambdaUpdateWrapper.setSql("stat_num = stat_num+" + mqVo.getCount());
albumStatMapper.update(null, albumStatLambdaUpdateWrapper);
}
}
我们需要根据用户Id 来获取播放记录 ,需要获取到专辑Id 与声音Id 封装到map中然后返回数据即可!
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/83
控制器 UserListenProcessApiController
/**
* 获取当前用户最近播放声音
* @return {albumId:1,trackId:12}
*/
@GuiGuLogin
@GetMapping("/userListenProcess/getLatelyTrack")
public Result<Map<String, Long>> getLatelyTrack(){
Long userId = AuthContextHolder.getUserId();
Map<String, Long> map = userListenProcessService.getLatelyTrack(userId);
return Result.ok(map);
}
UserListenProcessService接口:
/**
* 获取当前用户最近播放声音
* @return {albumId:1,trackId:12}
*/
Map<String, Long> getLatelyTrack(Long userId);
UserListenProcessServiceImpl实现类:
/**
* 获取当前用户最近播放声音
*
* @return {albumId:1,trackId:12}
*/
@Override
public Map<String, Long> getLatelyTrack(Long userId) {
//1.查询当前用户某个声音播放进度
//1.1 构建查询条件
Query query = new Query();
query.addCriteria(Criteria.where("userId").is(userId));
query.with(PageRequest.of(0, 1, Sort.Direction.DESC, "updateTime"));
//1.2 执行查询:注意每个用户都有自己播放进度集合
String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
UserListenProcess listenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);
if (listenProcess != null) {
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
统计声音需要更新的数据如下,我们将数据封装到一个实体类TrackStatVo中便于操作
在TrackInfoApiController 控制器中添加
/**
* 查询声音统计信息
* @param trackId
* @return
*/
@Operation(summary = "查询声音统计信息")
@GetMapping("/trackInfo/getTrackStatVo/{trackId}")
public Result<TrackStatVo> getTrackStatVo(@PathVariable Long trackId){
TrackStatVo trackStatVo = trackInfoService.getTrackStatVo(trackId);
return Result.ok(trackStatVo);
}
TrackInfoService接口:
/**
* 查询声音统计信息
* @param trackId
* @return
*/
TrackStatVo getTrackStatVo(Long trackId);
TrackInfoServiceImpl实现类:
/**
* 查询声音统计信息
* @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);
映射文件:
<!--根据声音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 中添加控制器
/**
* 更新不同分类不同排行TOP20数据
*
* @return
*/
@Operation(summary = "更新不同分类不同排行TOP20数据")
@GetMapping("/albumInfo/updateLatelyAlbumRanking")
public Result updateLatelyAlbumRanking() {
searchService.updateLatelyAlbumRanking();
return Result.ok();
}
SearchService接口:
/**
* 更新不同分类不同排行TOP20数据
* @return
*/
void updateLatelyAlbumRanking();
SearchServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
/**
* 更新不同分类不同排行TOP20数据
*
* @return
*/
@Override
public void updateLatelyAlbumRanking() {
try {
//1.远程调用专辑服务获取所有1级分类获取一级分类ID列表
List<BaseCategory1> baseCategory1List = albumFeignClient.getAllCategory1().getData();
if (CollectionUtil.isNotEmpty(baseCategory1List)) {
//2.遍历所有一级分类ID-根据分类ID+排序方式+限制返回文档数量
List<Long> category1IdList = baseCategory1List.stream().map(BaseCategory1::getId).collect(Collectors.toList());
for (Long category1Id : category1IdList) {
//声明当前分类hash结构Key
String key = RedisConstant.RANKING_KEY_PREFIX + category1Id;
//处理某个一级分类排行数据,遍历5种不同排序字段
String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"};
for (String rankingDimension : rankingDimensionArray) {
SearchResponse<AlbumInfoIndex> searchResponse = elasticsearchClient.search(
s -> s.index(INDE_NAME)
.query(q -> q.term(t -> t.field("category1Id").value(category1Id)))
.sort(sort -> sort.field(f -> f.field(rankingDimension).order(SortOrder.Desc)))
.size(20)
.source(s1 -> s1.filter(f -> f.includes("id", "albumTitle", "albumIntro", "coverUrl", "includeTrackCount", "playStatNum", "createTime", "payType"))),
AlbumInfoIndex.class
);
//3.解析当前分类下当前排序方式命中TOP20专辑文档列表
List<Hit<AlbumInfoIndex>> hitList = searchResponse.hits().hits();
if (CollectionUtil.isNotEmpty(hitList)) {
//3.1 遍历命中记录对象得到Hit对象中中_source(专辑对象)
List<AlbumInfoIndexVo> top20List = hitList.stream()
.map(hit -> {
AlbumInfoIndex albumInfoIndex = hit.source();
//3.2 处理高亮
Map<String, List<String>> highlightMap = hit.highlight();
if (CollectionUtil.isNotEmpty(highlightMap)) {
String highlightText = highlightMap.get("albumTitle").get(0);
albumInfoIndex.setAlbumTitle(highlightText);
}
return BeanUtil.copyProperties(albumInfoIndex, AlbumInfoIndexVo.class);
}).collect(Collectors.toList());
//4.将不同分类不同维度的TOP20数据放入Redis
redisTemplate.opsForHash().put(key, rankingDimension, top20List);
}
}
}
}
} catch (Exception e) {
log.error("[搜索服务]更新排行榜Redis异常:{}", 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 控制器中添加
/**
* 获取不同分类下不同排行维度TOP20
* @param category1Id
* @param dimension
* @return
*/
@Operation(summary = "获取不同分类下不同排行维度TOP20")
@GetMapping("/albumInfo/findRankingList/{category1Id}/{dimension}")
public Result<List<AlbumInfoIndexVo>> getRankingList(@PathVariable Long category1Id, @PathVariable String dimension) {
List<AlbumInfoIndexVo> list = searchService.getRankingList(category1Id, dimension);
return Result.ok(list);
}
SearchService接口:
/**
* 获取不同分类下不同排行维度TOP20
* @param category1Id
* @param dimension
* @return
*/
List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension);
SearchServiceImpl实现类:
/**
* 获取不同分类下不同排行维度TOP20
*
* @param category1Id
* @param dimension
* @return
*/
@Override
public List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension) {
//方式一:根据key+filed获取hash的VALUE
String key = RedisConstant.RANKING_KEY_PREFIX + category1Id;
//List<AlbumInfoIndexVo> list = (List<AlbumInfoIndexVo>) redisTemplate.opsForHash().get(key, dimension);
//方式二:创建操作hash对象
BoundHashOperations<String, String, List<AlbumInfoIndexVo>> boundHashOperations = redisTemplate.boundHashOps(key);
List<AlbumInfoIndexVo> list = boundHashOperations.get(dimension);
return list;
}