活动介绍

LambdaQueryWrapper中如何实现对count(*)进行排序

时间: 2023-10-13 18:18:49 浏览: 1279
LambdaQueryWrapper是MyBatis-Plus提供的查询构造器,它可以通过Lambda表达式来构造SQL查询语句。如果想对count(*)进行排序,可以使用orderByDesc方法,示例代码如下: ``` LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.select(count()).eq(User::getStatus, 1) .groupBy(User::getDeptId) .orderByDesc(count()); List<User> userList = userMapper.selectList(queryWrapper); ``` 在上述代码中,LambdaQueryWrapper的select方法中使用了count()函数,指定查询结果集合并后的总数量。然后通过eq方法指定了查询条件,groupBy方法指定了分组条件,orderByDesc方法指定了按照count(*)的值进行降序排序。最后调用selectList方法执行查询,得到符合条件的用户列表。 需要注意的是,在使用count()函数时需要指定别名,否则会报错。例如:select(count()).as("total")。
相关问题

**谷粒随享** ## 第7章 专辑/声音详情 **学习目标:** - 专辑详情业务需求 - 专辑服务 1.专辑信息 2.分类信息 3.统计信息 4,主播信息 - 搜索服务:汇总专辑详情数据 - 专辑包含**声音列表(付费标识动态展示)** - MongoDB文档型数据库应用 - 基于**MongoDB**存储用户对于声音**播放进度** - 基于Redis实现排行榜(将不同分类下包含各个维度热门专辑排行) # 1、专辑详情 ![详情-专辑详情和声音列表](assets/详情-专辑详情和声音列表.gif) 专辑详情页面渲染需要以下四项数据: - **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** 控制器 ```java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ @Operation(summary = "根据专辑ID查询专辑统计信息") @GetMapping("/albumInfo/getAlbumStatVo/{albumId}") public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId) { AlbumStatVo albumStatVo = albumInfoService.getAlbumStatVo(albumId); return Result.ok(albumStatVo); } ``` **AlbumInfoService**接口 ```java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ AlbumStatVo getAlbumStatVo(Long albumId); ``` **AlbumInfoServiceImpl**实现类 ```java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ @Override public AlbumStatVo getAlbumStatVo(Long albumId) { return albumInfoMapper.getAlbumStatVo(albumId); } ``` **albumInfoMapper.java** ```java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ AlbumStatVo getAlbumStatVo(@Param("albumId") Long albumId); ``` **albumInfoMapper.xml** ```sql <!--据专辑ID查询专辑统计信息--> <select id="getAlbumStatVo" resultType="com.atguigu.tingshu.vo.album.AlbumStatVo"> select stat.album_id, max(if(stat.stat_type='0401', stat.stat_num, 0)) playStatNum, max(if(stat.stat_type='0402', stat.stat_num, 0)) subscribeStatNum, max(if(stat.stat_type='0403', stat.stat_num, 0)) buyStatNum, max(if(stat.stat_type='0404', stat.stat_num, 0)) commentStatNum from album_stat stat where stat.album_id = #{albumId} and stat.is_deleted = 0 group by stat.album_id </select> ``` `service-album-client`模块**AlbumFeignClient** 接口中添加 ```java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ @GetMapping("/albumInfo/getAlbumStatVo/{albumId}") public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId); ``` **AlbumDegradeFeignClient**熔断类: ```java @Override public Result<AlbumStatVo> getAlbumStatVo(Long albumId) { log.error("[专辑模块]提供远程调用方法getAlbumStatVo服务降级"); return null; } ``` ## 1.2 服务调用方汇总数据 回显时,后台需要提供将数据封装到map集合中; ```java 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** 控制器中添加 ```java package com.atguigu.tingshu.search.api; import com.atguigu.tingshu.common.result.Result; import com.atguigu.tingshu.search.service.ItemService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @Tag(name = "专辑详情管理") @RestController @RequestMapping("api/search") @SuppressWarnings({"all"}) public class itemApiController { @Autowired private ItemService itemService; /** * 根据专辑ID查询专辑详情相关数据 * * @param albumId * @return */ @Operation(summary = "根据专辑ID查询专辑详情相关数据") @GetMapping("/albumInfo/{albumId}") public Result<Map<String, Object>> getItemInfo(@PathVariable Long albumId) { Map<String, Object> mapResult = itemService.getItemInfo(albumId); return Result.ok(mapResult); } } ``` 接口与实现 ```java package com.atguigu.tingshu.search.service; import java.util.Map; public interface ItemService { /** * 根据专辑ID查询专辑详情相关数据 * * @param albumId * @return */ Map<String, Object> getItemInfo(Long albumId); } ``` ```java 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查询专辑详情相关数据 * 1.albumInfo:当前专辑信息 * 2.albumStatVo:专辑统计信息 * 3.baseCategoryView:专辑分类信息 * 4.announcer:专辑主播信息 * * @param albumId * @return */ @Override public Map<String, Object> getItemInfo(Long albumId) { //1.创建响应结果Map对象 HashMap在多线程环境下并发读写线程不安全:导致key覆盖;导致死循环 //采用线程安全:ConcurrentHashMap Map<String, Object> mapResult = new ConcurrentHashMap<>(); //2.远程调用专辑服务获取专辑基本信息-封装albumInfo属性 CompletableFuture<AlbumInfo> albumInfoCompletableFuture = CompletableFuture.supplyAsync(() -> { AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData(); Assert.notNull(albumInfo, "专辑:{}不存在", albumId); mapResult.put("albumInfo", albumInfo); return albumInfo; }, threadPoolExecutor); //3.远程调用专辑服务获取专辑统计信息-封装albumStatVo属性 CompletableFuture<Void> albumStatCompletableFuture = CompletableFuture.runAsync(() -> { AlbumStatVo albumStatVo = albumFeignClient.getAlbumStatVo(albumId).getData(); Assert.notNull(albumStatVo, "专辑统计信息:{}不存在", albumId); mapResult.put("albumStatVo", albumStatVo); }, threadPoolExecutor); //4.远程调用专辑服务获取专辑分类信息-封装baseCategoryView属性 CompletableFuture<Void> baseCategoryViewCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> { BaseCategoryView categoryView = albumFeignClient.getCategoryView(albumInfo.getCategory3Id()).getData(); Assert.notNull(categoryView, "分类:{}不存在", albumInfo.getCategory3Id()); mapResult.put("baseCategoryView", categoryView); }, threadPoolExecutor); //5.远程调用用户服务获取主播信息-封装announcer属性 CompletableFuture<Void> announcerCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> { UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData(); Assert.notNull(userInfoVo, "用户:{}不存在", albumInfo.getUserId()); mapResult.put("announcer", userInfoVo); }, threadPoolExecutor); //6.组合异步任务,阻塞等待所有异步任务执行完毕 CompletableFuture.allOf( albumInfoCompletableFuture, albumStatCompletableFuture, baseCategoryViewCompletableFuture, announcerCompletableFuture ).join(); return mapResult; } } ``` ## 1.3 获取专辑声音列表 查询时,评论数关键字属性:commentStatNum ![](assets/image-20231005140148572.png) 需求:根据专辑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** 控制器: ```java /** * 该接口提供给给专辑服务,展示声音列表动态判断付费标识 * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ @Operation(summary = "判断当前用户某一页中声音列表购买情况") @PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}") public Result<Map<Long, Integer>> userIsPaidTrack( @PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> needChackTrackIdList) { Map<Long, Integer> mapResult = userInfoService.userIsPaidTrack(userId, albumId, needChackTrackIdList); return Result.ok(mapResult); } ``` **UserInfoService接口**: ```java /** * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needChackTrackIdList); ``` **UserInfoServiceImpl实现类**: ```java @Autowired private UserPaidAlbumMapper userPaidAlbumMapper; @Autowired private UserPaidTrackMapper userPaidTrackMapper; /** * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ @Override public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needChackTrackIdList) { //1.根据用户ID+专辑ID查询已购专辑表 LambdaQueryWrapper<UserPaidAlbum> userPaidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>(); userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getAlbumId, albumId); userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getUserId, userId); Long count = userPaidAlbumMapper.selectCount(userPaidAlbumLambdaQueryWrapper); if (count > 0) { //1.1 存在专辑购买记录-用户购买过该专辑,将待检查声音列表购买情况设置为:1 Map<Long, Integer> mapResult = new HashMap<>(); for (Long trackId : needChackTrackIdList) { mapResult.put(trackId, 1); } return mapResult; } //2. 不存在专辑购买记录-根据用户ID+声音列表查询已购声音表 LambdaQueryWrapper<UserPaidTrack> userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>(); userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getUserId, userId); userPaidTrackLambdaQueryWrapper.in(UserPaidTrack::getTrackId, needChackTrackIdList); //获取本页中已购买声音列表 List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(userPaidTrackLambdaQueryWrapper); //2.1 不存在声音购买记录-将待检查声音列表购买情况设置为:0 if (CollectionUtil.isEmpty(userPaidTrackList)) { Map<Long, Integer> mapResult = new HashMap<>(); for (Long trackId : needChackTrackIdList) { mapResult.put(trackId, 0); } return mapResult; } //2.2 存在声音购买记录-循环判断待检查声音ID找出哪些是已购,哪些是未购买 List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList()); Map<Long, Integer> mapResult = new HashMap<>(); for (Long needCheckTrackId : needChackTrackIdList) { //如果待检查声音ID包含在已购声音Id集合中(已购买) if (userPaidTrackIdList.contains(needCheckTrackId)) { mapResult.put(needCheckTrackId, 1); } else { //反之则未购买声音 mapResult.put(needCheckTrackId, 0); } } return mapResult; } ``` `service-user-client`模块中**UserFeignClient** 远程调用接口中添加: ```java /** * 该接口提供给给专辑服务,展示声音列表动态判断付费标识 * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ @PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}") public Result<Map<Long, Integer>> userIsPaidTrack( @PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> needChackTrackIdList); ``` **UserDegradeFeignClient熔断类**: ```java @Override public Result<Map<Long, Integer>> userIsPaidTrack(Long userId, Long albumId, List<Long> needChackTrackIdList) { log.error("[用户服务]提供远程调用方法userIsPaidTrack执行服务降级"); return null; } ``` ### 1.3.2 查询专辑声音列表 在`service-album` 微服务中添加控制器. 获取专辑声音列表时,我们将数据都统一封装到**AlbumTrackListVo**实体类中 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/89 **TrackInfoApiController控制器** ```java /** * 用于小程序端专辑页面展示分页声音列表,动态根据用户展示声音付费标识 * * @param albumId * @param page * @param limit * @return */ @GuiGuLogin(required = false) @Operation(summary = "用于小程序端专辑页面展示分页声音列表,动态根据用户展示声音付费标识") @GetMapping("/trackInfo/findAlbumTrackPage/{albumId}/{page}/{limit}") public Result<Page<AlbumTrackListVo>> getAlbumTrackPage(@PathVariable Long albumId, @PathVariable Integer page, @PathVariable Integer limit) { //1.获取用户ID Long userId = AuthContextHolder.getUserId(); //2.封装分页对象 Page<AlbumTrackListVo> pageInfo = new Page<>(page, limit); //3.调用业务层封装分页对象 pageInfo = trackInfoService.getAlbumTrackPage(pageInfo, albumId, userId); return Result.ok(pageInfo); } ``` **TrackInfoService接口:** ```java /** * 用于小程序端专辑页面展示分页声音列表,动态根据用户展示声音付费标识 * * @param pageInfo MP分页对象 * @param albumId 专辑ID * @param userId 用户ID * @return */ Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, 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:表示没有购买过 如果声音列表不包含,则将显示为付费,否则判断用户是否购买过声音,没有购买过设置为付费 ```java @Autowired private UserFeignClient userFeignClient; /** * 分页获取专辑下声音列表,动态根据用户情况展示声音付费标识 * * @param userId 用户ID * @param albumId 专辑ID * @param pageInfo 分页对象 * @return */ @Override public Page<AlbumTrackListVo> getAlbumTrackPage(Long userId, Long albumId, Page<AlbumTrackListVo> pageInfo) { //1.根据专辑ID分页获取该专辑下包含声音列表(包含声音统计信息)-默认声音付费标识为false pageInfo = albumInfoMapper.getAlbumTrackPage(pageInfo, albumId); //2.TODO 动态判断当前页中每个声音付费标识 关键点:找出付费情况 //2.根据专辑ID查询专辑信息 AlbumInfo albumInfo = albumInfoMapper.selectById(albumId); Assert.notNull(albumInfo, "专辑:{}不存在", albumId); String payType = albumInfo.getPayType(); //3.处理用户未登录情况 if (userId == null) { //3.1 判断专辑付费类型:VIP免费(0102)或 付费(0103) 除了免费试听外声音都应该设置付费标识 if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(payType) || SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(payType)) { //3.2 获取本页中声音列表,过滤将声音序号大于免费试听集数声音付费标识设置为true pageInfo.getRecords() .stream() .filter(albumTrackVo -> albumTrackVo.getOrderNum() > albumInfo.getTracksForFree()) //过滤获取除免费试听以外声音 .collect(Collectors.toList()) .stream().forEach(albumTrackListVo -> { albumTrackListVo.setIsShowPaidMark(true); }); } } else { //4.处理用户已登录情况 //4.1 远程调用用户服务获取用户信息得到用户身份 UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData(); Assert.notNull(userInfoVo, "用户{}不存在", userId); Integer isVip = userInfoVo.getIsVip(); //4.2 默认设置需要进一步确定购买情况标识:默认false Boolean isNeedCheckPayStatus = false; //4.2.1 如果专辑付费类型 VIP免费 if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(payType)) { //当前用户为普通用户或VIP会员过期 if (isVip.intValue() == 0) { isNeedCheckPayStatus = true; } if (isVip.intValue() == 1 && new Date().after(userInfoVo.getVipExpireTime())) { isNeedCheckPayStatus = true; } } //4.2.2 如果专辑付费类型 付费 if (SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(payType)) { //当前用户为普通用户或VIP会员过期 isNeedCheckPayStatus = true; } if (isNeedCheckPayStatus) { //4.3 进一步确定用户是否购买专辑或声音-远程调用用户服务获取本页中专辑或者声音购买情况 //本页中需要检查购买情况声音列表,过滤掉当前页免费试听声音 List<AlbumTrackListVo> needCheckTrackList = pageInfo.getRecords().stream() .filter(albumTrackListVo -> albumTrackListVo.getOrderNum() > albumInfo.getTracksForFree()) .collect(Collectors.toList()); //本页中需要检查购买情况声音ID列表,过滤掉当前页免费试听声音 List<Long> needCheckTrackIdList = needCheckTrackList.stream() .map(albumTrackListVo -> albumTrackListVo.getTrackId()) .collect(Collectors.toList()); Map<Long, Integer> userPayStatusTrackMap = userFeignClient.userIsPaidTrack(userId, albumId, needCheckTrackIdList).getData(); //4.4 循环当前页中声音列表-跟返回用户购买情况声音集合逐一判断 needCheckTrackList.stream().forEach(needCheckTrack -> { Integer payStatus = userPayStatusTrackMap.get(needCheckTrack.getTrackId()); if (payStatus.intValue() == 0) { //4.5 某个声音用户未购买,将设置付费标识 isShowPaidMark:true needCheckTrack.setIsShowPaidMark(true); } }); } } return pageInfo; } ``` **TrackInfoMapper接口**:条件必须是当前已经开放并且是审核通过状态的数据,并且还需要获取到声音的播放量以及评论数量 ```java /** * 查询指定专辑下包含声音列表 * * @param pageInfo * @param albumId * @return */ Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, @Param("albumId") Long albumId); ``` **TrackInfoMapper.xml** 映射文件 动态SQL ```sql #分页查询指定专辑下包含声音列表(包含统计信息) select * from track_info where album_id = 307; select * from track_info where album_id = 307 and id = 16289; select * from track_stat where track_id = 16289; select ti.id trackId, ti.track_title trackTitle, ti.media_duration mediaDuration, ti.order_num orderNum, ti.create_time createTime, max(if(ts.stat_type='0701', ts.stat_num, 0)) playStatNum, max(if(ts.stat_type='0702', ts.stat_num, 0)) collectStatNum, max(if(ts.stat_type='0703', ts.stat_num, 0)) praiseStatNum, max(if(ts.stat_type='0704', ts.stat_num, 0)) commentStatNum from track_info ti left join track_stat ts on ts.track_id = ti.id where ti.album_id = 307 and ti.is_deleted = 0 group by ti.id order by ti.order_num ``` ```sql <!--分页获取专辑下声音列表--> <select id="getAlbumTrackPage" resultType="com.atguigu.tingshu.vo.album.AlbumTrackListVo"> select ti.id trackId, ti.track_title, ti.media_duration, ti.order_num, ti.create_time, max(if(stat.stat_type='0701', stat.stat_num, 0)) playStatNum, max(if(stat.stat_type='0702', stat.stat_num, 0)) collectStatNum, max(if(stat.stat_type='0703', stat.stat_num, 0)) praiseStatNum, max(if(stat.stat_type='0704', stat.stat_num, 0)) commentStatNum from track_info ti left join track_stat stat on stat.track_id = ti.id where ti.album_id = #{albumId} and ti.is_deleted = 0 group by ti.id order by ti.order_num </select> ``` 测试: - 手动增加用户购买专辑记录:**user_paid_album** - 手动增加用户购买声音记录:**user_paid_track** - 手动修改VIP会员:**user_info** 情况一:未登录情况,专辑付费类型:VIP免费 付费 查看声音列表->试听声音免费+其余都需要展示付费标识 情况二:登录情况 - 普通用户 - 免费 全部免费 - VIP付费 试听声音免费+用户购买过专辑/声音,未购买展示付费标识 - 付费:试听声音免费+用户购买过专辑/声音,未购买展示付费标识 - VIP用户 - 免费 全部免费 - VIP付费 全部免费 - 付费:试听声音免费+用户购买过专辑/声音,未购买展示付费标识 时间处理: ```java @Data @Schema(description = "UserInfoVo") public class UserInfoVo implements Serializable { @Schema(description = "用户id") private Long id; @Schema(description = "微信openId") private String wxOpenId; @Schema(description = "nickname") private String nickname; @Schema(description = "主播用户头像图片") private String avatarUrl; @Schema(description = "用户是否为VIP会员 0:普通用户 1:VIP会员") private Integer isVip; @Schema(description = "当前VIP到期时间,即失效时间") @DateTimeFormat( pattern = "yyyy-MM-dd" ) @JsonFormat( shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "GMT+8" ) @JsonDeserialize private Date vipExpireTime; } ``` # 2、MongoDB文档型数据库 详情见:**第6章 MongoDB入门.md** **播放进度**对应的实体类: ```java @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、声音详情 ![详情-获取播放记录进度](assets/详情-获取播放记录进度.gif) ## 3.1 获取声音播放进度 在播放声音的时候,会有触发一个获取播放进度的控制器!因为页面每隔10s会自动触发一次保存功能,会将数据写入MongoDB中。所以我们直接从MongoDB中获取到上一次声音的播放时间即可! ![](assets/tingshu013.png) > YAPI接口:http://192.168.200.6:3000/project/11/interface/api/71 在 `service-user` 微服务的 **UserListenProcessApiController** 控制器中添加 ```java /** * 获取当前用户收听声音播放进 * * @param trackId * @return */ @GuiGuLogin(required = false) @Operation(summary = "获取当前用户收听声音播放进度") @GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}") public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) { Long userId = AuthContextHolder.getUserId(); if (userId != null) { BigDecimal breakSecond = userListenProcessService.getTrackBreakSecond(userId, trackId); return Result.ok(breakSecond); } return Result.ok(); } ``` **UserListenProcessService接口**: ```java /** * 获取当前用户收听声音播放进度 * @param userId 用户ID * @param trackId 声音ID * @return */ BigDecimal getTrackBreakSecond(Long userId, Long trackId); ``` **UserListenProcessServiceImpl**实现类: ```java package com.atguigu.tingshu.user.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; import com.alibaba.fastjson.JSON; import com.atguigu.tingshu.common.constant.KafkaConstant; import com.atguigu.tingshu.common.constant.RedisConstant; import com.atguigu.tingshu.common.constant.SystemConstant; import com.atguigu.tingshu.common.service.KafkaService; import com.atguigu.tingshu.common.util.MongoUtil; import com.atguigu.tingshu.model.user.UserListenProcess; import com.atguigu.tingshu.user.service.UserListenProcessService; import com.atguigu.tingshu.vo.album.TrackStatMqVo; import com.atguigu.tingshu.vo.user.UserListenProcessVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; import java.util.concurrent.TimeUnit; @Service @SuppressWarnings({"all"}) public class UserListenProcessServiceImpl implements UserListenProcessService { @Autowired private MongoTemplate mongoTemplate; /** * 获取当前用户收听声音播放进度 * * @param userId 用户ID * @param trackId 声音ID * @return */ @Override public BigDecimal getTrackBreakSecond(Long userId, Long trackId) { //1.构建查询条件 Query query = new Query(); query.addCriteria(Criteria.where("userId").is(userId).and("trackId").is(trackId)); //2.执行查询播放进度 UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); if (userListenProcess != null) { return userListenProcess.getBreakSecond(); } return new BigDecimal("0.00"); } } ``` ![image-20240307200116030](assets/image-20240307200116030.png) ## 3.2 更新播放进度 页面每隔10秒左右更新播放进度. 1. 更新播放进度页面会传递 专辑Id ,秒数,声音Id 。后台会将这个三个属性封装到UserListenProcessVo 对象中。然后利用MongoDB进行存储到UserListenProcess实体类中! 2. 为了提高用户快速访问,将用户信息存储到缓存中。先判断当前用户Id 与 声音Id 是否存在,不存在的话才将数据存储到缓存,并且要发送消息给kafka。 3. kafka 监听消息并消费,更新专辑与声音的统计数据。 ### 3.2.1 更新MongoDB ![](assets/tingshu015.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/73 在 **UserListenProcessApiController** 控制器中添加 ```java /** * 更新当前用户收听声音播放进度 * @param userListenProcessVo * @return */ @GuiGuLogin(required = false) @Operation(summary = "更新当前用户收听声音播放进度") @PostMapping("/userListenProcess/updateListenProcess") public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo){ Long userId = AuthContextHolder.getUserId(); if (userId != null) { userListenProcessService.updateListenProcess(userId, userListenProcessVo); } return Result.ok(); } ``` **UserListenProcessService**接口: ```java /** * 更新当前用户收听声音播放进度 * @param userId 用户ID * @param userListenProcessVo 播放进度信息 * @return */ void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo); ``` **UserListenProcessServiceImpl**实现类: ```java @Autowired private RedisTemplate redisTemplate; @Autowired private KafkaService kafkaService; /** * 更新当前用户收听声音播放进度 * * @param userId 用户ID * @param userListenProcessVo 播放进度信息 * @return */ @Override public void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo) { //1.根据用户ID+声音ID获取播放进度 Query query = new Query(); //1.1 设置查询条件 query.addCriteria(Criteria.where("userId").is(userId).and("trackId").is(userListenProcessVo.getTrackId())); //1.2 设置查询第一条记录(避免小程序暂停后恢复播放将积压更新进度请求并发发起,导致新增多条播放进度) query.limit(1); UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); if (userListenProcess == null) { //2.如果播放进度不存在-新增播放进度 userListenProcess = new UserListenProcess(); userListenProcess.setUserId(userId); userListenProcess.setAlbumId(userListenProcessVo.getAlbumId()); userListenProcess.setTrackId(userListenProcessVo.getTrackId()); userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond()); userListenProcess.setIsShow(1); userListenProcess.setCreateTime(new Date()); userListenProcess.setUpdateTime(new Date()); } else { //3.如果播放进度存在-更新进度 userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond()); userListenProcess.setUpdateTime(new Date()); } mongoTemplate.save(userListenProcess, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); //4.采用Redis提供set k v nx ex 确保在规定时间内(24小时/当日内)播放进度统计更新1次 String key = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + userId + ":" + userListenProcessVo.getTrackId(); long ttl = DateUtil.endOfDay(new Date()).getTime() - System.currentTimeMillis(); Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, userListenProcess.getTrackId(), ttl, TimeUnit.MILLISECONDS); if (flag) { //5.如果是首次更新播放进度,发送消息到Kafka话题 //5.1 构建更新声音播放进度MQVO对象 TrackStatMqVo mqVo = new TrackStatMqVo(); //生成业务唯一标识,消费者端(专辑服务、搜索服务)用来做幂等性处理,确保一个消息只能只被处理一次 mqVo.setBusinessNo(IdUtil.fastSimpleUUID()); mqVo.setAlbumId(userListenProcessVo.getAlbumId()); mqVo.setTrackId(userListenProcessVo.getTrackId()); mqVo.setStatType(SystemConstant.TRACK_STAT_PLAY); mqVo.setCount(1); //5.2 发送消息到更新声音统计话题中 kafkaService.sendMessage(KafkaConstant.QUEUE_TRACK_STAT_UPDATE, JSON.toJSONString(mqVo)); } } ``` ### 3.2.2 更新MySQL统计信息 在`service-album` 微服务中添加监听消息: ```java package com.atguigu.tingshu.album.receiver; import com.alibaba.fastjson.JSON; import com.atguigu.tingshu.album.service.AlbumInfoService; import com.atguigu.tingshu.common.constant.KafkaConstant; import com.atguigu.tingshu.vo.album.TrackStatMqVo; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; /** * @author: atguigu * @create: 2023-12-19 15:31 */ @Slf4j @Component public class AlbumReceiver { @Autowired private AlbumInfoService albumInfoService; /** * 监听到更新声音统计信息 * 1.考虑消息幂等性 2.是否需要事务管理 * * @param record */ @KafkaListener(topics = KafkaConstant.QUEUE_TRACK_STAT_UPDATE) public void updateTrackStat(ConsumerRecord<String, String> record) { String value = record.value(); if (StringUtils.isNotBlank(value)) { log.info("[专辑服务],监听到更新声音统计消息:{}", value); TrackStatMqVo mqVo = JSON.parseObject(value, TrackStatMqVo.class); albumInfoService.updateTrackStat(mqVo); } } } ``` 在**TrackInfoService** 中添加接口 ```java /** * MQ监听更新声音统计信息 * @param mqVo */ void updateTrackStat(TrackStatMqVo mqVo); ``` 在**TrackInfoServiceImpl** 中添加实现 ```java @Autowired private AlbumStatMapper albumStatMapper; @Autowired private RedisTemplate redisTemplate; /** * MQ监听更新声音统计信息(包含:播放、收藏、点赞、评论) * * @param mqVo */ @Override @Transactional(rollbackFor = Exception.class) public void updateTrackStat(TrackStatMqVo mqVo) { //1.做幂等性处理,统一个消息只处理一次 采用set k(业务消息唯一标识) v NX EX String key = "mq:" + mqVo.getBusinessNo(); try { Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, mqVo.getBusinessNo(), 1, TimeUnit.HOURS); if (flag) { //2.更新声音统计信息 trackStatMapper.updateStat(mqVo.getTrackId(), mqVo.getStatType(), mqVo.getCount()); //3.更新专辑统计信息(播放量、评论量只要声音+1,对应专辑也得+1) if (SystemConstant.TRACK_STAT_PLAY.equals(mqVo.getStatType())) { albumStatMapper.updateStat(mqVo.getAlbumId(), SystemConstant.ALBUM_STAT_PLAY, mqVo.getCount()); } if (SystemConstant.TRACK_STAT_COMMENT.equals(mqVo.getStatType())) { albumStatMapper.updateStat(mqVo.getAlbumId(), SystemConstant.ALBUM_STAT_COMMENT, mqVo.getCount()); } } } catch (Exception e) { //如果更新数据库发送异常,事务会进行回滚,下次再次投递消息允许继续处理统一个消息 redisTemplate.delete(key); throw new RuntimeException(e); } } ``` **TrackStatMapper**.java 添加方法 ```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 public interface TrackStatMapper extends BaseMapper<TrackStat> { /** * 更新声音统计信息 * @param trackId 声音ID * @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 接口添加 ```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 专辑上次播放专辑声音 ![image-20231012111356796](assets/image-20231012111356796.png) 我们需要根据用户Id 来获取播放记录 ,需要获取到专辑Id 与声音Id 封装到map中然后返回数据即可! > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/83 控制器 **UserListenProcessApiController** ```java /** * 获取当前用户上次播放专辑声音记录 * * @return */ @GuiGuLogin @GetMapping("/userListenProcess/getLatelyTrack") public Result<Map<String, Long>> getLatelyTrack() { Long userId = AuthContextHolder.getUserId(); return Result.ok(userListenProcessService.getLatelyTrack(userId)); } ``` **UserListenProcessService接口:** ```java /** * 获取用户最近一次播放记录 * @param userId * @return */ Map<String, Long> getLatelyTrack(Long userId); ``` **UserListenProcessServiceImpl实现类**: ```java /** * 获取用户最近一次播放记录 * * @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; } ``` ## 3.4 获取声音统计信息 ![](assets/tingshu014.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/75 统计声音需要更新的数据如下,我们将数据封装到一个实体类中便于操作 ```java @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** 控制器中添加 ```java /** * 根据声音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接口**: ```java /** * 根据声音ID,查询声音统计信息 * @param trackId * @return */ TrackStatVo getTrackStatVo(Long trackId); ``` **TrackInfoServiceImpl实现类**: ```java /** * 根据声音ID,查询声音统计信息 * @param trackId * @return */ @Override public TrackStatVo getTrackStatVo(Long trackId) { return trackInfoMapper.getTrackStatVo(trackId); } ``` **TrackInfoMapper**.java ```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** ```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 ``` # 4、更新Redis排行榜 手动调用一次更新,查看排行榜。后续会整合xxl-job 分布式定时任务调度框架做定时调用。 ![详情-排行榜](assets/详情-排行榜.gif) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/77 `service-album `微服务中**BaseCategoryApiController**控制器中添加 ```java /** * 查询所有一级分类列表 * @return */ @Operation(summary = "查询所有一级分类列表") @GetMapping("/category/findAllCategory1") public Result<List<BaseCategory1>> getAllCategory1() { return Result.ok(baseCategoryService.list()); } ``` **AlbumFeignClient** ```java /** * 查询所有一级分类列表 * @return */ @GetMapping("/category/findAllCategory1") public Result<List<BaseCategory1>> getAllCategory1(); ``` **AlbumDegradeFeignClient熔断类**: ```java @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** 中添加控制器 ```java /** * 为定时更新首页排行榜提供调用接口 * @return */ @Operation(summary = "为定时更新首页排行榜提供调用接口") @GetMapping("/albumInfo/updateLatelyAlbumRanking") public Result updateLatelyAlbumRanking(){ searchService.updateLatelyAlbumRanking(); return Result.ok(); } ``` **SearchService**接口: ```java /** * 获取不同分类下不同排序方式榜单专辑列表 */ void updateLatelyAlbumRanking(); ``` **SearchServiceImpl实现类:** ```java @Autowired private RedisTemplate redisTemplate; /** * 获取不同分类下不同排序方式榜单专辑列表 */ @Override public void updateLatelyAlbumRanking() { try { //1.远程调用专辑服务获取所有1级分类列表 List<BaseCategory1> category1List = albumFeignClient.getdAllCategory1().getData(); Assert.notNull(category1List, "一级分类为空"); //2.循环遍历1级分类列表,获取该分类下5种不同排序方式榜单专辑 for (BaseCategory1 baseCategory1 : category1List) { Long category1Id = baseCategory1.getId(); //3.在处理当前1级分类中,再次循环5种不同排序方式得到具体榜单数据 //3.1 声明排序方式数组 String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"}; for (String rankingDimension : rankingDimensionArray) { //3.2 调用ES检索接口获取榜单数据 SearchResponse<AlbumInfoIndex> searchResponse = elasticsearchClient.search( s -> s.index(INDEX_NAME) .query(q -> q.term(t -> t.field("category1Id").value(category1Id))) .sort(sort -> sort.field(f -> f.field(rankingDimension).order(SortOrder.Desc))) .size(10) , AlbumInfoIndex.class ); //3.3 获取当前分类下某个排序方式榜单专辑列表 List<Hit<AlbumInfoIndex>> hits = searchResponse.hits().hits(); if (CollectionUtil.isNotEmpty(hits)) { List<AlbumInfoIndex> list = hits.stream().map(hit -> hit.source()).collect(Collectors.toList()); //4.将榜单专辑列表存入Redis-Hash中 //4.1 声明Redis排行榜Hash接口 Key 形式:前缀+1级分类ID field:排序方式 val:榜单列表 String key = RedisConstant.RANKING_KEY_PREFIX + category1Id; //4.2 将当前分类榜单数据放入Redis中 redisTemplate.opsForHash().put(key, rankingDimension, list); } } } } catch (Exception e) { log.error("[搜索服务]更新排行榜异常:{}", e); throw new RuntimeException(e); } } ``` # 5、获取排行榜 ![image-20231012114420751](assets/image-20231012114420751.png) 点击排行榜的时候,能看到获取排行榜的地址 排行榜:key=ranking:category1Id field = hotScore 或 playStatNum 或 subscribeStatNum 或 buyStatNum 或albumCommentStatNum value=List<AlbumInfoIndexVo> ![](assets/tingshu016.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/81 **SearchApiController** 控制器中添加 ```java /** * 获取指定1级分类下不同排序方式榜单列表-从Redis中获取 * @param category1Id * @param dimension * @return */ @Operation(summary = "获取指定1级分类下不同排序方式榜单列表") @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**接口: ```java /** * 获取指定1级分类下不同排序方式榜单列表-从Redis中获取 * @param category1Id * @param dimension * @return */ List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension); ``` **SearchServiceImpl实现类**: ```java /** * 获取指定1级分类下不同排序方式榜单列表-从Redis中获取 * * @param category1Id * @param dimension * @return */ @Override public List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension) { //1.构建分类排行榜Hash结构Key String key = RedisConstant.RANKING_KEY_PREFIX + category1Id; //2.获取Redis中hash结构中value Boolean flag = redisTemplate.opsForHash().hasKey(key, dimension); if (flag) { List<AlbumInfoIndex> list = (List) redisTemplate.opsForHash().get(key, dimension); return list; } return null; } ``` 结合上面的内容介绍断点续播的实现思路,不需要给出代码

