第5章 详情介绍.md 40 KB

谷粒随享

第5章 专辑/声音详情

学习目标:

1、专辑详情

专辑详情页面渲染需要以下四项数据:

  • albumInfo:当前专辑信息
  • albumStatVo:专辑统计信息
  • baseCategoryView:专辑分类信息
  • announcer:专辑主播信息

因此接下来,我们需要在专辑微服务用户微服务中补充RestFul接口实现 并且 提供远程调用Feign API接口给专辑搜索微服务来调用获取。

在专辑检索微服务中编写控制器汇总专辑详情所需数据

以下是详情需要获取到的数据集

  1. 通过专辑Id 获取专辑数据{已存在}
  2. 通过专辑Id 获取专辑统计信息{不存在}
  3. 通过三级分类Id 获取到分类数据{已存在}
  4. 通过用户Id 获取到主播信息{不存在}
  5. 获取专辑声音列表-{不存在}

1.1 服务提供方提供接口

1.1.1 根据专辑Id 获取专辑数据(已完成)

1.1.2 根据三级分类Id获取到分类信息(已完成)

1.1.3 根据用户Id 获取主播信息(已完成)

1.1.4 根据专辑Id 获取统计信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/67

AlbumInfoApiController 控制器

/**
 * 根据专辑Id 获取到统计信息
 * @param albumId
 * @return
 */
@Operation(summary = "获取到专辑统计信息")
@GetMapping("/albumInfo/getAlbumStatVo/{albumId}")
public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId){
    //	获取服务层方法
    AlbumStatVo albumStatVo = albumInfoService.getAlbumStatVoByAlbumId(albumId);
    return Result.ok(albumStatVo);
}

AlbumInfoService接口

/**
 * 根据专辑Id 获取到统计信息
 * @param albumId
 * @return
 */
AlbumStatVo getAlbumStatVoByAlbumId(Long albumId);

AlbumInfoServiceImpl实现类

@Override
public AlbumStatVo getAlbumStatVoByAlbumId(Long albumId) {
  //	调用mapper 层方法
  return albumInfoMapper.getAlbumStatVoByAlbumId(albumId);
}

albumInfoMapper.java

/**
* 根据专辑Id 获取到统计信息
* @param albumId
* @return
*/
AlbumStatVo getAlbumStatVoByAlbumId(@Param("albumId") Long albumId);

albumInfoMapper.xml

<!--根据专辑Id 获取到统计数据-->
<select id="getAlbumStatVoByAlbumId" resultType="com.atguigu.tingshu.vo.album.AlbumStatVo">
    select
           info.album_id,
           max(if(info.stat_type = '0401', info.stat_num, 0)) play_stat_num,
           max(if(info.stat_type = '0402', info.stat_num, 0)) subscribe_stat_num,
           max(if(info.stat_type = '0403', info.stat_num, 0)) buy_stat_num,
           max(if(info.stat_type = '0404', info.stat_num, 0)) comment_stat_num
    from (select stat.album_id,
                 stat.stat_type,
                 stat.stat_num
          from album_stat stat
          where stat.album_id = #{albumId}) info
    group by info.album_id
</select>

service-album-client模块AlbumFeignClient 接口中添加

/**
 * 根据专辑Id 获取到统计信息
 * @param albumId
 * @return
 */
@GetMapping("/albumInfo/getAlbumStatVo/{albumId}")
public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId);

AlbumDegradeFeignClient熔断类:

@Component
public class AlbumDegradeFeignClient implements AlbumFeignClient {

    @Override
    public Result<AlbumStatVo> getAlbumStatVo(Long albumId) {
        return null;
    }
}    

1.2 服务调用方汇总数据

回显时,后台需要提供将数据封装到map集合中;

result.put("albumInfo", albumInfo);			获取专辑信息
result.put("albumStatVo", albumStatVo);		获取专辑统计信息
result.put("baseCategoryView", baseCategoryView);	获取分类信息
result.put("announcer", userInfoVo);	获取主播信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/69

service-search 微服务itemApiController 控制器中添加

package com.atguigu.tingshu.search.api;

import com.atguigu.tingshu.common.login.GuiguLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.search.service.ItemService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@Tag(name = "专辑详情管理")
@RestController
@RequestMapping("api/search")
@SuppressWarnings({"all"})
public class itemApiController {

	@Autowired
	private ItemService itemService;


