第5章 详情介绍.md 46 KB

谷粒随享

第5章 专辑/声音详情

学习目标:

  • 专辑详情业务需求
    • 专辑服务 1.专辑信息 2.分类信息 3.统计信息 4,主播信息
    • 搜索服务:汇总专辑详情数据
  • 专辑包含声音列表(付费标识动态展示)
  • MongoDB文档型数据库应用
  • 基于MongoDB存储用户对于声音播放进度
  • 基于Redis实现排行榜(将不同不同分类下包含各个维度热门专辑排行)

1、专辑详情

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

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

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

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

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

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

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 = "根据专辑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;
}

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.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;
    }
}

1.3 获取专辑声音列表

需求:根据专辑ID分页查询声音列表,返回当前页10条记录,对每条声音付费标识处理。关键点:哪个声音需要展示付费标识。

默认每个声音付费标识为:false

判断专辑付费类型:0101-免费、0102-vip免费、0103-付费

  • 用户未登录
    • 专辑类型不是免费,将除了免费可以试听声音外,将本页中其余声音付费标识设置:true
  • 用户登录(获取是否为VIP)
    • 不是VIP,或者VIP过期(除了免费以外声音全部设置为付费)
    • 是VIP,专辑类型为付费 需要进行处理
  • 统一处理需要付费情况
    • 获取用户购买情况(专辑购买,或者声音购买)得到每个声音构建状态
    • 判断根据用户购买情况设置声音付费标识

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

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;
    }
}

1.3.2 查询专辑声音列表

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实现类:

  • 根据专辑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 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>

测试:

  • 手动增加用户购买专辑记录:user_paid_album
  • 手动增加用户购买声音记录:user_paid_track
  • 手动修改VIP会员:user_info

情况一:未登录情况,专辑付费类型:VIP免费 付费 查看声音列表->试听声音免费+其余都需要展示付费标识

情况二:登录情况

  • 普通用户
    • 免费 全部免费
    • VIP付费 试听声音免费+用户购买过专辑/声音,未购买展示付费标识
    • 付费:试听声音免费+用户购买过专辑/声音,未购买展示付费标识
  • VIP用户
    • 免费 全部免费
    • VIP付费 全部免费
    • 付费:试听声音免费+用户购买过专辑/声音,未购买展示付费标识

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 //必须登录  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");
}

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
 */
@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));
    }
}

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.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);
}

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 控制器中添加

/**
 * 根据声音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

3.4 专辑上次播放专辑声音

image-20231012111356796

我们需要根据用户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;
}

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() {
    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);
    }
}

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 控制器中添加

/**
 * 获取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;
}