谷粒随享
学习目标:
专辑详情页面渲染需要以下四项数据:
因此接下来,我们需要在专辑微服务、用户微服务中补充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 statVo = albumInfoService.getAlbumStatVo(albumId);
return Result.ok(statVo);
}
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
package com.atguigu.tingshu.album.mapper;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.query.album.AlbumInfoQuery;
import com.atguigu.tingshu.vo.album.AlbumListVo;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface AlbumInfoMapper extends BaseMapper<AlbumInfo> {
AlbumStatVo getAlbumStatVo(@Param("albumId") Long albumId);
}
albumInfoMapper.xml
<!--根据专辑Id 获取到统计数据-->
<select id="getAlbumStatVo" resultType="com.atguigu.tingshu.vo.album.AlbumStatVo">
select
stat.album_id albumId,
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 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("[专辑模块Feign调用]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> getItem(@PathVariable Long albumId) {
Map<String, Object> mapResult = itemService.getItem(albumId);
return Result.ok(mapResult);
}
}
接口与实现
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 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.ThreadPoolExecutor;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class ItemServiceImpl implements ItemService {
@Autowired
private AlbumFeignClient albumFeignClient;
@Autowired
private UserFeignClient userFeignClient;
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
/**
* 根据专辑ID查询专辑详情信息
* 汇总专辑详情页面渲染所有四项数据
* - albumInfo:当前专辑信息(已完成)
* - albumStatVo:专辑统计信息(已完成)
* - baseCategoryView:专辑分类信息(已完成)
* - announcer:专辑主播信息
*
* @param albumId
* @return
*/
@Override
public Map<String, Object> getItem(Long albumId) {
//多线程环境下进行并发读写hashMap线程不安全替换为ConcurrentHashMap
Map<String, Object> mapResult = new ConcurrentHashMap<>();
//1.远程调用专辑服务获取专辑基本信息
CompletableFuture<AlbumInfo> albumInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
Assert.notNull(albumInfo, "专辑信息为空");
mapResult.put("albumInfo", albumInfo);
return albumInfo;
}, threadPoolExecutor);
//2.远程调用专辑服务获取专辑统计信息
CompletableFuture<Void> statCompletableFuture = CompletableFuture.runAsync(() -> {
AlbumStatVo albumStatVo = albumFeignClient.getAlbumStatVo(albumId).getData();
Assert.notNull(albumStatVo, "专辑统计信息为空");
mapResult.put("albumStatVo", albumStatVo);
}, threadPoolExecutor);
//3.获取到专辑后,得到专辑对应三级分类ID,获取分类信息
CompletableFuture<Void> baseCategoryViewCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
BaseCategoryView baseCategoryView = albumFeignClient.getCategoryViewBy3Id(albumInfo.getCategory3Id()).getData();
Assert.notNull(baseCategoryView, "分类信息为空");
mapResult.put("baseCategoryView", baseCategoryView);
}, threadPoolExecutor);
//4.获取到专辑后,得到专辑对应主播ID,获取主播信息
CompletableFuture<Void> userInfoCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
UserInfoVo userInfoVo = userFeignClient.getUserInfoVoByUserId(albumInfo.getUserId()).getData();
Assert.notNull(userInfoVo, "主播信息为空");
mapResult.put("announcer", userInfoVo);
}, threadPoolExecutor);
CompletableFuture.allOf(
albumInfoCompletableFuture,
baseCategoryViewCompletableFuture,
statCompletableFuture,
userInfoCompletableFuture
).join();
return mapResult;
}
}
需求:根据专辑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 控制器:
/**
* 获取专辑声音列表某页中,用户对于声音付费情况
*
* @param userId
* @param albumId
* @param trackIdList
* @return
*/
@Operation(summary = "获取专辑声音列表某页中,用户对于声音付费情况")
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrackList(@PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> trackIdList) {
Map<Long, Integer> mapResult = userInfoService.userIsPaidTrackList(userId, albumId, trackIdList);
return Result.ok(mapResult);
}
UserInfoService接口:
/**
* 获取专辑声音列表某页中,用户对于声音付费情况
*
* @param userId
* @param albumId
* @param trackIdList
* @return
*/
Map<Long, Integer> userIsPaidTrackList(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> userIsPaidTrackList(Long userId, Long albumId, List<Long> trackIdList) {
//1.根据用户ID+专辑ID查询专辑购买记录 如果有记录,将trackIdList购买情况返回1
LambdaQueryWrapper<UserPaidAlbum> userPaidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>();
userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getUserId, userId);
userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getAlbumId, albumId);
Long count = userPaidAlbumMapper.selectCount(userPaidAlbumLambdaQueryWrapper);
if (count > 0) {
//用户购买该专辑
Map<Long, Integer> map = new HashMap<>();
for (Long trackId : trackIdList) {
//将购买结果:已购买
map.put(trackId, 1);
}
return map;
}
//2.根据用户ID+声音列表查询声音购买集合记录,判断哪些声音被购买,哪些没有被购买
LambdaQueryWrapper<UserPaidTrack> userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>();
userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getUserId, userId);
userPaidTrackLambdaQueryWrapper.in(UserPaidTrack::getTrackId, trackIdList);
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(userPaidTrackLambdaQueryWrapper);
if (CollectionUtil.isEmpty(userPaidTrackList)) {
//2.1 专辑当前页包含声音列表用户一个都没购买
Map<Long, Integer> map = new HashMap<>();
for (Long trackId : trackIdList) {
//将购买结果:未购买
map.put(trackId, 0);
}
return map;
}
//2.2 用户有购买声音 判断哪些是被购买,哪些未被购买
//2.2.1 得到用户已购买声音ID
List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
//2.2.2 遍历待检测声音ID列表 判断已购买声音记录中是否包含声音ID
Map<Long, Integer> map = new HashMap<>();
for (Long trackId : trackIdList) {
if(userPaidTrackIdList.contains(trackId)){
map.put(trackId, 1);
}else{
map.put(trackId, 0);
}
}
return map;
}
service-user-client
模块中UserFeignClient 远程调用接口中添加:
/**
* 获取专辑声音列表某页中,用户对于声音付费情况
*
* @param userId
* @param albumId
* @param trackIdList
* @return
*/
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrackList(@PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> trackIdList);
UserDegradeFeignClient熔断类:
@Component
public class UserDegradeFeignClient implements UserInfoFeignClient {
@Override
public Result<Map<Long, Integer>> userIsPaidTrackList(Long userId, Long albumId, List<Long> trackIdList) {
log.error("远程调用[用户服务]userIsPaidTrackList方法服务降级");
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) //如果登录就会获取到用户ID
@Operation(summary = "查询当前用户查看专辑声音分页列表")
@GetMapping("/trackInfo/findAlbumTrackPage/{albumId}/{page}/{limit}")
public Result<Page<AlbumTrackListVo>> getUserAlbumTrackPage(@PathVariable Long albumId, @PathVariable Integer page, @PathVariable Integer limit) {
//1.获取用户ID(可以有,可能没有)
Long userId = AuthContextHolder.getUserId();
//2.构建MP分页对象
Page<AlbumTrackListVo> pageInfo = new Page<>(page, limit);
pageInfo = trackInfoService.getUserAlbumTrackPage(pageInfo, userId, albumId);
return Result.ok(pageInfo);
}
TrackInfoService接口:
/**
* 根据用户登录情况,查询当前专辑某页中声音列表付费情况
* @param pageInfo
* @param userId
* @param albumId
* @return
*/
Page<AlbumTrackListVo> getUserAlbumTrackPage(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
* @param albumId
* @return
*/
@Override
public Page<AlbumTrackListVo> getUserAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, Long userId, Long albumId) {
//1.根据专辑ID分页查询声音列表-(包含声音统计信息)
pageInfo = trackInfoMapper.getUserAlbumTrackPage(pageInfo, albumId);
List<AlbumTrackListVo> trackList = pageInfo.getRecords();
//TODO 对当前页声音付费标识业务处理 关键:找出声音需要付费情况处理
//2.根据专辑ID查询专辑信息-得到专辑付费类型以及免费试听集数(VIP免费或者付费才有)
AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
//2.1 获取专辑付费类型 0101-免费、0102-vip免费、0103-付费
String payType = albumInfo.getPayType();
//3.用户未登录
if (userId == null) {
// 3.1 专辑付费类型为:0102-vip免费 或者 0103-付费
if (!SystemConstant.ALBUM_PAY_TYPE_FREE.equals(payType)) {
//3.2 将当前页中声音列表获取到,找出非试听声音列表,为非试听声音设置“付费”标识
trackList.stream().filter(trackInfo -> {
//声音序号大于>免费试听集数 就得到需要付费的声音
return trackInfo.getOrderNum() > albumInfo.getTracksForFree();
}).collect(Collectors.toList()).stream().forEach(trackInfo -> {
//除了试听的声音外,其他的声音全部设置为:付费
trackInfo.setIsShowPaidMark(true);
});
}
} else {
//4.用户已登录 如果登录为普通用户或VIP过期了->未购买专辑未购买相关声音,将声音设置为付费标识;付费的专辑->未购买相关专辑或相关声音,将声音设置为付费
//4.1 远程调用用户微服务获取用户信息(VIP状态)
UserInfoVo userInfoVo = userFeignClient.getUserInfoVoByUserId(userId).getData();
//声明变量是否需要购买
boolean isNeedPay = false;
//4.2 付费类型:VIP免费-->普通用户或者VIP过期 进一步查看用户是否购买过专辑或者声音
if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(payType)) {
if (userInfoVo.getIsVip().intValue() == 0) {
//普通用户
isNeedPay = true;
}
if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().before(new Date())) {
//VIP会员 会员过期- 会员到期时间 在 当前时间之前 -- 后期会有延迟任务更新会员过期时间
isNeedPay = true;
}
} else if (SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(payType)) {
//4.3 付费类型:付费-->通用户或者VIP,进一步查看用户是否购买过专辑或者声音
isNeedPay = true;
}
//5.统一处理需要购买情况:如果用户未购买专辑或者声音,将声音付费标识改为:true
if (isNeedPay) {
//5.1 得到当前页中声音列表--需要将免费试听集数过滤掉
List<AlbumTrackListVo> trackListVoList = trackList.stream().filter(trackInfo -> {
//将试听的集数过滤掉
return trackInfo.getOrderNum() > albumInfo.getTracksForFree();
}).collect(Collectors.toList());
//需要进一步验证用户购买情况声音ID
List<Long> trackIdList = trackListVoList.stream().map(AlbumTrackListVo::getTrackId).collect(Collectors.toList());
//5.2 远程调用用户微服务-查询当前页中声音列表购买情况Map<声音ID,购买状态>
Map<Long, Integer> buyStatusMap = userFeignClient.userIsPaidTrackList(userId, albumId, trackIdList).getData();
//5.3 如果当前页中声音未购买 将指定声音付费标识改为:true
for (AlbumTrackListVo albumTrackListVo : trackListVoList) {
//获取声音购买结果
Integer isBuy = buyStatusMap.get(albumTrackListVo.getTrackId());
if (isBuy == 0) {
//说明当前用户未购买该专辑或者该声音
albumTrackListVo.setIsShowPaidMark(true);
}
}
}
}
return pageInfo;
}
TrackInfoMapper接口:条件必须是当前已经开放并且是审核通过状态的数据,并且还需要获取到声音的播放量以及评论数量
@Mapper
public interface TrackInfoMapper extends BaseMapper<TrackInfo> {
/**
* 根据专辑ID分页查询声音列表
* @param pageInfo 分页对象
* @param albumId 专辑ID
* @return
*/
Page<AlbumTrackListVo> getUserAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, @Param("albumId") Long albumId);
}
TrackInfoMapper.xml 映射文件
<!--分页查询声音列表包含统计信息-->
<select id="getUserAlbumTrackPage" resultType="com.atguigu.tingshu.vo.album.AlbumTrackListVo">
select
info.trackId,
info.trackTitle,
info.mediaDuration,
info.orderNum,
info.createTime,
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)) albumCommentStatNum
from (select
ti.id trackId,
ti.track_title trackTitle,
ti.media_duration mediaDuration,
ti.order_num orderNum,
ti.create_time createTime,
stat.stat_type,
stat.stat_num
from track_info ti left join track_stat stat
on stat.track_id = ti.id
where ti.album_id = #{albumId} and ti.is_deleted = 0) info
group by info.trackId
order by info.orderNum 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 //必须登录 true:每试听几秒引导用户登录; false:允许用户一直进行试听
@GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}")
public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) {
Long userId = AuthContextHolder.getUserId();
BigDecimal breakSec = userListenProcessService.getTrackBreakSecond(userId, trackId);
return Result.ok(breakSec);
}
UserListenProcessService接口:
/**
* 查询当前用户对于入参声音上次播放时间
*
* @param trackId
* @return
*/
BigDecimal getTrackBreakSecond(Long userId, Long trackId);
UserListenProcessServiceImpl实现类:
/**
* 查询当前用户对于入参声音上次播放时间
*
* @param trackId
* @return
*/
@Override
public BigDecimal getTrackBreakSecond(Long userId, Long trackId) {
//1.封装查询条件:用户ID+声音ID
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);
//3.执行查询MongoDB中存入用户声音播放进度
UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);
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
*/
@Operation(summary = "新增或更新用户声音播放进度")
@GuiGuLogin
@PostMapping("/userListenProcess/updateListenProcess")
public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo) {
userListenProcessService.updateListenProcess(userListenProcessVo);
return Result.ok();
}
UserListenProcessService接口:
/**
* 新增或更新用户声音播放进度
* @param userListenProcessVo
* @return
*/
void updateListenProcess(UserListenProcessVo userListenProcessVo);
UserListenProcessServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private KafkaService kafkaService;
/**
* 新增或更新用户声音播放进度
*
* @param vo
* @return
*/
@Override
public void updateListenProcess(UserListenProcessVo vo) {
Long userId = AuthContextHolder.getUserId();
//1.根据用户ID+声音ID查询播放进度
Query query = new Query(Criteria.where("userId").is(userId).and("trackId").is(vo.getTrackId()));
String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
UserListenProcess listenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);
if (listenProcess != null) {
//2.如果播放进度存在,直接修改播放进度:秒数
listenProcess.setUpdateTime(new Date());
listenProcess.setBreakSecond(vo.getBreakSecond());
mongoTemplate.save(listenProcess, collectionName);
} else {
//3.如果播放进度不存在,新增播放进度文档到MOngoDB
listenProcess = new UserListenProcess();
BeanUtil.copyProperties(vo, listenProcess);
listenProcess.setUserId(userId);
listenProcess.setIsShow(1);
listenProcess.setCreateTime(new Date());
listenProcess.setUpdateTime(new Date());
mongoTemplate.save(listenProcess, collectionName);
}
//4.TODO 基于Kafka消息队列异步更新声音/专辑统计信息
//4.1 避免用户“恶意”刷播放量,业务要求:一天内同一个用户对同一个声音统计信息只能更新一次
String key = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + userId + ":" + vo.getTrackId();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, userId, 24, TimeUnit.HOURS);
//4.2 封装更新统计信息MQ对象,将发送到“声音统计话题”,将来由专辑微服务(消费者)监听更新声音,专辑统计信息
if (flag) {
TrackStatMqVo mqVo = new TrackStatMqVo();
//为了避免MQ服务器对同一个消息进行多次投递,产生业务标识
mqVo.setBusinessNo(RedisConstant.BUSINESS_PREFIX + IdUtil.fastSimpleUUID());
mqVo.setStatType(SystemConstant.TRACK_STAT_PLAY);
mqVo.setAlbumId(vo.getAlbumId());
mqVo.setTrackId(vo.getTrackId());
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.common.constant.RedisConstant;
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.data.redis.core.RedisTemplate;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author: atguigu
* @create: 2023-10-30 15:59
*/
@Slf4j
@Component
public class AlbumReceiver {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private TrackInfoService trackInfoService;
/**
* 监听更新声音统计话题消息,更新统计信息
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_TRACK_STAT_UPDATE)
public void updateStat(ConsumerRecord<String, String> record) {
String value = record.value();
if (StringUtils.isNotBlank(value)) {
log.info("[专辑服务],监听到更新统计信息消息:{}", value);
//1.将收到的MQ消息转为Java对象
TrackStatMqVo mqVo = JSON.parseObject(value, TrackStatMqVo.class);
//2.先进行幂等性处理,一个消息只能被处理一次(网络抖动,MQ服务器本身对同一消息进行多次投递)
String key = mqVo.getBusinessNo();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, null, 1, TimeUnit.HOURS);
if (flag) {
//说明是第一次处理该业务
trackInfoService.updateStat(mqVo.getAlbumId(), mqVo.getTrackId(), mqVo.getStatType(), mqVo.getCount());
}
}
}
}
在TrackInfoService 中添加接口
/**
* 更新声音统计信息
*
* @param albumId
* @param trackId
* @param statType
* @param count
*/
void updateStat(Long albumId, Long trackId, String statType, Integer count);
在TrackInfoServiceImpl 中添加实现
/**
* 更新声音统计信息
*
* @param albumId 专辑ID
* @param trackId 声音ID
* @param statType 统计类型
* @param count 数量
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStat(Long albumId, Long trackId, String statType, Integer count) {
//1.更新声音统计信息
trackStatMapper.updateStat(trackId, statType, count);
//2.更新专辑统计信息(如果是声音播放量或者声音评论量同步修改专辑统计信息)
if (SystemConstant.TRACK_STAT_PLAY.equals(statType)) {
//TODO 主要要将播放的统计类型 改为 声音的统计类型 “0401”
albumStatMapper.updateStat(albumId, SystemConstant.ALBUM_STAT_PLAY, count);
}
if(SystemConstant.TRACK_STAT_COMMENT.equals(statType)){
//TODO 主要要将播放的统计类型 改为 声音的统计类型 “0404”
albumStatMapper.updateStat(albumId, SystemConstant.ALBUM_STAT_COMMENT, count);
}
}
TrackStatMapper.java 添加方法
package com.atguigu.tingshu.album.mapper;
import com.atguigu.tingshu.model.album.TrackStat;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface TrackStatMapper extends BaseMapper<TrackStat> {
/**
* 更新声音统计信息
*
* @param trackId
* @param statType
* @param count
*/
@Update("update track_stat set stat_num = stat_num + #{count} where track_id = #{trackId} and stat_type = #{statType}")
void updateStat(@Param("trackId") Long trackId, @Param("statType") String statType, @Param("count") Integer count);
}
AlbumStatMapper.java 接口添加
package com.atguigu.tingshu.album.mapper;
import com.atguigu.tingshu.model.album.AlbumStat;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface AlbumStatMapper extends BaseMapper<AlbumStat> {
@Update("update album_stat set stat_num = stat_num + #{count} where album_id = #{albumId} and stat_type = #{statType}")
void updateStat(@Param("albumId") Long albumId, @Param("statType") String statType, @Param("count") Integer count);
}
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){
return Result.ok(trackInfoService.getTrackStatVo(trackId));
}
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
/**
* 获取声音统计信息
* @param trackId
* @return
*/
@Select("select\n" +
" track_id,\n" +
" max(if(stat_type='0701', stat_num, 0)) playStatNum,\n" +
" max(if(stat_type='0702', stat_num, 0)) collectStatNum,\n" +
" max(if(stat_type='0703', stat_num, 0)) praiseStatNum,\n" +
" max(if(stat_type='0704', stat_num, 0)) commentStatNum\n" +
" from track_stat where track_id = #{trackId} and is_deleted=0\n" +
"group by track_id")
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
我们需要根据用户Id 来获取播放记录 ,需要获取到专辑Id 与声音Id 封装到map中然后返回数据即可!
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/83
控制器 UserListenProcessApiController
/**
* 获取当前用户上次播放专辑声音记录
*
* @return
*/
@GuiGuLogin
@GetMapping("/userListenProcess/getLatelyTrack")
public Result<Map<String, Long>> getLatelyTrack() {
Long userId = AuthContextHolder.getUserId();
return Result.ok(userListenProcessService.getLatelyTrack(userId));
}
UserListenProcessService接口:
/**
* 获取用户最近一次播放记录
* @param userId
* @return
*/
Map<String, Long> getLatelyTrack(Long userId);
UserListenProcessServiceImpl实现类:
/**
* 获取用户最近一次播放记录
*
* @param userId
* @return
*/
@Override
public Map<String, Long> getLatelyTrack(Long userId) {
//根据用户ID查询播放进度集合,按照更新时间倒序,获取第一条记录
//1.构建查询条件对象
Query query = new Query();
//1.1 封装用户ID查询条件
query.addCriteria(Criteria.where("userId").is(userId));
//1.2 按照更新时间排序
query.with(Sort.by(Sort.Direction.DESC, "updateTime"));
//1.3 只获取第一条记录
query.limit(1);
//2.执行查询
UserListenProcess listenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
if (listenProcess != null) {
//封装响应结果
Map<String, Long> mapResult = new HashMap<>();
mapResult.put("albumId", listenProcess.getAlbumId());
mapResult.put("trackId", listenProcess.getTrackId());
return mapResult;
}
return null;
}
手动调用一次更新,查看排行榜。后续会整合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() {
return Result.ok(baseCategoryService.list());
}
AlbumFeignClient
/**
* 查询所有一级分类列表
* @return
*/
@GetMapping("/category/findAllCategory1")
public Result<List<BaseCategory1>> getAllCategory1();
AlbumDegradeFeignClient熔断类:
@Override
public Result<List<BaseCategory1>> getAllCategory1() {
log.error("[专辑模块Feign调用]getAllCategory1异常");
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/79
在SearchApiController 中添加控制器
/**
* 改接口仅用于测试,手动更新排行数据;后续改为定时更新(分布式任务调度框架实现)
*
* @return
*/
@Operation(summary = "更新所有分类下排行榜-手动调用")
@GetMapping("/albumInfo/updateLatelyAlbumRanking")
public Result updateLatelyAlbumRanking() {
searchService.updateLatelyAlbumRanking();
return Result.ok();
}
SearchService接口:
/**
* 更新排行数据,从ES中获取不同分类下不同排行列表,存入Redis中hash
*
* @return
*/
void updateLatelyAlbumRanking();
SearchServiceImpl实现类:
@Autowired
private RedisTemplate redisTemplate;
/**
* 更新排行数据,从ES中获取不同分类下不同排行列表,存入Redis中hash
*
* @return
*/
@Override
public void updateLatelyAlbumRanking() {
try {
//1.远程调用专辑服务获取一级分类列表
List<BaseCategory1> baseCategory1List = albumFeignClient.getAllCategory1().getData();
Assert.notNull(baseCategory1List, "1级分类列表为空");
List<Long> category1IdList = baseCategory1List.stream().map(BaseCategory1::getId).collect(Collectors.toList());
//2.循环一级分类列表,根据一级分类ID,以及当前分类下:固定5中排序方式查询ES中专辑列表;将不同排行数据存入Redis
for (Long baseCategory1Id : category1IdList) {
//2.0 声明当前分类热度数据hash结构的key
String rankingKey = RedisConstant.RANKING_KEY_PREFIX + baseCategory1Id;
//2.1 构建排序字段数组(热度、播放量、订阅量、购买量、评论量)
String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"};
//2.2 循环排序字段数组-发起ES检索请求
for (String dimension : rankingDimensionArray) {
SearchResponse<AlbumInfoIndex> searchResponse = elasticsearchClient.search(
s -> s.index(INDEX_NAME)
.query(q -> q.term(t -> t.field("category1Id").value(baseCategory1Id)))
.sort(sort -> sort.field(f -> f.field(dimension).order(SortOrder.Desc)))
.source(source -> source.filter(f -> f.excludes("attributeValueIndexList",
"hotScore",
"category1Id",
"category2Id",
"category3Id")))
, AlbumInfoIndex.class);
//2.3 解析ES命中不同排行数据
List<Hit<AlbumInfoIndex>> hits = searchResponse.hits().hits();
if (CollectionUtil.isNotEmpty(hits)) {
List<AlbumInfoIndex> albumInfoIndexList = hits.stream().map(hit -> hit.source()).collect(Collectors.toList());
//2.4 将数据存入Redis-Hash结构中
redisTemplate.opsForHash().put(rankingKey, dimension, albumInfoIndexList);
}
}
}
} catch (Exception e) {
log.error("[专辑]热门检索异常:{}", 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 控制器中添加
/**
* 获取Redis中不同分类下不同排序方式-专辑排行榜
*
* @param category1Id
* @param dimension
* @return
*/
@Operation(summary = "获取Redis中不同分类下不同排序方式-专辑排行榜")
@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接口:
/**
* 获取Redis中不同分类下不同排序方式-专辑排行榜
*
* @param category1Id
* @param dimension
* @return
*/
List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension);
SearchServiceImpl实现类:
/**
* 获取Redis中不同分类下不同排序方式-专辑排行榜
*
* @param category1Id
* @param dimension
* @return
*/
@Override
public List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension) {
//1.构建分类热门专辑Key
String rankingKey = RedisConstant.RANKING_KEY_PREFIX + category1Id;
//2.获取Hash结构中Val
List<AlbumInfoIndex> list = (List<AlbumInfoIndex>) redisTemplate.opsForHash().get(rankingKey, dimension);
return list;
}