	/**
	 * 根据专辑Id 获取详情数据

	 * @param albumId
	 * @return
	 */
	@GuiguLogin(required = false)
	@Operation(summary = "专辑详情")
	@GetMapping("/albumInfo/{albumId}")
	public Result getItem(@PathVariable Long albumId){
		// 获取到专辑详情数据
		Map<String,Object> result = this.itemService.getItem(albumId);
		// 返回数据
		return Result.ok(result);
	}
}

service-util模块中提供线程池配置类

package com.atguigu.tingshu.common.thread;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author: atguigu
 * @create: 2023-09-25 10:09
 */
@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(){
        return new ThreadPoolExecutor(
                17,
                17,
                0,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(200),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

接口与实现

package com.atguigu.tingshu.search.service;

import java.util.Map;

public interface ItemService {
    /**
     * 根据专辑Id 获取数据
     * @param albumId
     * @return
     */
    Map<String, Object> getItem(Long albumId);
}
package com.atguigu.tingshu.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.search.service.ItemService;
import com.atguigu.tingshu.user.client.AlbumFeignClient;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;

@Slf4j
@Service
@SuppressWarnings({"all"})
public class ItemServiceImpl implements ItemService {

    @Autowired
    private AlbumFeignClient albumFeignClient;

    @Autowired
    private UserFeignClient userFeignClient;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    /**
     * 汇总专辑详情相关信息
     *
     * @param albumId
     * @return
     */
       @Override
    public Map<String, Object> getItem(Long albumId) {
        Map<String, Object> mapResult = new HashMap<>();
        CompletableFuture<AlbumInfo> albumCompletableFuture = CompletableFuture.supplyAsync(() -> {
            AlbumInfo albumInfo = albumFeignClient.getAlbumInfoById(albumId).getData();
            // 封装albumInfo专辑信息
            mapResult.put("albumInfo", albumInfo);
            return albumInfo;
        }, threadPoolExecutor);
    
        CompletableFuture<Void> albumStatCompletableFuture = CompletableFuture.runAsync(() -> {
            AlbumStatVo albumStatVo = albumFeignClient.getAlbumStatVo(albumId).getData();
            mapResult.put("albumStatVo", albumStatVo);
        }, threadPoolExecutor);
    
        CompletableFuture<Void> baseCategoryViewCompletableFuture = albumCompletableFuture.thenAcceptAsync(albumInfo -> {
            BaseCategoryView baseCategoryView = albumFeignClient.getCategoryView(albumInfo.getCategory3Id()).getData();
            mapResult.put("baseCategoryView", baseCategoryView);
            log.info("baseCategoryView:{}", JSON.toJSONString(baseCategoryView));
        }, threadPoolExecutor);
    
        CompletableFuture<Void> announcerCompletableFuture = albumCompletableFuture.thenAcceptAsync(albumInfo -> {
            UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData();
            mapResult.put("announcer", userInfoVo);
        }, threadPoolExecutor);
    
        CompletableFuture.allOf(albumCompletableFuture, albumStatCompletableFuture, baseCategoryViewCompletableFuture, announcerCompletableFuture).join();
        return mapResult;
    }
}

1.3 获取专辑声音列表

1.3.1 获取用户声音列表付费情况

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/87

user_paid_album 这张表记录了用户购买过的专辑

如果购买过,则在map 中存储数据 key=trackId value = 1

UserInfoApiController 控制器:

/**
 * 判断用户是否购买声音列表
 *
 * @param albumId
 * @param trackIdList
 * @return
 */
@Operation(summary = "判断用户是否购买声音列表")
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(@PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> trackIdList) {
    //	调用服务层方法
    Map<Long, Integer> map = userInfoService.userIsPaidTrack(userId, albumId, trackIdList);
    //	返回map 集合数据
    return Result.ok(map);
}

UserInfoService接口

/**
     * 判断用户是否购买过声音列表
     * @param userId
     * @param albumId
     * @param trackIdList
     * @return
     */
Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> trackIdList);

UserInfoServiceImpl实现类

@Autowired
private UserPaidAlbumMapper userPaidAlbumMapper;

@Autowired
private UserPaidTrackMapper userPaidTrackMapper;


/**
 * 判断用户是否已购买专辑或者专辑下声音
 *
 * @param userId      用户ID
 * @param albumId     专辑ID
 * @param trackIdList 专辑下声音ID列表
 * @return
 */
@Override
public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> trackIdList) {
    //根据用户userId+专辑albumId 获取专辑付费记录
    LambdaQueryWrapper<UserPaidAlbum> paidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>();
    paidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getUserId, userId)
            .eq(UserPaidAlbum::getAlbumId, albumId);
    UserPaidAlbum userPaidAlbum = userPaidAlbumMapper.selectOne(paidAlbumLambdaQueryWrapper);
    if (userPaidAlbum != null) {
        Map<Long, Integer> map = new HashMap<>();
        for (Long trackId : trackIdList) {
            map.put(trackId, 1);
        }
        return map;
    }

    //根据用户ID+声音ID查询声音付费记录
    LambdaQueryWrapper<UserPaidTrack> paidTrackQueryWrapper = new LambdaQueryWrapper<>();
    paidTrackQueryWrapper.eq(UserPaidTrack::getUserId, userId)
            .in(UserPaidTrack::getTrackId, trackIdList);
    List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(paidTrackQueryWrapper);
    if (CollectionUtil.isNotEmpty(userPaidTrackList)) {
        //获取已购买声音ID集合
        List<Long> paidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
        //遍历入参中声音ID判断是否已支付
        Map<Long, Integer> map = new HashMap<>();
        for (Long trackId : trackIdList) {
            if (paidTrackIdList.contains(trackId)) {
                map.put(trackId, 1);
            } else {
                map.put(trackId, 0);
            }
        }
        return map;
    } else {
        //如果无声音付费记录 则将所有声音标识为未付费
        Map<Long, Integer> map = new HashMap<>();
        for (Long trackId : trackIdList) {
            map.put(trackId, 0);
        }
        return map;
    }
}