<think>我们之前已经讨论了断点续播的实现思路,现在结合提供的代码,我将详细解释断点续播的实现思路,而不给出具体代码。 ### 断点续播实现思路 断点续播的核心是记录用户对每个声音的播放进度,并在用户再次播放时从上次中断的位置继续播放。以下是实现断点续播的关键步骤和设计考虑: #### 1. **播放进度存储** - **存储位置**:使用MongoDB存储用户的播放进度。MongoDB是一个文档型数据库,适合存储非结构化的数据,如播放进度记录。 - **数据结构**:每个用户的播放进度记录包含以下字段: - `userId`:用户ID - `albumId`:专辑ID - `trackId`:声音ID - `breakSecond`:中断的秒数(播放位置) - `updateTime`:更新时间 - **分表策略**:根据用户ID对播放进度表进行分表,避免单表过大,提高查询效率。例如,表名可以设计为`user_listen_process_{userId}`。 #### 2. **更新播放进度** - **更新时机**:客户端每隔一段时间(例如10秒)向服务端发送一次播放进度更新请求。 - **更新逻辑**: - 根据用户ID和声音ID查询MongoDB中是否已有播放进度记录。 - 如果不存在,创建新的播放进度记录。 - 如果已存在,更新`breakSecond`和`updateTime`字段。 - **并发控制**:为了避免小程序暂停后恢复播放时积压的更新请求导致多条记录,查询时使用`limit(1)`确保只处理一条记录。 #### 3. **防止重复统计播放行为** - **问题**:每次更新播放进度时,如果直接发送统计消息,会导致同一用户在短时间内多次更新进度被重复统计。 - **解决方案**:使用Redis的`SETNX`命令(设置键值,仅当键不存在时)实现24小时内同一用户对同一声音的播放行为只统计一次。 - 生成Redis的key:`user_track_repeat_stat:userId:trackId`。 - 设置过期时间为当天剩余时间(毫秒),确保key在当天结束时自动过期。 - 如果`SETNX`返回`true`(表示首次设置),则发送Kafka消息更新声音的播放统计;否则,不发送。 #### 4. **播放进度查询** - 当用户再次播放同一声音时,客户端请求服务端获取上次的播放进度。 - 服务端根据用户ID和声音ID查询MongoDB中的播放进度记录,返回`breakSecond`字段的值。 - 如果查询不到,返回0(从开头播放)。 #### 5. **异常处理** - **MongoDB不可用**:如果MongoDB查询失败,可以尝试从Redis缓存中获取最近一次的播放进度(如果之前有缓存的话)。 - **Kafka消息发送失败**:实现重试机制,最多重试3次,每次重试间隔时间指数级增加(1秒、2秒、4秒)。如果重试失败,将消息存入死信队列。 #### 6. **跨设备同步** - 当用户更换设备时,需要同步播放进度。 - 实现方案: - 用户登录新设备时,从主设备获取最新播放进度。 - 更新当前设备的播放进度。 - 通过Kafka广播同步事件,通知其他设备更新进度。 #### 7. **性能优化** - **异步更新**:播放进度更新操作使用异步处理,避免阻塞主线程。 - **缓存**:将最近播放的进度缓存在Redis中,减少对MongoDB的查询压力。 ### 总结 断点续播功能通过结合MongoDB持久化存储播放进度、Redis实现防重统计和缓存,以及Kafka进行异步消息传递,实现了用户播放进度的记录、更新和查询。同时,通过异常处理和跨设备同步机制,确保了功能的可靠性和用户体验的一致性。 §§相关问题§§: 1. 为什么选择MongoDB来存储播放进度,而不是传统的关系型数据库? 2. 如何确保在并发更新播放进度时不会出现数据不一致? 3. 在播放进度更新时,如何防止同一用户的多次更新被重复统计? 4. 如果用户更换设备,如何实现播放进度的同步?</think>### 断点续播实现思路详解 #### 一、核心设计原理 断点续播的核心是记录用户播放中断位置,并在下次播放时恢复该位置。系统需要解决三个关键问题: 1. **进度持久化**:可靠存储用户的播放位置 2. **进度同步**:多设备间播放进度同步 3. **行为统计**:避免重复统计播放行为 #### 二、技术实现方案 ##### 1. 进度存储机制 - **数据结构**: - 用户ID、专辑ID、声音ID - 中断秒数(breakSecond) - 更新时间(用于确定最新进度) - **存储选择**: - 使用MongoDB文档数据库存储(适合非结构化数据) - 按用户ID分表存储(避免单表过大) - 查询优化:`limit(1)`确保只获取最新记录 ##### 2. 进度更新流程 1. **查询现有进度**: - 根据用户ID+声音ID查询MongoDB - 使用`findOne`获取最近一条记录 2. **记录处理**: - 不存在记录 → 创建新文档(设置初始值) - 存在记录 → 更新breakSecond和updateTime 3. **持久化操作**: - 使用`mongoTemplate.save()`保存到对应集合 4. **防重统计**: - 使用Redis SETNX实现24小时内只统计一次 - Key设计:`user_track_repeat_stat:{userId}:{trackId}` - TTL设置为当日剩余毫秒数 ##### 3. 播放行为统计 1. **首次更新触发**: - 当SETNX返回true时(表示首次设置) - 构建Kafka消息体(含业务唯一ID) 2. **消息内容**: - 专辑ID、声音ID - 统计类型(播放) - 计数增量(+1) 3. **消息发送**: - 发送到TRACK_STAT_UPDATE队列 - 消费者端做幂等处理 ##### 4. 进度查询流程 1. **请求参数**: - 用户ID(从认证上下文获取) - 声音ID(路径参数) 2. **数据库查询**: - 根据用户ID+声音ID查询MongoDB - 按更新时间倒序取第一条 3. **结果返回**: - 存在记录 → 返回breakSecond - 无记录 → 返回0(从头播放) #### 三、关键优化点 1. **并发处理**: - 使用MongoDB的原子操作`findAndModify` - 避免小程序暂停/恢复导致多条进度记录 2. **跨设备同步**: - 设备变更时触发同步事件 - 通过Kafka广播同步消息 - 各设备更新本地进度存储 3. **异常处理**: - MongoDB不可用时降级到Redis缓存 - Kafka消息发送失败时重试+死信队列 - 时钟偏移增加10%缓冲时间 4. **统计精确性**: - 业务唯一ID确保消息幂等性 - Redis+MySQL双重防重机制 - 分布式锁保证统计准确性 #### 四、架构优势 1. **存储分层**: - MongoDB:灵活存储播放进度 - Redis:高效防重控制 - MySQL:持久化统计数据 2. **解耦设计**: ```mermaid graph LR A[客户端] -->|更新进度| B[播放服务] B -->|写进度| C[MongoDB] B -->|防重检查| D[Redis] B -->|统计事件| E[Kafka] E --> F[统计服务] E --> G[推荐服务] ``` 3. **扩展能力**: - 支持历史进度回溯 - 可扩展播放行为分析 - 适配多终端类型