service-user-client模块中UserFeignClient 远程调用接口中添加:

/**
 * 判断用户是否购买声音列表
 *
 * @param albumId
 * @param trackIdList
 * @return
 */
@PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(@PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> trackIdList)

UserDegradeFeignClient熔断类

@Component
public class UserDegradeFeignClient implements UserInfoFeignClient {

    @Override
    public Result<Map<Long, Integer>> userIsPaidTrack(Long userId, Long albumId, List<Long> trackIdList) {
        return null;
    }
}

1.3.2 根据专辑Id 获取到声音列表

service-album 微服务中添加控制器. 获取专辑声音列表时,我们将数据都统一封装到AlbumTrackListVo实体类中

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/89

TrackInfoApiController控制器

/**
 * 根据专辑Id获取声音列表
 *
 * @param albumId
 * @param page
 * @param limit
 * @return
 */
@GuiguLogin(required = false)
@Operation(summary = "获取专辑声音分页列表")
@GetMapping("/trackInfo/findAlbumTrackPage/{albumId}/{page}/{limit}")
public Result<IPage<AlbumTrackListVo>> getAlbumTrackPage(
        @PathVariable Long albumId,
        @PathVariable Long page,
        @PathVariable Long limit) {
    //	获取用户Id
    Long userId = AuthContextHolder.getUserId();
    //	构建分页对象
    Page<AlbumTrackListVo> pageInfo = new Page<>(page, limit);
    //	调用服务层方法
    pageInfo = trackInfoService.getAlbumTrackPage(pageInfo, albumId, userId);
    //	返回数据
    return Result.ok(pageInfo);
}

TrackInfoService接口:

public interface TrackInfoService extends IService<TrackInfo> {   
	IPage<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageParam, Long albumId, Long userId);
}

TrackInfoServiceImpl实现类:

  • 根据专辑Id 获取到专辑列表,
    • 用户为空的时候,然后找出哪些是需要付费的声音并显示付费 isShowPaidMark=true

付费类型: 0101-免费 0102-vip付费 0103-付费

  • ​ 用户不为空的时候
    • 判断用户的类型
    • vip 免费类型
    • 如果不是vip 需要付费
    • 如果是vip 但是已经过期了 也需要付费
    • 需要付费
    • 统一处理需要付费业务

​ 获取到声音Id列表集合 与 用户购买声音Id集合进行比较 将用户购买的声音存储到map中,key=trackId value = 1或0; 1:表示购买过,0:表示没有购买过

如果声音列表不包含,则将显示为付费,否则判断用户是否购买过声音,没有购买过设置为付费

@Autowired
private UserFeignClient userFeignClient;

/**
 * 分页获取专辑声音列表
 * @param pageParam
 * @param albumId
 * @param userId
 * @return
 */
@Override
public Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageParam, Long albumId, Long userId) {
    //	根据专辑Id 获取到专辑
    Page<AlbumTrackListVo> pageInfo = trackInfoMapper.getAlbumTrackPage(pageParam, albumId);
    //	判断用户是否需要付费:0101-免费 0102-vip付费 0103-付费
    AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
    Assert.notNull(albumInfo, "专辑对象不能为空");

    //	判断用户是否登录
    if (null == userId) {
        //	除免费的都需要显示付费标识
        if (!SystemConstant.ALBUM_PAY_TYPE_FREE.equals(albumInfo.getPayType())) {
            //处理试听声音,获取需要付费的声音列表
            pageInfo.getRecords().stream().filter(albumTrackListVo ->
                    //获取付费专辑编号大于5的(非免费声音)
                    albumTrackListVo.getOrderNum().intValue() > albumInfo.getTracksForFree()
            ).collect(Collectors.toList()).stream().forEach(albumTrackListVo -> {
                //	显示付费通知
                albumTrackListVo.setIsShowPaidMark(true);
            });
        }
    } else {
        //	用户已登录
        //	声明变量是否需要付费,默认不需要付费
        boolean isNeedPaid = false;
        //  vip 付费情况
        if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(albumInfo.getPayType())) {
            //	获取用户信息
            UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData();
            //	1.	VIP 免费,如果不是vip则需要付费,将这个变量设置为true,需要购买
            if (0 == userInfoVo.getIsVip().intValue()) {
                isNeedPaid = true;
            }
            //1.1 如果是vip但是vip过期了(定时任务还为更新状态)
            if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().before(new Date())) {
                isNeedPaid = true;
            } else if (SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(albumInfo.getPayType())) {
                //	2.	付费
                isNeedPaid = true;
            }
        }
        //需要付费,判断用户是否购买过专辑或声音
        if (isNeedPaid) {
            List<AlbumTrackListVo> albumTrackNeedPaidListVoList = pageInfo.getRecords().stream().filter(albumTrackListVo -> albumTrackListVo.getOrderNum().intValue() > albumInfo.getTracksForFree()).collect(Collectors.toList());
            //	判断
            if (!CollectionUtils.isEmpty(albumTrackNeedPaidListVoList)) {
                //	判断用户是否购买该声音
                //	获取到声音Id 集合列表
                List<Long> trackIdList = albumTrackNeedPaidListVoList.stream().map(AlbumTrackListVo::getTrackId).collect(Collectors.toList());
                //	获取用户购买的声音列表
                Result<Map<Long, Integer>> mapResult = userFeignClient.userIsPaidTrack(userId, albumId, trackIdList);
                Assert.notNull(mapResult, "声音集合不能为空.");
                Map<Long, Integer> map = mapResult.getData();
                Assert.notNull(map, "map集合不能为空.");
                albumTrackNeedPaidListVoList.forEach(albumTrackListVo -> {
                    if (!map.containsKey(albumTrackListVo.getTrackId())) {
                        // 显示付费
                        albumTrackListVo.setIsShowPaidMark(false);
                    } else {
                        //	如果map.get(albumTrackLis tVo.getTrackId()) == 1 已经购买过,则不显示付费标识;
                        boolean isBuy = map.get(albumTrackListVo.getTrackId()) == 1 ? false : true;
                        albumTrackListVo.setIsShowPaidMark(isBuy);
                    }
                });
            }
        }
    }
    return pageInfo;
}

TrackInfoMapper接口:条件必须是当前已经开放并且是审核通过状态的数据,并且还需要获取到声音的播放量以及评论数量

@Mapper
public interface TrackInfoMapper extends BaseMapper<TrackInfo> {
    /**
     * 获取专辑声音列表
     * @param pageParam
     * @param albumId
     * @return
     */
    Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageParam, @Param("albumId") Long albumId);

}

TrackInfoMapper.xml 映射文件