LambdaQueryWrapper去重查询

LambdaQueryWrapper是MyBatis-Plus提供的一种查询构造器,它可以通过Lambda表达式来构建查询条件,从而简化了代码的编写。在LambdaQueryWrapper中,我们可以使用selectDistinct()方法来实现去重查询。具体实现方式如下: LambdaQueryWrapper&lt;T&gt; wrapper = new LambdaQueryWrapper&lt;&gt;(); wrapper.selectDistinct("column1", "column2", ...); wrapper.eq(T::getField1, value1) .ne(T::getField2, value2) .in(T::getField3, list) .orderByAsc(T::getField4); 其中,selectDistinct()方法用于指定需要去重的列,eq()、ne()、in()等方法用于指定查询条件,orderByAsc()方法用于指定排序方式。最后,我们可以通过调用MyBatis-Plus提供的count()方法来获取查询结果的数量。
阅读全文

相关推荐

@Override public Page<Order> getOrders(String customerName, Integer page, Integer limit, HttpServletRequest request) { UserVO orderVO = (UserVO) request.getSession().getAttribute(UserConstant.LOGIN_INFO); if (orderVO == null || !"0".equals(orderVO.getRole())) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } if (limit > 100) { throw new BusinessException(ErrorCode.OPERATION_ERROR); } Page<Order> orderPage = new Page<>(page, limit); LambdaQueryWrapper<Order> orderLambdaQueryWrapper = new LambdaQueryWrapper<Order>() .like(StringUtils.hasText(customerName), Order::getCustomerName, customerName) .orderByDesc(Order::getId); Page<Order> selectPage = orderMapper.selectPage(orderPage, orderLambdaQueryWrapper); return selectPage; }上面是我的代码,报错信息如下:### Error querying database. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order' at line 1 ### The error may exist in com/ease/book/mapper/OrderMapper.java (best guess) ### The error may involve defaultParameterMap ### The error occurred while setting parameters ### SQL: SELECT COUNT(*) AS total FROM order ### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order' at line 1 ] 21:48:48.300 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - Using @ExceptionHandler com.ease.book.exception.GlobalExceptionHandler#runtimeExceptionHandler(RuntimeException) 21:48:48.304 [http-nio-8080-exec-1] ERROR com.ease.book.exception.GlobalExceptionHandler - RuntimeException org.springframework.jdbc.BadSqlGrammarException: ### Error querying database. Cause: java.sql.SQLSyntaxErrorExcepti