<!--根据专辑 Id 获取到声音列表-->
<select id="getAlbumTrackPage" resultType="com.atguigu.tingshu.vo.album.AlbumTrackListVo">
  select
  info.trackId,
  trackTitle,
  info.mediaDuration,
  info.orderNum,
  info.createTime,
  MAX(IF(info.statType = '0701', info.statNum, 0)) as playStatNum,
  MAX(IF(info.statType = '0704', info.statNum, 0)) as commentStatNum
  from
  (select
  track.id as trackId,
  track.track_title as trackTitle,
  track.media_duration as mediaDuration,
  track.order_num as orderNum,
  track.create_time as createTime,
  stat.stat_type as statType,
  stat.stat_num as statNum
  from track_info track
  left join track_stat stat on stat.track_id = track.id
  where track.album_id = #{albumId} and track.is_open = '1' and track.status = '0501') info
  group by info.trackId
  order by info.orderNum asc
</select>

2、MongoDB文档型数据库

详情见:第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;

}

3、声音详情

3.1 获取声音播放进度

在播放声音的时候,会有触发一个获取播放进度的控制器!因为页面每隔10s会自动触发一次保存功能,会将数据写入MongoDB中。所以我们直接从MongoDB中获取到上一次声音的播放时间即可!

YAPI接口:http://192.168.200.6:3000/project/11/interface/api/71

service-user 微服务的 UserListenProcessApiController 控制器中添加

/**
  * 获取声音播放的时间
  * @param trackId
  * @return
  */
@GuiguLogin
@Operation(summary = "获取声音的上次跳出时间")
@GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}")
public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) {
  //	获取用户Id
  Long userId = AuthContextHolder.getUserId();
  //	调用服务层方法
  BigDecimal trackBreakSecond = userListenProcessService.getTrackBreakSecond(userId, trackId);
  //	返回数据
  return Result.ok(trackBreakSecond);
}

UserListenProcessService接口

/**
     * 根据用户Id,声音Id 获取到播放进度
     * @param userId
     * @param trackId
     * @return
     */
BigDecimal getTrackBreakSecond(Long userId, Long trackId);

UserListenProcessServiceImpl实现类:

@Override
public BigDecimal getTrackBreakSecond(Long userId, Long trackId) {
  //	根据用户Id,声音Id获取播放进度对象
  Query query = Query.query(Criteria.where("userId").is(userId).and("trackId").is(trackId));
  UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
  //	判断
  if (null != userListenProcess){
    //	获取到播放的跳出时间
    return userListenProcess.getBreakSecond();
  }
  return new BigDecimal("0");
}

3.2 更新播放进度

页面每隔10秒左右更新播放进度.

  1. 更新播放进度页面会传递 专辑Id ,秒数,声音Id 。后台会将这个三个属性封装到UserListenProcessVo 对象中。然后利用MongoDB进行存储到UserListenProcess实体类中!
  2. 为了提高用户快速访问,将用户信息存储到缓存中。先判断当前用户Id 与 声音Id 是否存在,不存在的话才将数据存储到缓存,并且要发送消息给kafka。
  3. kafka 监听消息并消费,更新专辑与声音的统计数据。

3.2.1 更新MongoDB

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/73

UserListenProcessApiController 控制器中添加

/**
  * 更新播放进度
  * @param userListenProcessVo
  * @return
  */
@GuiguLogin
@Operation(summary = "更新播放进度")
@PostMapping("/userListenProcess/updateListenProcess")
public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo) {
  //	获取用户Id
  Long userId = AuthContextHolder.getUserId();
  //	调用服务层方法
  userListenProcessService.updateListenProcess(userId, userListenProcessVo);
  return Result.ok();
}

UserListenProcessService接口:

/**
* 更新播放进度
* @param userId
* @param userListenProcessVo
*/
void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo);

UserListenProcessServiceImpl实现类:


@Autowired
private RedisTemplate redisTemplate;

@Autowired
private KafkaService kafkaService;