{ String userId = Optional.ofNullable(param.getUserId()).orElse(LoginUserUtil.getLoginUserId()); Integer queryType = Optional.ofNullable(param.getQueryType()).orElse(1); Integer createTimeSort = Optional.ofNullable(param.getCreateTimeSort()).orElse(0); List<TraineeUserNoteDTO> allNoteList = new ArrayList<>(); if (queryType == 1){ //查询我发布的笔记 allNoteList = traineeUserNoteMapper.getMyNotePage(createTimeSort, userId); } else if (queryType == 2) { //查询我点赞的笔记 List<ZshLikeDTO> zshLikeDTOS = traineeLikeMapper.selectList(new LambdaQueryWrapper<ZshLikeDTO>() .eq(ZshLikeDTO::getUserId, userId) .eq(ZshLikeDTO::getObjectType, 4)); if (CollectionUtils.isEmpty(zshLikeDTOS)){ return new Page<>(param.getCurrent(), param.getSize()); } List<String> notes = zshLikeDTOS.stream().map(ZshLikeDTO::getObjectId).collect(Collectors.toList()); List<ZshUserNoteDTO> zshNoteList = traineeUserNoteMapper.selectBatchIds(notes); allNoteList = zshNoteList.stream().map(zshUserNoteDTO -> { TraineeUserNoteDTO traineeUserNoteDTO = new TraineeUserNoteDTO(); BeanUtils.copyProperties(zshUserNoteDTO, traineeUserNoteDTO); return traineeUserNoteDTO; }).collect(Collectors.toList()); } //获取笔记点赞数,需获取全部笔记及点赞数才可排序 List<String> noteIdList = allNoteList.stream() .map(TraineeUserNoteDTO::getId) .filter(Objects::nonNull) .collect(Collectors.toList()); if (noteIdList.isEmpty()) { return new Page<>(param.getCurrent(), param.getSize(), 0); } Map<String, Integer> mapLikeCount = traineeLikeMapper.selectLikeCount(noteIdList, 4) .stream() .collect(Collectors.toMap(IdCountModel::getId, IdCountModel::getCount)); List<MyNoteVO> allNoteLikeList = allNoteList.stream().map(dto -> { MyNoteVO myNoteVO = new MyNoteVO(); BeanUtils.copyProperties(dto, myNoteVO); //设置笔记点赞数 myNoteVO.setLikeCount(mapLikeCount.getOrDefault(myNoteVO.getId(), 0)); return myNoteVO; }).collect(Collectors.toList()); //根据点赞数排序 if (param.getLikeCountSort() != null){ if (param.getLikeCountSort() == 1

最新推荐

recommend-type

C# Socket通信源码:多连接支持与断线重连功能的物联网解决方案

内容概要:本文介绍了一套基于C#编写的Socket服务器与客户端通信源码,源自商业级物联网项目。这套代码实现了双Socket机制、多连接支持以及断线重连功能,适用于各类C#项目(如MVC、Winform、控制台、Webform)。它通过简单的静态类调用即可获取客户端传输的数据,并内置了接收和发送数据缓冲队列,确保数据传输的稳定性。此外,代码提供了数据读取接口,但不涉及具体的数据处理逻辑。文中详细展示了服务端和客户端的基本配置与使用方法,强调了在实际应用中需要注意的问题,如避免主线程执行耗时操作以防内存膨胀。 适合人群:具备基本C#编程能力的研发人员,尤其是对Socket通信有一定了解并希望快速集成相关功能到现有项目中的开发者。 使用场景及目标:① 需要在短时间内为C#项目增加稳定的Socket通信功能;② 实现多设备间的数据交换,特别是对于智能家居、工业传感器等物联网应用场景。 其他说明:虽然该代码能够满足大多数中小型项目的通信需求,但对于需要高性能、低延迟的金融级交易系统则不太合适。同时,代码并未采用异步技术,因此在面对海量连接时可能需要进一步优化。
recommend-type

掌握XFireSpring整合技术:HELLOworld原代码使用教程

标题:“xfirespring整合使用原代码”中提到的“xfirespring”是指将XFire和Spring框架进行整合使用。XFire是一个基于SOAP的Web服务框架,而Spring是一个轻量级的Java/Java EE全功能栈的应用程序框架。在Web服务开发中,将XFire与Spring整合能够发挥两者的优势,例如Spring的依赖注入、事务管理等特性,与XFire的简洁的Web服务开发模型相结合。 描述:“xfirespring整合使用HELLOworld原代码”说明了在这个整合过程中实现了一个非常基本的Web服务示例,即“HELLOworld”。这通常意味着创建了一个能够返回"HELLO world"字符串作为响应的Web服务方法。这个简单的例子用来展示如何设置环境、编写服务类、定义Web服务接口以及部署和测试整合后的应用程序。 标签:“xfirespring”表明文档、代码示例或者讨论集中于XFire和Spring的整合技术。 文件列表中的“index.jsp”通常是一个Web应用程序的入口点,它可能用于提供一个用户界面,通过这个界面调用Web服务或者展示Web服务的调用结果。“WEB-INF”是Java Web应用中的一个特殊目录,它存放了应用服务器加载的Servlet类文件和相关的配置文件,例如web.xml。web.xml文件中定义了Web应用程序的配置信息,如Servlet映射、初始化参数、安全约束等。“META-INF”目录包含了元数据信息,这些信息通常由部署工具使用,用于描述应用的元数据,如manifest文件,它记录了归档文件中的包信息以及相关的依赖关系。 整合XFire和Spring框架,具体知识点可以分为以下几个部分: 1. XFire框架概述 XFire是一个开源的Web服务框架,它是基于SOAP协议的,提供了一种简化的方式来创建、部署和调用Web服务。XFire支持多种数据绑定,包括XML、JSON和Java数据对象等。开发人员可以使用注解或者基于XML的配置来定义服务接口和服务实现。 2. Spring框架概述 Spring是一个全面的企业应用开发框架,它提供了丰富的功能,包括但不限于依赖注入、面向切面编程(AOP)、数据访问/集成、消息传递、事务管理等。Spring的核心特性是依赖注入,通过依赖注入能够将应用程序的组件解耦合,从而提高应用程序的灵活性和可测试性。 3. XFire和Spring整合的目的 整合这两个框架的目的是为了利用各自的优势。XFire可以用来创建Web服务,而Spring可以管理这些Web服务的生命周期,提供企业级服务,如事务管理、安全性、数据访问等。整合后,开发者可以享受Spring的依赖注入、事务管理等企业级功能,同时利用XFire的简洁的Web服务开发模型。 4. XFire与Spring整合的基本步骤 整合的基本步骤可能包括添加必要的依赖到项目中,配置Spring的applicationContext.xml,以包括XFire特定的bean配置。比如,需要配置XFire的ServiceExporter和ServicePublisher beans,使得Spring可以管理XFire的Web服务。同时,需要定义服务接口以及服务实现类,并通过注解或者XML配置将其关联起来。 5. Web服务实现示例:“HELLOworld” 实现一个Web服务通常涉及到定义服务接口和服务实现类。服务接口定义了服务的方法,而服务实现类则提供了这些方法的具体实现。在XFire和Spring整合的上下文中,“HELLOworld”示例可能包含一个接口定义,比如`HelloWorldService`,和一个实现类`HelloWorldServiceImpl`,该类有一个`sayHello`方法返回"HELLO world"字符串。 6. 部署和测试 部署Web服务时,需要将应用程序打包成WAR文件,并部署到支持Servlet 2.3及以上版本的Web应用服务器上。部署后,可以通过客户端或浏览器测试Web服务的功能,例如通过访问XFire提供的服务描述页面(WSDL)来了解如何调用服务。 7. JSP与Web服务交互 如果在应用程序中使用了JSP页面,那么JSP可以用来作为用户与Web服务交互的界面。例如,JSP可以包含JavaScript代码来发送异步的AJAX请求到Web服务,并展示返回的结果给用户。在这个过程中,JSP页面可能使用XMLHttpRequest对象或者现代的Fetch API与Web服务进行通信。 8. 项目配置文件说明 项目配置文件如web.xml和applicationContext.xml分别在Web应用和服务配置中扮演关键角色。web.xml负责定义Web组件,比如Servlet、过滤器和监听器,而applicationContext.xml则负责定义Spring容器中的bean,包括数据源、事务管理器、业务逻辑组件和服务访问器等。 总之,通过上述整合使用原代码的知识点,可以深入理解XFire与Spring框架的结合使用,以及如何开发和部署基本的Web服务。这些技术知识有助于进行更高层次的Web服务开发,以及在复杂的IT环境中灵活运用各种框架和工具。
recommend-type

【Unity2018汉化大揭秘】:一步到位优化中文用户体验

# 摘要 本论文详细介绍了Unity2018汉化项目的实施过程,从理论准备到实践步骤,再到后期的测试与优化,最终分享了汉化经验和心得。首先,强调了汉化项目对于用户体验提升和产品国际化的重要性,并分析了汉化资源的分类、识别与管理方法。接着,详细阐述了汉化工具的选择、脚本编写、资源手动调整等实践步骤,以及汉化质量测试、体验优化和项目维护更新的重要性。论文最后通过案例研究,分析了汉化中的挑战和应对策略,并预测了汉化技术未来的发展方向。整个汉化过程不仅涉及技术层面的操作,还包含了深入的理论研究和实践心得分享,对提升汉化项目的效率和质量具有指导意义。 # 关键字 Unity汉化;用户体验;国际化;资源
recommend-type

iPhone

<think>我们注意到用户意图不明确,但提到了“照片提取”和“其他功能帮助”。因此,我们需要通过搜索来获取关于iPhone照片提取的常见方法以及其他可能的功能帮助。由于用户问题比较宽泛,我们将重点放在照片提取上,因为这是明确提到的关键词。同时,我们也会考虑一些其他常用功能的帮助。首先,针对照片提取,可能涉及从iPhone导出照片、从备份中提取照片、或者从损坏的设备中恢复照片等。我们将搜索这些方面的信息。其次,关于其他功能帮助,我们可以提供一些常见问题的快速指南,如电池优化、屏幕时间管理等。根据要求,我们需要将答案组织为多个方法或步骤,并在每个步骤间换行。同时,避免使用第一人称和步骤词汇。由于
recommend-type

驾校一点通软件:提升驾驶证考试通过率

标题“驾校一点通”指向的是一款专门为学员考取驾驶证提供帮助的软件,该软件强调其辅助性质,旨在为学员提供便捷的学习方式和复习资料。从描述中可以推断出,“驾校一点通”是一个与驾驶考试相关的应用软件,这类软件一般包含驾驶理论学习、模拟考试、交通法规解释等内容。 文件标题中的“2007”这个年份标签很可能意味着软件的最初发布时间或版本更新年份,这说明了软件具有一定的历史背景和可能经过了多次更新,以适应不断变化的驾驶考试要求。 压缩包子文件的文件名称列表中,有以下几个文件类型值得关注: 1. images.dat:这个文件名表明,这是一个包含图像数据的文件,很可能包含了用于软件界面展示的图片,如各种标志、道路场景等图形。在驾照学习软件中,这类图片通常用于帮助用户认识和记忆不同交通标志、信号灯以及驾驶过程中需要注意的各种道路情况。 2. library.dat:这个文件名暗示它是一个包含了大量信息的库文件,可能包含了法规、驾驶知识、考试题库等数据。这类文件是提供给用户学习驾驶理论知识和准备科目一理论考试的重要资源。 3. 驾校一点通小型汽车专用.exe:这是一个可执行文件,是软件的主要安装程序。根据标题推测,这款软件主要是针对小型汽车驾照考试的学员设计的。通常,小型汽车(C1类驾照)需要学习包括车辆构造、基础驾驶技能、安全行车常识、交通法规等内容。 4. 使用说明.html:这个文件是软件使用说明的文档,通常以网页格式存在,用户可以通过浏览器阅读。使用说明应该会详细介绍软件的安装流程、功能介绍、如何使用软件的各种模块以及如何通过软件来帮助自己更好地准备考试。 综合以上信息,我们可以挖掘出以下几个相关知识点: - 软件类型:辅助学习软件,专门针对驾驶考试设计。 - 应用领域:主要用于帮助驾考学员准备理论和实践考试。 - 文件类型:包括图片文件(images.dat)、库文件(library.dat)、可执行文件(.exe)和网页格式的说明文件(.html)。 - 功能内容:可能包含交通法规知识学习、交通标志识别、驾驶理论学习、模拟考试、考试题库练习等功能。 - 版本信息:软件很可能最早发布于2007年,后续可能有多个版本更新。 - 用户群体:主要面向小型汽车驾照考生,即C1类驾照学员。 - 使用方式:用户需要将.exe安装文件进行安装,然后根据.html格式的使用说明来熟悉软件操作,从而利用images.dat和library.dat中的资源来辅助学习。 以上知识点为从给定文件信息中提炼出来的重点,这些内容对于了解“驾校一点通”这款软件的功能、作用、使用方法以及它的发展历史都有重要的指导意义。
recommend-type

【DFLauncher自动化教程】:简化游戏启动流程,让游戏体验更流畅

# 摘要 DFLauncher是一个功能丰富的游戏启动和管理平台,本论文将介绍其安装、基础使用、高级设置、社区互动以及插件开发等方面。通过对配置文件的解析、界面定制、自动化功能的实现、高级配置选项、安全性和性能监控的详细讨论,本文阐述了DFLauncher如何帮助用户更高效地管理和优化游戏环境。此外,本文还探讨了DFLauncher社区的资源分享、教育教程和插件开发等内容,
recommend-type

自适应卡尔曼滤波是什么意思

<think>嗯,用户这次想深入了解自适应卡尔曼滤波(AKF)的概念和原理。从对话历史看,用户之前研究过相机自动对焦系统的降噪技术,其中提到过自适应卡尔曼滤波的应用场景。用户可能是个工程师或研究者,正在探索信号处理在具体场景的实现细节。 用户提供的三篇参考文献很有价值:[1]是基础理论综述,[2]聚焦多传感器场景,[3]讨论噪声协方差自适应方法。需要特别注意相机AF系统的特殊需求——实时性要求高(每秒数十次对焦计算)、噪声环境复杂(机械振动/弱光干扰),这些在解释原理时要结合具体案例。 技术要点需要分层解析:先明确标准卡尔曼滤波的局限(固定噪声参数),再展开自适应机制。对于相机AF场景,重
recommend-type

EIA-CEA 861B标准深入解析:时间与EDID技术

EIA-CEA 861B标准是美国电子工业联盟(Electronic Industries Alliance, EIA)和消费电子协会(Consumer Electronics Association, CEA)联合制定的一个技术规范,该规范详细规定了视频显示设备和系统之间的通信协议,特别是关于视频显示设备的时间信息(timing)和扩展显示识别数据(Extended Display Identification Data,简称EDID)的结构与内容。 在视频显示技术领域,确保不同品牌、不同型号的显示设备之间能够正确交换信息是至关重要的,而这正是EIA-CEA 861B标准所解决的问题。它为制造商提供了一个统一的标准,以便设备能够互相识别和兼容。该标准对于确保设备能够正确配置分辨率、刷新率等参数至关重要。 ### 知识点详解 #### EIA-CEA 861B标准的历史和重要性 EIA-CEA 861B标准是随着数字视频接口(Digital Visual Interface,DVI)和后来的高带宽数字内容保护(High-bandwidth Digital Content Protection,HDCP)等技术的发展而出现的。该标准之所以重要,是因为它定义了电视、显示器和其他显示设备之间如何交互时间参数和显示能力信息。这有助于避免兼容性问题,并确保消费者能有较好的体验。 #### Timing信息 Timing信息指的是关于视频信号时序的信息,包括分辨率、水平频率、垂直频率、像素时钟频率等。这些参数决定了视频信号的同步性和刷新率。正确配置这些参数对于视频播放的稳定性和清晰度至关重要。EIA-CEA 861B标准规定了多种推荐的视频模式(如VESA标准模式)和特定的时序信息格式,使得设备制造商可以参照这些标准来设计产品。 #### EDID EDID是显示设备向计算机或其他视频源发送的数据结构,包含了关于显示设备能力的信息,如制造商、型号、支持的分辨率列表、支持的视频格式、屏幕尺寸等。这种信息交流机制允许视频源设备能够“了解”连接的显示设备,并自动设置最佳的输出分辨率和刷新率,实现即插即用(plug and play)功能。 EDID的结构包含了一系列的块(block),其中定义了包括基本显示参数、色彩特性、名称和序列号等在内的信息。该标准确保了这些信息能以一种标准的方式被传输和解释,从而简化了显示设置的过程。 #### EIA-CEA 861B标准的应用 EIA-CEA 861B标准不仅适用于DVI接口,还适用于HDMI(High-Definition Multimedia Interface)和DisplayPort等数字视频接口。这些接口技术都必须遵循EDID的通信协议,以保证设备间正确交换信息。由于标准的广泛采用,它已经成为现代视频信号传输和显示设备设计的基础。 #### EIA-CEA 861B标准的更新 随着技术的进步,EIA-CEA 861B标准也在不断地更新和修订。例如,随着4K分辨率和更高刷新率的显示技术的发展,该标准已经扩展以包括支持这些新技术的时序和EDID信息。任何显示设备制造商在设计新产品时,都必须考虑最新的EIA-CEA 861B标准,以确保兼容性。 #### 结论 EIA-CEA 861B标准是电子显示领域的一个重要规范,它详细定义了视频显示设备在通信时所使用的信号时序和设备信息的格式。该标准的存在,使得不同厂商生产的显示设备可以无缝连接和集成,极大地增强了用户体验。对于IT专业人士而言,了解和遵守EIA-CEA 861B标准是进行视频系统设计、故障诊断及设备兼容性测试的重要基础。
recommend-type

【DFLauncher应用实战】:如何将DFLauncher融入矮人要塞并提升效率

# 摘要 DFLauncher是一款功能全面的游戏管理工具,旨在简化游戏安装、启动和维护过程。本文介绍了DFLauncher的基本使用方法,详细解析了其核心功能,包括游戏库管理、游戏配置优化、更新机制等。同时,文章探讨了DFLauncher在特定游戏“矮人要塞”中的集成应用,以及通过插件和脚本进行的高级定制。故障诊断与系统优化章节提供了实用的诊断方法和性能提升技巧。最后,本文展望了DFLauncher的未来发展方向,并鼓励社区贡献和用户反馈,以满足不断变化的用户需求。 # 关键字 DFLauncher;游戏管理工具;安装配置;性能优化;故障诊断;社区贡献;定制化扩展;网络功能集成 参考资源
recommend-type

银河麒麟系统打开屏保

<think>嗯,用户的问题是关于银河麒麟操作系统如何开启屏幕保护程序。这个问题其实挺实用的,毕竟长时间盯着电脑屏幕容易造成视觉疲劳,屏幕保护能起到一定的保护作用。 从用户提供的引用内容来看,引用[2]提到了银河麒麟系统的常见问题处理,包括重启输入法和双屏配置问题,虽然不直接相关,但说明用户对系统设置有一定了解。引用[3]是关于安装cmake的,可能用户之前尝试过其他系统配置操作。 用户可能是在办公环境中使用银河麒麟系统,需要保护视力。考虑到银河麒麟是基于Linux开发的国产操作系统,它的屏幕保护设置应该和主流Linux发行版类似,但可能有自己的特色功能。 我回忆了一下Linux系统的屏