@Override
public void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo) {
    //1.根据用户ID+声音ID查询声音播放进度
    Query query = Query.query(Criteria.where("userId").is(userId).and("trackId").is(userListenProcessVo.getTrackId()));
    UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
    //2.如果存在进度更新播放时间
    if (userListenProcess != null) {
        //	设置更新时间
        userListenProcess.setUpdateTime(new Date());
        //	设置跳出时间
        userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond());
        //	存储数据
        mongoTemplate.save(userListenProcess, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
    } else {
        //3.如果不存在则创建进度对象新增播放进度对象-第一次播放该声音,更新声音统计信息
        //	创建对象
        userListenProcess = new UserListenProcess();
        //	进行属性拷贝
        BeanUtils.copyProperties(userListenProcessVo, userListenProcess);
        //	设置Id
        userListenProcess.setId(ObjectId.get().toString());
        //	设置用户Id
        userListenProcess.setUserId(userId);
        //	设置是否显示
        userListenProcess.setIsShow(1);
        //	创建时间
        userListenProcess.setCreateTime(new Date());
        //	更新时间
        userListenProcess.setUpdateTime(new Date());
        //	保存数据
        mongoTemplate.save(userListenProcess, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
    }
    //4.避免一天内多次更新统计信息;基于Kafka消息更新专辑统计信息
    String ifRepeatStatKey = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + DateUtil.today() + ":" + userId + ":" + userListenProcessVo.getTrackId();
    Boolean ifRepeatStat = redisTemplate.opsForValue().setIfAbsent(ifRepeatStatKey, userId, 24, TimeUnit.HOURS);
    if (ifRepeatStat) {
        //一天内当前用户首次播放
        TrackStatMqVo trackStatMqVo = new TrackStatMqVo();
        trackStatMqVo.setBusinessNo(UUID.randomUUID().toString().replaceAll("-",""));
        trackStatMqVo.setAlbumId(userListenProcessVo.getAlbumId());
        trackStatMqVo.setTrackId(userListenProcessVo.getTrackId());
        trackStatMqVo.setStatType(SystemConstant.TRACK_STAT_PLAY);
        trackStatMqVo.setCount(1);
        //	发送消息
        kafkaService.sendMessage(KafkaConstant.QUEUE_TRACK_STAT_UPDATE, JSON.toJSONString(trackStatMqVo));
    }
}

3.2.2 更新MySQL统计信息

service-album 微服务中添加监听消息:

package com.atguigu.tingshu.album.receiver;

import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.album.service.TrackInfoService;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class TrackReceiver {

    @Autowired
    private TrackInfoService trackInfoService;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 监听更新统计状态
     *
     * @param consumerRecord
     */
    @KafkaListener(topics = KafkaConstant.QUEUE_TRACK_STAT_UPDATE)
    public void updateStat(ConsumerRecord<String, String> consumerRecord) {
        //  获取发送的数据
        TrackStatMqVo trackStatMqVo = JSON.parseObject(consumerRecord.value(), TrackStatMqVo.class);
        log.info("更新声音统计:" + JSON.toJSONString(trackStatMqVo));
        //  业务去重
        String key = trackStatMqVo.getBusinessNo();
        boolean isExist = redisTemplate.opsForValue().setIfAbsent(key, 1, 1, TimeUnit.HOURS);
        //  如果key存入成功
        if (isExist) {
            //  调用服务层方法
            trackInfoService.updateStat(trackStatMqVo.getAlbumId(), trackStatMqVo.getTrackId(), trackStatMqVo.getStatType(), trackStatMqVo.getCount());
        }
    }
}

TrackInfoService 中添加接口

/**
 * 更新统计方法
 * @param albumId
 * @param trackId
 * @param statType
 * @param count
 */
void updateStat(Long albumId, Long trackId, String statType,Integer count);

TrackInfoServiceImpl 中添加实现

@Override
@Transactional(rollbackFor = Exception.class)
public void updateStat(Long albumId, Long trackId, String statType, Integer count) {
    //	更新统计数据
    trackInfoMapper.updateStat(trackId, statType, count);
    //	更新评论数
    if(statType.equals(SystemConstant.TRACK_STAT_COMMENT)) {
        albumInfoMapper.updateStat(albumId, SystemConstant.ALBUM_STAT_COMMENT, count);
    }
    //	更新播放量
    if(statType.equals(SystemConstant.TRACK_STAT_PLAY)) {
        albumInfoMapper.updateStat(albumId, SystemConstant.ALBUM_STAT_PLAY, count);
    }
}

TrackInfoMapper.java 添加方法

/**
   * 更新评论数据
   * @param trackId
   * @param statType
   * @param count
   */
Integer updateStat(@Param("trackId") Long trackId, @Param("statType") String statType, @Param("count") Integer count);

TrackInfoMapper.xml 实现

<!--更新统计状态-->
<update id="updateStat">
        update track_stat
        set stat_num = stat_num + #{count}
		where track_id = #{trackId} and stat_type = #{statType}
</update>

AlbumInfoMapper.java 接口添加

/**
     * 更新评论与播放量
     * @param albumId
     * @param statType
     * @param count
     */
Integer updateStat(@Param("albumId") Long albumId, @Param("statType") String statType, @Param("count") Integer count);

AlbumInfoMapper.xml 实现

<update id="updateStat">
        update album_stat
        set stat_num = stat_num + #{count}
		where album_id = #{albumId} and stat_type = #{statType}
</update>

3.3 获取声音统计信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/75

统计声音需要更新的数据如下,我们将数据封装到一个实体类中便于操作

@Data
@Schema(description = "用户声音统计信息")
public class TrackStatVo {


   @Schema(description = "播放量")
   private Integer playStatNum;

   @Schema(description = "订阅量")
   private Integer collectStatNum;

   @Schema(description = "点赞量")
   private Integer praiseStatNum;

   @Schema(description = "评论数")
   private Integer commentStatNum;

}

TrackInfoApiController 控制器中添加

/**
  * 获取声音统计接口
  * @param trackId
  * @return
  */
@Operation(summary = "获取声音统计信息")
@GetMapping("/trackInfo/getTrackStatVo/{trackId}")
public Result<TrackStatVo> getTrackStatVo(@PathVariable Long trackId) {
  //	调用服务层方法
  TrackStatVo trackStatVo = trackInfoService.getTrackStatVoByTrackId(trackId);
  //	返回对象
  return Result.ok(trackStatVo);
}

TrackInfoService接口

/**
  * 根据声音Id 获取统计信息
  * @param trackId
  * @return
  */
TrackStatVo getTrackStatVoByTrackId(Long trackId);

TrackInfoServiceImpl实现类

 @Override
 public TrackStatVo getTrackStatVoByTrackId(Long trackId) {
 	//	调用mapper 层方法
 	return trackInfoMapper.getTrackStat(trackId);
 }

TrackInfoMapper.java

/**
  * 根据声音Id 获取到声音统计
  * @param trackId
  * @return
  */
TrackStatVo getTrackStat(@Param("trackId") Long trackId);

TrackInfoMapper.xml

<!--  根据声音Id统计声音状态.  -->
<select id="getTrackStat" resultType="com.atguigu.tingshu.vo.album.TrackStatVo">
        select
            MAX(IF(info.statType = '0701', info.statNum, 0)) as playStatNum,
MAX(IF(info.statType = '0702', info.statNum, 0)) as collectStatNum,
MAX(IF(info.statType = '0703', info.statNum, 0)) as praiseStatNum,
MAX(IF(info.statType = '0704', info.statNum, 0)) as commentStatNum
        from (
  select
                     stat.track_id as trackId,
  stat.stat_type as statType,
  stat.stat_num as statNum
                 from track_stat stat
                 where track_id = #{trackId}
) info
        group by info.trackId
</select>

3.4 专辑上次播放专辑声音

image-20231012111356796

我们需要根据用户Id 来获取播放记录 ,需要获取到专辑Id 与声音Id 封装到map中然后返回数据即可!

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/83

控制器 UserListenProcessApiController

/**
 * 获取最近一次播放声音
 * @return
 */
@GuiguLogin
@Operation(summary = "获取最近一次播放声音")
@GetMapping("/userListenProcess/getLatelyTrack")
public Result<Map<String,Object>> getLatelyTrack() {
	// 获取用户Id
	Long userId = AuthContextHolder.getUserId();
	// 获取播放记录
	Map<String,Object> map = userListenProcessService.getLatelyTrack(userId);
	// 返回数据
	return Result.ok(map);
}

UserListenProcessService接口:

/**
 * 获取播放记录
 * @param userId
 * @return
 */
Map<String, Object> getLatelyTrack(Long userId);

UserListenProcessServiceImpl实现类

@Override
public Map<String, Object> getLatelyTrack(Long userId) {
   Query query = Query.query(Criteria.where("userId").is(userId));
   Sort sort = Sort.by(Sort.Direction.DESC, "updateTime");
   query.with(sort);
   UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId));
   if(null == userListenProcess) {
      return null;
   }
   Map<String, Object> map = new HashMap<>();
   map.put("albumId", userListenProcess.getAlbumId());
   map.put("trackId", userListenProcess.getTrackId());
   return map;
}

4、更新Redis排行榜

手动调用一次更新,查看排行榜。后续会整合xxl-job 分布式定时任务调度框架做定时调用。

image-20231012114427463

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/77

service-album微服务中BaseCategoryApiController控制器中添加

/**
 * 查询所有的一级分类数据
 * @return
 */
@Operation(summary = "查询所有的一级分类信息")
@GetMapping("/category/findAllCategory1")
public Result<List<BaseCategory1>> getAllCategory1() {
	//	获取所有的一级分类数据
	List<BaseCategory1> baseCategory1List = baseCategoryService.getAllCategory1();
	return Result.ok(baseCategory1List);
}

BaseCategoryService接口:

/**
  * 获取所有一级分类数据
  * @return
  */
List<BaseCategory1> getAllCategory1();

BaseCategoryServiceImpl实现类:

@Override
public List<BaseCategory1> getAllCategory1() {
  return baseCategory1Mapper.selectList(new LambdaQueryWrapper<BaseCategory1>().orderByAsc(BaseCategory1::getOrderNum));
}

AlbumFeignClient

/**
 * 查询所有的一级分类数据
 * @return
 */
@GetMapping("/category/findAllCategory1")
public Result<List<BaseCategory1>> getAllCategory1();

AlbumDegradeFeignClient熔断类

@Override
public Result<List<BaseCategory1>> getAllCategory1() {
    return null;      
}

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/79

SearchApiController 中添加控制器

/**
 * 更新排行榜
 *
 * @return
 */
@SneakyThrows
@Operation(summary = "更新排行榜")
@GetMapping("/albumInfo/updateLatelyAlbumRanking")
public Result updateLatelyAlbumRanking() {
    //  调用服务层方法
    searchService.updateLatelyAlbumRanking();
    //  默认返回
    return Result.ok();
}

SearchService接口:

/**
  * 更新排行榜
  */
void updateLatelyAlbumRanking();

SearchServiceImpl实现类:


@Autowired
private RedisTemplate redisTemplate;

@Override
public void updateLatelyAlbumRanking() {
    //  排行榜,按分类维度统计, 先获取分类数据
    Result<List<BaseCategory1>> baseCategory1Result = albumFeignClient.getAllCategory1();
    Assert.notNull(baseCategory1Result,"对象不能为空");
    List<BaseCategory1> baseCategory1List = baseCategory1Result.getData();
    if (!CollectionUtils.isEmpty(baseCategory1List)){
        //  循环遍历
        for (BaseCategory1 baseCategory1 : baseCategory1List) {
            // 统计维度:热度:hotScore、播放量:playStatNum、订阅量:subscribeStatNum、购买量:buyStatNum、评论数:albumCommentStatNum
            String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"};
            for (String rankingDimension : rankingDimensionArray) {
                SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(s -> s
                        .index("albuminfo")
                        .size(10)
                        .sort(t -> t.field(f -> f.field(rankingDimension).order(SortOrder.Desc))), AlbumInfoIndex.class);

                //  解析查询列表
                List<Hit<AlbumInfoIndex>> albumInfoIndexHitList = response.hits().hits();
                List<AlbumInfoIndex> albumInfoIndexList = albumInfoIndexHitList.stream().map(hit -> hit.source()).collect(Collectors.toList());
                //  将排行榜数据更新到缓存中
                redisTemplate.boundHashOps(RedisConstant.RANKING_KEY_PREFIX+baseCategory1.getId()).put(rankingDimension,albumInfoIndexList);
            }
        }
    }
}

5、获取排行榜

image-20231012114420751

点击排行榜的时候,能看到获取排行榜的地址

排行榜:key=ranking:category1Id field = hotScore 或 playStatNum 或 subscribeStatNum 或 buyStatNum 或albumCommentStatNum value=List

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/81

SearchApiController 控制器中添加

/**
 * 获取排行榜列表
 * @param category1Id
 * @param dimension
 * @return
 */
@Operation(summary = "获取排行榜列表")
@GetMapping("/albumInfo/findRankingList/{category1Id}/{dimension}")
public Result<List<AlbumInfoIndexVo>> getRankingList(@PathVariable Long category1Id, @PathVariable String dimension) {
    //  调用服务层方法
    List<AlbumInfoIndexVo> infoIndexVoList = searchService.getRankingList(category1Id, dimension);
    //  返回结果集
    return Result.ok(infoIndexVoList);
}

SearchService接口:

/**
     * 获取排行榜列表
     * @param category1Id
     * @param dimension
     * @return
     */
List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension);

SearchServiceImpl实现类

@Override
public List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension) {
	return (List<AlbumInfoIndexVo>) redisTemplate.boundHashOps(RedisConstant.RANKING_KEY_PREFIX + category1Id).get(dimension);
}