第12天 下订单

This commit is contained in:
Y1NanPing 2025-08-01 20:58:58 +08:00
parent 34cb7047bb
commit e287bcea12
60 changed files with 1174 additions and 265 deletions

View File

@ -38,41 +38,47 @@ public class GuiGuCacheAspect {
List<Object> argsList = Arrays.asList(pjp.getArgs());
String args = "none";
if (CollUtil.isNotEmpty(argsList)) {
//将argsList转换成字符串,用_拼接
args = argsList.stream()
.map(Object::toString)
.collect(Collectors.joining("_"));
}
//用写这个注解的prefix属性拼接上参数args得到缓存数据Key
String dataKey = guiGuCache.prefix() + args;
//1.2 从Redis中获取缓存数据
Object result = redisTemplate.opsForValue().get(dataKey);
//1.3 命中缓存直接返回即可
if (result != null) {
return result;
}
//2.尝试获取分布式锁
//2.1 构建锁Key
String lockKey = dataKey + RedisConstant.CACHE_LOCK_SUFFIX;
//2.2 基于RedissonClient创建锁对象
RLock lock = redissonClient.getLock(lockKey);
boolean flag= lock.tryLock(ALBUM_LOCK_WAIT_PX1, ALBUM_LOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//2.3 尝试获取锁-设置锁过期时间
boolean flag =
lock.tryLock(ALBUM_LOCK_WAIT_PX1, ALBUM_LOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//3.获取锁成功执行目标方法将结果缓存到Redis中业务执行结束释放锁
if(flag){
if (flag) {
//3.1 执行查询数据库方法
try {
//3.1 执行查询数据库方法
result = pjp.proceed();
//设置超时时间,防止缓存雪崩
//3.2 将查询结果放入缓存中将结果返回
long ttl = RedisConstant.ALBUM_TIMEOUT + RandomUtil.randomInt(600);
redisTemplate.opsForValue().set(dataKey, result, ttl, TimeUnit.SECONDS);
return result;
} finally {
//3.3 释放锁
lock.unlock();
}
}else{
} else {
//4.获取锁失败自旋
TimeUnit.MILLISECONDS.sleep(30);
return this.doBasicProfiling(pjp, guiGuCache);
}
} catch (Throwable e) {
//5.如果Redis服务可不用兜底处理直接执行目标方法
log.error("Redis服务不可用请检查Redis服务是否正常");
return pjp.proceed();
}

View File

@ -20,29 +20,29 @@ public class ThreadPoolConfig {
@Autowired(required = false)
ZipkinHelper zipkinHelper;
/**
* 基于JDKJUC提供线程池Class
*/
@Bean
public Executor threadPoolExecutor() {
//1.获取当前服务器核心数确定核心线程数
int cpuCoreCount = Runtime.getRuntime().availableProcessors();
int threadCount = cpuCoreCount * 2;
//2.通过构造方法创建线程池对象
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(
threadCount,
threadCount,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
//3.可选提交创建核心线程
threadPoolExecutor.prestartCoreThread();
return threadPoolExecutor;
}
// /**
// * 基于JDKJUC提供线程池Class
// */
// @Bean
// public Executor threadPoolExecutor() {
// //1.获取当前服务器核心数确定核心线程数
// int cpuCoreCount = Runtime.getRuntime().availableProcessors();
// int threadCount = cpuCoreCount * 2;
// //2.通过构造方法创建线程池对象
// ThreadPoolExecutor threadPoolExecutor =
// new ThreadPoolExecutor(
// threadCount,
// threadCount,
// 0,
// TimeUnit.SECONDS,
// new ArrayBlockingQueue<>(200),
// Executors.defaultThreadFactory(),
// new ThreadPoolExecutor.CallerRunsPolicy()
// );
// //3.可选提交创建核心线程
// threadPoolExecutor.prestartCoreThread();
// return threadPoolExecutor;
// }
/**

View File

@ -0,0 +1,52 @@
package com.atguigu.tingshu.common.config.redis;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* redisson配置信息
*/
@Data
@Configuration
@ConfigurationProperties("spring.data.redis")
public class RedissonConfig {
private String host;
private String password;
private String port;
private int timeout = 3000;
private static String ADDRESS_PREFIX = "redis://";
/**
* 自动装配
*/
@Bean
RedissonClient redissonSingle() {
Config config = new Config();
if (StringUtils.isBlank(host)) {
throw new RuntimeException("host is empty");
}
//设置看门狗时间=锁释放时间
config.setLockWatchdogTimeout(10000);
SingleServerConfig serverConfig =
config.useSingleServer()
.setAddress(ADDRESS_PREFIX + this.host + ":" + port)
.setTimeout(this.timeout);
if (StringUtils.isNotBlank(this.password)) {
serverConfig.setPassword(this.password);
}
return Redisson.create(config);
}
}

View File

@ -2,16 +2,14 @@ package com.atguigu.tingshu.album;
import com.atguigu.tingshu.album.impl.AlbumDegradeFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategory1;
import com.atguigu.tingshu.model.album.BaseCategory3;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.model.album.*;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
import java.util.Map;
/**
* <p>
@ -30,9 +28,11 @@ public interface AlbumFeignClient {
*/
@GetMapping("/albumInfo/getAlbumInfo/{id}")
public Result<AlbumInfo> getAlbumInfo(@PathVariable Long id);
/**
* 后续改为Redis缓存
* 根据三级分类ID查询分类视图
*
* @param category3Id
* @return
*/
@ -44,11 +44,13 @@ public interface AlbumFeignClient {
@GetMapping("/albumInfo/getAlbumStatVo/{albumId}")
public Result<AlbumStatVo>getAlbumStatVo(@PathVariable Long albumId);
public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId);
@GetMapping("/category/findAllCategory1")
Result<List<BaseCategory1>> findAllCategory1();
Result<List<BaseCategory1>> findAllCategory1();
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> findWaitBuyTrackList(@PathVariable Long trackId, @PathVariable Integer trackCount);
}

View File

@ -3,15 +3,13 @@ package com.atguigu.tingshu.album.impl;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategory1;
import com.atguigu.tingshu.model.album.BaseCategory3;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.model.album.*;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
@Slf4j
@ -47,4 +45,11 @@ public class AlbumDegradeFeignClient implements AlbumFeignClient {
log.error("[专辑服务]提供远程调用方法findAllCategory1执行服务降级");
return null;
}
@Override
public Result<List<TrackInfo>> findWaitBuyTrackList(Long trackId, Integer trackCount) {
log.error("[专辑服务]提供远程调用方法findWaitBuyTrackList执行服务降级");
return null;
}
}

View File

@ -1,6 +1,7 @@
package com.atguigu.tingshu.user.client;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.client.impl.UserDegradeFeignClient;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import org.springframework.cloud.openfeign.FeignClient;
@ -19,10 +20,11 @@ import java.util.Map;
*
* @author atguigu
*/
@FeignClient(value = "service-user",path = "/api/user",fallback = UserDegradeFeignClient.class)
@FeignClient(value = "service-user", path = "/api/user", fallback = UserDegradeFeignClient.class)
public interface UserFeignClient {
/**
* 根据用户ID查询用户信息
*
* @param userId
* @return
*/
@ -35,4 +37,14 @@ public interface UserFeignClient {
@RequestBody List<Long> needCheckPayStateTrackIdList
);
@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id);
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId);
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> findUserPaidTrackList(@PathVariable Long albumId);
}

View File

@ -2,6 +2,7 @@ package com.atguigu.tingshu.user.client.impl;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
@ -25,4 +26,22 @@ public class UserDegradeFeignClient implements UserFeignClient {
log.error("[用户服务]提供远程调用方法userIsPaidTrack执行服务降级");
return null;
}
@Override
public Result<VipServiceConfig> getVipServiceConfig(Long id) {
log.error("[用户服务]提供远程调用方法userIsPaidTrack执行服务降级");
return null;
}
@Override
public Result<Boolean> isPaidAlbum(Long albumId) {
log.error("[用户服务]提供远程调用方法isPaidAlbum执行服务降级");
return null;
}
@Override
public Result<List<Long>> findUserPaidTrackList(Long albumId) {
log.error("[用户服务]提供远程调用方法findUserPaidTrackList执行服务降级");
return null;
}
}

View File

@ -20,6 +20,7 @@
<module>service-order</module>
<module>service-payment</module>
<module>service-user</module>
<module>service-cdc</module>
</modules>
<dependencies>

View File

@ -1,11 +1,18 @@
package com.atguigu.tingshu.account.api;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.AuthContextHolder;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@Tag(name = "用户账户管理")
@RestController
@RequestMapping("api/account")
@ -14,6 +21,16 @@ public class UserAccountApiController {
@Autowired
private UserAccountService userAccountService;
@GuiGuLogin
@Operation(summary = "查询当前用户账户可用余额")
@GetMapping("/userAccount/getAvailableAmount")
public Result<BigDecimal> getAvailableAmount(){
Long userId = AuthContextHolder.getUserId();
BigDecimal Amount = userAccountService.getAvailableAmount(userId);
return Result.ok(Amount);
}
}

View File

@ -22,4 +22,6 @@ public interface UserAccountService extends IService<UserAccount> {
* @param orderNo 订单编号
*/
void saveUserAccountDetail(Long userId, String title, BigDecimal amount, String orderNo);
BigDecimal getAvailableAmount(Long userId);
}

View File

@ -6,6 +6,7 @@ import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.account.UserAccount;
import com.atguigu.tingshu.model.account.UserAccountDetail;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -58,4 +59,14 @@ public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserA
userAccountDetail.setOrderNo(orderNo);
userAccountDetailMapper.insert(userAccountDetail);
}
@Override
public BigDecimal getAvailableAmount(Long userId) {
UserAccount userAccount = userAccountMapper.selectOne(
new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getUserId, userId)
);
BigDecimal availableAmount = userAccount.getAvailableAmount();
return availableAmount;
}
}

View File

@ -1,5 +1,11 @@
package com.atguigu.tingshu;
import com.atguigu.tingshu.common.constant.RedisConstant;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@ -10,10 +16,21 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@EnableDiscoveryClient
@EnableFeignClients
@EnableScheduling //开启定时任务功能
public class ServiceAlbumApplication {
@Slf4j
public class ServiceAlbumApplication implements CommandLineRunner {
@Autowired
private RedissonClient redissonClient;
public static void main(String[] args) {
SpringApplication.run(ServiceAlbumApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
if (!bloomFilter.isExists()){
log.info("初始化布隆过滤器,将布隆过滤器配置信息写入redis中采用hash结构存储");
bloomFilter.tryInit(10000, 0.03);
}
}
}

View File

@ -7,6 +7,7 @@ import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.query.album.TrackInfoQuery;
import com.atguigu.tingshu.vo.album.AlbumTrackListVo;
import com.atguigu.tingshu.vo.album.TrackInfoVo;
@ -162,6 +163,37 @@ public class TrackInfoApiController {
return Result.ok(trackStatVo);
}
/**
* 基于用户选择声音作为起始标准获取未购买声音数量得到分集购买列表
*
* @param trackId
* @return [{name:"本集",price:0.1,trackCount:1},{name:"后10集",price:1,trackCount:10},...,{name:"全集",price:4.3,trackCount:43},]
*/
@GuiGuLogin
@Operation(summary = "基于用户选择声音作为起始标准获取未购买声音数量,得到分集购买列表")
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> findUserTrackPaidList(@PathVariable Long trackId){
Long userId = AuthContextHolder.getUserId();
List<Map<String, Object>> list =trackInfoService.findUserTrackPaidList(trackId,userId);
return Result.ok(list);
}
/**
* 基于选择购买声音ID作为起始获取指定数量未购买声音列表
*
* @param trackId
* @param trackCount
* @return
*/
@GuiGuLogin
@Operation(summary = "基于选择购买声音ID作为起始获取指定数量未购买声音列表")
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> findWaitBuyTrackList(@PathVariable Long trackId, @PathVariable Integer trackCount){
Long userId = AuthContextHolder.getUserId();
List<TrackInfo> list =trackInfoService.findWaitBuyTrackList(trackId,trackCount,userId);
return Result.ok( list);
}

View File

@ -30,4 +30,10 @@ public interface AlbumInfoService extends IService<AlbumInfo> {
List<AlbumInfo> findUserAllAlbumList(Long userId);
AlbumStatVo getAlbumStatVo(Long albumId);
/**
* 查询指定状态专辑ID列表
* @param status
* @return
*/
List<Long> findAlbumListByStatus(String status);
}

View File

@ -6,6 +6,7 @@ import com.atguigu.tingshu.vo.album.*;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import java.util.Map;
public interface TrackInfoService extends IService<TrackInfo> {
@ -37,4 +38,9 @@ public interface TrackInfoService extends IService<TrackInfo> {
TrackStatVo getTrackStatVo(Long trackId);
void updateStat(TrackStatMqVo mqVo);
List<Map<String, Object>> findUserTrackPaidList(Long trackId, Long userId);
List<TrackInfo> findWaitBuyTrackList(Long trackId, Integer trackCount, Long userId);
}

View File

@ -241,4 +241,21 @@ public class AlbumInfoServiceImpl extends ServiceImpl<AlbumInfoMapper, AlbumInfo
public AlbumStatVo getAlbumStatVo(Long albumId) {
return albumInfoMapper.getAlbumStatVo(albumId);
}
//通过状态查询所有的专辑
@Override
public List<Long> findAlbumListByStatus(String status) {
List<AlbumInfo> albumInfos = albumInfoMapper.selectList(
new LambdaQueryWrapper<AlbumInfo>()
.eq(AlbumInfo::getStatus, status)
.select(AlbumInfo::getId)
);
if(CollUtil.isNotEmpty(albumInfos)){
return albumInfos.stream().map(AlbumInfo::getId).collect(Collectors.toList());
}
return null;
}
}

View File

@ -1,6 +1,7 @@
package com.atguigu.tingshu.album.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import com.atguigu.tingshu.album.mapper.AlbumInfoMapper;
import com.atguigu.tingshu.album.mapper.AlbumStatMapper;
@ -15,11 +16,13 @@ import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.AlbumStat;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.album.TrackStat;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.query.album.TrackInfoQuery;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.album.*;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -30,9 +33,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import static com.atguigu.tingshu.common.constant.SystemConstant.ALBUM_PAY_TYPE_REQUIRE;
@ -290,7 +291,7 @@ public class TrackInfoServiceImpl extends ServiceImpl<TrackInfoMapper, TrackInfo
pageInfo.getRecords()
.stream()
.filter(track -> track.getOrderNum() > tracksForFree)
.forEach(track-> track.setIsShowPaidMark(payStatusMap.get(track.getTrackId()) == 0));
.forEach(track -> track.setIsShowPaidMark(payStatusMap.get(track.getTrackId()) == 0));
}
}
return pageInfo;
@ -299,7 +300,7 @@ public class TrackInfoServiceImpl extends ServiceImpl<TrackInfoMapper, TrackInfo
@Override
@GuiGuCache(prefix = "album:trackInfo:stat:")
public TrackStatVo getTrackStatVo(Long trackId) {
return trackInfoMapper.getTrackStatVo(trackId);
return trackInfoMapper.getTrackStatVo(trackId);
}
@ -314,9 +315,8 @@ public class TrackInfoServiceImpl extends ServiceImpl<TrackInfoMapper, TrackInfo
.eq(TrackStat::getStatType, mqVo.getStatType())
.setSql("stat_num = stat_num +" + mqVo.getCount())
);
//2.如果是声音播放评论还需要更新声音所属专辑统计数值
if(SystemConstant.TRACK_STAT_PLAY.equals(mqVo.getStatType())){
if (SystemConstant.TRACK_STAT_PLAY.equals(mqVo.getStatType())) {
albumStatMapper.update(
null,
new LambdaUpdateWrapper<AlbumStat>()
@ -325,8 +325,7 @@ public class TrackInfoServiceImpl extends ServiceImpl<TrackInfoMapper, TrackInfo
.setSql("stat_num = stat_num +" + mqVo.getCount())
);
}
if(SystemConstant.TRACK_STAT_COMMENT.equals(mqVo.getStatType())){
if (SystemConstant.TRACK_STAT_COMMENT.equals(mqVo.getStatType())) {
albumStatMapper.update(
null,
new LambdaUpdateWrapper<AlbumStat>()
@ -339,7 +338,83 @@ public class TrackInfoServiceImpl extends ServiceImpl<TrackInfoMapper, TrackInfo
}
@Override
public List<Map<String, Object>> findUserTrackPaidList(Long trackId, Long userId) {
//根据声音id去查询声音的属性
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
//查出来声音是哪个专辑的
Long albumId = trackInfo.getAlbumId();
//获取声音的排序值
Integer orderNum = trackInfo.getOrderNum();
//2.根据"起始"声音序号+专辑ID获取所有待购买声音列表可能包含已购买声音
List<TrackInfo> waitBuyTrackList = trackInfoMapper.selectList(
new LambdaQueryWrapper<TrackInfo>()
.eq(TrackInfo::getAlbumId, albumId)
.ge(TrackInfo::getOrderNum, orderNum)
.select(TrackInfo::getId)
);
//3.远程调用用户服务获取已购买声音ID列表
List<Long> userPaidTrackIdList = userFeignClient.findUserPaidTrackList(albumId).getData();
if (CollUtil.isNotEmpty(userPaidTrackIdList)) {
//4.如果存在已购声音ID则将已购声音ID从待购买声音列表中移除得到未购买声音数量
waitBuyTrackList = waitBuyTrackList.stream()
.filter(f -> !userPaidTrackIdList.contains(f.getId()))
.collect(Collectors.toList());
}
//5.基于未购买声音数量生成分集购买列表
//查出来未存在的专辑数量
int size = waitBuyTrackList.size();
AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
BigDecimal price = albumInfo.getPrice();
//5.1 创建分集购买集合List
List<Map<String, Object>> list = new ArrayList<>();
//5.2 构建"本集"分集购买对象
Map<String, Object> currMap = new HashMap<>();
currMap.put("name", "本集");
currMap.put("price", price);
currMap.put("trackCount", 1);
list.add(currMap);
//5.3 构建其他"后*集"分集购买对象 未购买数量23 43
for (int i = 10; i <= 50; i += 10) {
Map<String, Object> map = new HashMap<>();
if (size >= i) {
map.put("name", "" + i + "");
map.put("price", price.multiply(new BigDecimal(i)));
map.put("trackCount", i);
list.add(map);
} else {
map.put("name", "" + size + "");
map.put("price", price.multiply(new BigDecimal(size)));
map.put("trackCount", size);
list.add(map);
break;
}
}
return list;
}
@Override
public List<TrackInfo> findWaitBuyTrackList(Long trackId, Integer trackCount, Long userId) {
//1.根据选择声音ID查询声音得到专辑ID跟序号
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
Long albumId = trackInfo.getAlbumId();
Integer orderNum = trackInfo.getOrderNum();
//2.远程调用用户服务获取已购买声音ID列表
List<Long> userPaidTrackIdList = userFeignClient.findUserPaidTrackList(albumId).getData();
//3.获取指定数量未购买声音列表
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<TrackInfo>()
.eq(TrackInfo::getAlbumId, albumId)
.eq(TrackInfo::getOrderNum, orderNum)
.last("limit " + trackCount)
.select(TrackInfo::getId, TrackInfo::getTrackTitle, TrackInfo::getCoverUrl, TrackInfo::getAlbumId);
if (CollUtil.isNotEmpty(userPaidTrackIdList)) {
queryWrapper.notIn(TrackInfo::getId, userPaidTrackIdList);
}
List<TrackInfo> trackInfoList = trackInfoMapper.selectList(queryWrapper);
return trackInfoList;
}
}

View File

@ -0,0 +1,80 @@
package com.atguigu.tingshu.album.task;
import com.atguigu.tingshu.album.service.AlbumInfoService;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.album.AlbumInfo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Slf4j
public class RebuildBloomFilterTask {
@Autowired
private RedissonClient redissonClient;
@Autowired
private AlbumInfoService albumInfoService;
@Scheduled(cron = "0 0 2 1 * ?")
public void rebuildBloomFilter() {
//1.获取到原有布隆过滤器对象 得到配置信息期望数据量现有数量预估值误判率
RBloomFilter<Long> oldBloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
long count = oldBloomFilter.count();
long expectedInsertions = oldBloomFilter.getExpectedInsertions();
double falseProbability = oldBloomFilter.getFalseProbability();
//2.如果现有数量预估值大于期望数据规模进行扩容
if(count>=expectedInsertions){
log.info("当前布隆过滤器已满,开始扩容");
//2.1 新建布隆过滤器对象初始化配置信息不变
RBloomFilter<Object> newBloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER + ":new");
newBloomFilter.tryInit(expectedInsertions * 2, falseProbability);
//2.2 将现有过审专辑ID添加到新布隆过滤器对象中
List<Long> albumList = albumInfoService.findAlbumListByStatus(SystemConstant.ALBUM_STATUS_PASS);
for (Long albumId : albumList) {
newBloomFilter.add(albumId);
}
log.info("新布隆过滤器已创建完成");
//2.3 删除旧布隆过滤器对象配置&位图
oldBloomFilter.delete();
//2.4 重命名布隆过滤器改为原有名称
newBloomFilter.rename(RedisConstant.ALBUM_BLOOM_FILTER);
log.info("扩容结束");
}else{
log.info("开始重建布隆过滤器");
//3.如果现有数量预估值小于期望数据规模进行重建
//3.1 新建布隆过滤器对象初始化配置信息不变
RBloomFilter<Long> newBloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER + ":new");
newBloomFilter.tryInit(expectedInsertions, falseProbability);
try {
//3.2 将现有过审专辑ID添加到新布隆过滤器对象中
} catch (Exception e) {
throw new RuntimeException(e);
}
List<Long> albumIdList = albumInfoService.findAlbumListByStatus(SystemConstant.ALBUM_STATUS_PASS);
for (Long albumId : albumIdList) {
newBloomFilter.add(albumId);
}
//3.3 删除旧布隆过滤器对象配置&位图
oldBloomFilter.delete();
//3.4 重命名布隆过滤器改为原有名称
newBloomFilter.rename(RedisConstant.ALBUM_BLOOM_FILTER);
log.info("重建布隆过滤器结束");
}
}
}

View File

@ -8,6 +8,8 @@ import com.atguigu.tingshu.model.album.TrackInfo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@ -27,34 +29,41 @@ public class ReviewTask {
@Autowired
private AuditService auditService;
@Autowired
private RedissonClient redissonClient;
/**
* 每5秒检查一次审核结果
*/
@Scheduled(cron = "0/5 * * * * ?")
public void checkReviewResultTask() {
log.info("开始检查审核任务");
//1.查询审核状态为"审核中"声音列表
List<TrackInfo> trackInfoList = trackInfoMapper.selectList(
new LambdaQueryWrapper<TrackInfo>()
.eq(TrackInfo::getStatus, SystemConstant.TRACK_STATUS_REVIEWING)
.select(TrackInfo::getId, TrackInfo::getReviewTaskId)
.last("limit 100")
);
//2.遍历列表查询审核结果
if (CollUtil.isNotEmpty(trackInfoList)) {
for (TrackInfo trackInfo : trackInfoList) {
String suggestion = auditService.getReviewResult(trackInfo.getReviewTaskId());
if (StringUtils.isNotBlank(suggestion)) {
//3.根据审核任务结果更新声音审核状态
if ("pass".equals(suggestion)) {
trackInfo.setStatus(SystemConstant.TRACK_STATUS_PASS);
} else if ("block".equals(suggestion)) {
trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS);
} else if ("review".equals(suggestion)) {
trackInfo.setStatus(SystemConstant.TRACK_STATUS_ARTIFICIAL);
RLock lock = redissonClient.getLock("reviewtask:lock");
boolean b = lock.tryLock();
if (b) {
log.info("开始检查审核任务");
//1.查询审核状态为"审核中"声音列表
List<TrackInfo> trackInfoList = trackInfoMapper.selectList(
new LambdaQueryWrapper<TrackInfo>()
.eq(TrackInfo::getStatus, SystemConstant.TRACK_STATUS_REVIEWING)
.select(TrackInfo::getId, TrackInfo::getReviewTaskId)
.last("limit 100")
);
//2.遍历列表查询审核结果
if (CollUtil.isNotEmpty(trackInfoList)) {
for (TrackInfo trackInfo : trackInfoList) {
String suggestion = auditService.getReviewResult(trackInfo.getReviewTaskId());
if (StringUtils.isNotBlank(suggestion)) {
//3.根据审核任务结果更新声音审核状态
if ("pass".equals(suggestion)) {
trackInfo.setStatus(SystemConstant.TRACK_STATUS_PASS);
} else if ("block".equals(suggestion)) {
trackInfo.setStatus(SystemConstant.TRACK_STATUS_NO_PASS);
} else if ("review".equals(suggestion)) {
trackInfo.setStatus(SystemConstant.TRACK_STATUS_ARTIFICIAL);
}
trackInfoMapper.updateById(trackInfo);
}
trackInfoMapper.updateById(trackInfo);
}
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.atguigu.tingshu</groupId>
<artifactId>service</artifactId>
<version>1.0</version>
</parent>
<artifactId>service-cdc</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.github.xizixuejie</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>0.0.17</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,19 @@
package com.atguigu.tingshu;
import io.xzxj.canal.spring.annotation.EnableCanalListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
/**
* @author: atguigu
* @create: 2025-03-21 15:46
*/
@EnableCanalListener
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class CDCApplicaiton {
public static void main(String[] args) {
SpringApplication.run(CDCApplicaiton.class, args);
}
}

View File

@ -0,0 +1,36 @@
package com.atguigu.tingshu.listener;
import com.atguigu.tingshu.model.album.AlbumInfo;
import io.xzxj.canal.core.annotation.CanalListener;
import io.xzxj.canal.core.listener.EntryListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
/**
* @author: atguigu
* @create: 2025-03-22 08:46
*/
@Slf4j
@CanalListener(destination = "tingshuTopic", schemaName = "tingshu_album", tableName = "album_info")
public class AlbumListener implements EntryListener<AlbumInfo> {
@Autowired
private RedisTemplate redisTemplate;
/**
* 监听到转机信息表更新
* @param before 变更前数据
* @param after 变更后的数据
* @param fields 变更字段
*/
@Override
public void update(AlbumInfo before, AlbumInfo after, Set<String> fields) {
log.info("[cdc]监听到变更数据,变更前:{},变更后:{}", before, after);
Long id = after.getId();
String redisKey = "album:info:"+id;
redisTemplate.delete(redisKey);
}
}

View File

@ -0,0 +1,35 @@
package com.atguigu.tingshu.listener;
import com.atguigu.tingshu.model.user.UserInfo;
import io.xzxj.canal.core.annotation.CanalListener;
import io.xzxj.canal.core.listener.EntryListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
/**
* @author: atguigu
* @create: 2025-03-21 15:47
*/
@Slf4j
@CanalListener(destination = "tingshuTopic", schemaName = "tingshu_user", tableName = "user_info")
public class UserListener implements EntryListener<UserInfo> {
@Autowired
private RedisTemplate redisTemplate;
/**
* 监听用户表更新回调方法
* @param before
* @param after
* @param fields
*/
@Override
public void update(UserInfo before, UserInfo after, Set<String> fields) {
log.info("[cdc]监听到变更数据:{}", after);
String redisKey = "user:info:"+after.getId();
redisTemplate.delete(redisKey);
}
}

View File

@ -0,0 +1,36 @@
package com.atguigu.tingshu.listener;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import io.xzxj.canal.core.annotation.CanalListener;
import io.xzxj.canal.core.listener.EntryListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
/**
* @author: atguigu
* @create: 2025-03-22 08:46
*/
@Slf4j
@CanalListener(destination = "tingshuTopic", schemaName = "tingshu_user", tableName = "vip_service_config")
public class VipServiceConfigListener implements EntryListener<VipServiceConfig> {
@Autowired
private RedisTemplate redisTemplate;
/**
* 监听到VIP套餐信息表更新
* @param before 变更前数据
* @param after 变更后的数据
* @param fields 变更字段
*/
@Override
public void update(VipServiceConfig before, VipServiceConfig after, Set<String> fields) {
log.info("[cdc]监听到变更数据,变更前:{},变更后:{}", before, after);
Long id = after.getId();
String redisKey = "user:vipconfig:"+id;
redisTemplate.delete(redisKey);
}
}

View File

@ -0,0 +1,8 @@
spring.application.name=service-canal
spring.profiles.active=dev
spring.main.allow-bean-definiton-overriding=true
spring.cloud.nacos.discovery.server-addr=192.168.200.6:8848
spring.cloud.nacos.config.server-addr=192.168.200.6:8848
spring.cloud.nacos.config.prefix=${spring.application.name}
spring.cloud.nacos.config.file-extension=yaml
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml

View File

@ -1,8 +1,15 @@
package com.atguigu.tingshu.order.api;
import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.order.service.OrderInfoService;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -16,5 +23,14 @@ public class OrderInfoApiController {
private OrderInfoService orderInfoService;
@GuiGuLogin
@Operation(summary = "三种商品VIP会员、专辑、声音订单结算")
@PostMapping("/orderInfo/trade")
public Result<OrderInfoVo> trade(@RequestBody TradeVo tradeVo) {
OrderInfoVo orderInfoVo =orderInfoService.trade(tradeVo);
return Result.ok(orderInfoVo);
}
}

View File

@ -1,9 +1,12 @@
package com.atguigu.tingshu.order.service;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface OrderInfoService extends IService<OrderInfo> {
OrderInfoVo trade(TradeVo tradeVo);
}

View File

@ -1,13 +1,36 @@
package com.atguigu.tingshu.order.service.impl;
import cn.hutool.core.lang.Assert;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.order.mapper.OrderInfoMapper;
import com.atguigu.tingshu.order.service.OrderInfoService;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.order.OrderDerateVo;
import com.atguigu.tingshu.vo.order.OrderDetailVo;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.weaver.ast.Or;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@SuppressWarnings({"all"})
@ -15,6 +38,165 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
@Autowired
private OrderInfoMapper orderInfoMapper;
@Autowired
private UserFeignClient userFeignClient;
@Qualifier("com.atguigu.tingshu.album.AlbumFeignClient")
@Autowired
private AlbumFeignClient albumFeignClient;
@Override
public OrderInfoVo trade(TradeVo tradeVo) {
//从线程中取出userid来
Long userId = AuthContextHolder.getUserId();
//1.创建订单VO对象创建初始三个金额两个集合商品优惠
OrderInfoVo orderInfoVo = new OrderInfoVo();
//1.1 初始化原价金额
// 必须是BigDecimal类型且必须是字符串
BigDecimal originalAmount = new BigDecimal("0.00");
//1.2 初始化减免金额
BigDecimal derateAmount = new BigDecimal("0.00");
//1.3 初始化订单金额
BigDecimal orderAmount = new BigDecimal("0.00");
//1.4.初始化订单明细
List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
//1.5.初始化订单减免明细列表
List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
//todo 流水号 签名之类的还未添加
//1.6 获取购买项目类型 付款项目类型: 1001-专辑 1002-声音 1003-vip会员
String itemType = tradeVo.getItemType();
//2. 处理购买项目类型VIP套餐
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
//2.1 远程调用用户服务得到VIP套餐信息
VipServiceConfig vipServiceConfig = userFeignClient.getVipServiceConfig(tradeVo.getItemId()).getData();
Assert.notNull(vipServiceConfig, "VIP套餐{}不存在", tradeVo.getItemId());
//2.2 计算原金额订单金额减免金额
originalAmount = vipServiceConfig.getPrice();
orderAmount = vipServiceConfig.getDiscountPrice();
//2.3 封装商品信息列表
OrderDetailVo orderDetailVo = new OrderDetailVo();
//套餐的ID
orderDetailVo.setItemId(tradeVo.getItemId());
//套餐的名字
orderDetailVo.setItemName("VIP套餐" + vipServiceConfig.getName());
//套餐图片的url
orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl());
//套餐的折后价
orderDetailVo.setItemPrice(orderAmount);
orderDetailVoList.add(orderDetailVo);
//2.4 如果存在优惠封装优惠列表
//bigdecimal是不能用equals判断的 因为1.10和1.1不相等
//compareTo(orderAmount)==0是相等
//compareTo(orderAmount)==-1小于
//compareTo(orderAmount)==1是大于 如果大于的话说明有了折扣
if (originalAmount.compareTo(orderAmount) == 1) {
derateAmount = originalAmount.subtract(orderAmount);
//存在优惠 我们要封装优惠列表
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateAmount(derateAmount);
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
orderDerateVo.setRemarks("VIP套餐限时优惠:" + derateAmount);
orderDerateVoList.add(orderDerateVo);
}
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
//3. 处理购买项目类型专辑
//3.1 远程调用用户服务判断是否重复购买专辑如果已购过则业务终止
Long albumId = tradeVo.getItemId();
Boolean data = userFeignClient.isPaidAlbum(albumId).getData();
//如果data为true的话就走断言
Assert.isFalse(data, "用户已购买专辑{}", albumId);
//3.2 远程调用专辑服务获取专辑价格以及折扣普通用户折扣VIP会员折扣
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
BigDecimal price = albumInfo.getPrice();
BigDecimal discount = albumInfo.getDiscount();
BigDecimal vipDiscount = albumInfo.getVipDiscount();
//判断用户是否为vip 如果是vip就走vip折扣 如果不是就走普通用户的折扣
Boolean isVIP = false;
UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData();
Assert.notNull(userInfoVo, "用户{}不存在", userId);
//判断一下用户是否为vip以及vip的过期时间是否超过了当时的时间
if (userInfoVo.getIsVip().intValue() == 1
&& userInfoVo.getVipExpireTime().after(new Date())) {
isVIP = true;
}
//3.4 计算相关价格
//3.4.1 暂时将订单价=原价
originalAmount = price;
orderAmount = originalAmount;
//当你是普通客户 但是专辑是有普通用户折扣的时候 计算一下专辑价格
//discount.doubleValue()!=-1
//BigDecimal.doubleValue() 是将 BigDecimal 转换为 double 类型的操作
//如果是普通用户且有折扣,则订单价=原价 100 *折扣 6
//..divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP)是保留两位小数的标准写法
if (!isVIP && discount.doubleValue() != -1) {
orderAmount =originalAmount.multiply(discount)
.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
}
//如果是会员,则按照会员折扣走
if (isVIP && vipDiscount.doubleValue() != -1) {
orderAmount = originalAmount.multiply(vipDiscount)
.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
}
//3.5 封装商品信息列表
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(albumId);
orderDetailVo.setItemName("专辑:"+albumInfo.getAlbumTitle());
orderDetailVo.setItemUrl(albumInfo.getCoverUrl());
orderDetailVo.setItemPrice(originalAmount);
orderDetailVoList.add(orderDetailVo);
//2.4 如果存在优惠封装优惠列表
if(originalAmount.compareTo(orderAmount)==1){
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setRemarks(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
orderDerateVo.setDerateType("VIP套餐限时优惠:" + derateAmount);
orderDerateVo.setDerateAmount(originalAmount.subtract(orderAmount));
orderDerateVoList.add(orderDerateVo);
}
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) {
//4.处理购买项目类型声音
//4.1 远程调用专辑服务获取未购买声音列表
Long trackId = tradeVo.getItemId();
List<TrackInfo> trackInfoList =
albumFeignClient.findWaitBuyTrackList(trackId, tradeVo.getTrackCount()).getData();
Assert.notNull(trackInfoList, "不存在待结算声音",trackId);
//4.2 远程调用专辑服务获取专辑价格声音单价声音不支持折扣
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(trackInfoList.get(0).getAlbumId()).getData();
Assert.notNull(albumInfo, "专辑{}不存在", albumInfo.getId());
BigDecimal price = albumInfo.getPrice();
//4.3 计算相关价格
originalAmount = price.multiply(new BigDecimal(trackInfoList.size()));
orderAmount = originalAmount;
//4.4 封装订单明细列表
orderDetailVoList = trackInfoList.stream().map(
trackInfo -> {
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(trackInfo.getId());
orderDetailVo.setItemName("声音:" + trackInfo.getTrackTitle());
orderDetailVo.setItemUrl(trackInfo.getCoverUrl());
orderDetailVo.setItemPrice(price);
return orderDetailVo;
}
).collect(Collectors.toList());
}
//5.封装订单VO对象
//5.1 封装相关价格信息
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setDerateAmount(derateAmount);
//5.2 封装商品相关集合
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//5.3 TODO 封装其他信息流水号时间戳及签名项目类型等
//6.响应订单VO对象
return orderInfoVo;
}
}

View File

@ -28,14 +28,6 @@
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>io.github.biezhi</groupId>
<artifactId>TinyPinyin</artifactId>

View File

@ -71,37 +71,45 @@ public class SearchReciever {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
// /**
// * 监听更新声音/搜索统计信息
// *
// * @param mqVo
// * @param message
// * @param channel
// */
//
// @RabbitListener(bindings = @QueueBinding(
// exchange = @Exchange(value = MqConst.EXCHANGE_TRACK, durable = "true"),
// value = @Queue(value = MqConst.QUEUE_ALBUM_ES_STAT_UPDATE, durable = "true"),
// key = MqConst.ROUTING_TRACK_STAT_UPDATE)
// )
// @SneakyThrows
// public void updateStat(TrackStatMqVo mqVo, Message message, Channel channel){
// if (mqVo != null)log.info("更新声音/专辑统计信息:{}", mqVo);
// //1.幂等性处理
// //1.1 从MQ消息对象中获取消息唯一标识
// String key = RedisConstant.BUSINESS_PREFIX + "db:" + mqVo.getBusinessNo();
// //1.2 尝试采用set nx写入Redis
// Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.MINUTES);
//
// //1.3 写入成功消息才进行业务处理写入失败消息重复被MQ服务器投递忽略消息
// if (flag) {
// //2.业务处理
// searchService.upperAlbum(mqVo.getAlbumId());
//
// }
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// }
/**
* 监听更新声音/搜索统计信息
*
* @param mqVo
* @param message
* @param channel
*/
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = MqConst.EXCHANGE_TRACK, durable = "true"),
value = @Queue(value = MqConst.QUEUE_ALBUM_ES_STAT_UPDATE, durable = "true"),
key = MqConst.ROUTING_TRACK_STAT_UPDATE)
)
@SneakyThrows
public void updateStat(TrackStatMqVo mqVo, Message message, Channel channel) {
if (mqVo != null) {
log.info("更新声音/专辑统计信息:{}", mqVo);
//1.幂等性处理
//1.1 从MQ消息对象中获取消息唯一标识
String key = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + "es:" + mqVo.getBusinessNo();
//1.2 尝试采用set nx写入Redis
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
//1.3 写入成功消息才进行业务处理写入失败消息重复被MQ服务器投递忽略消息
if (flag) {
try {
//2.业务处理
searchService.updateAlbumStat(mqVo);
} catch (Exception e) {
redisTemplate.delete(key);
//如果异常将无法处理消息发送到死信异常交换机->死信队列->消费者处理进入存入消费者异常消息表->人工处理
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}

View File

@ -4,6 +4,7 @@ import co.elastic.clients.elasticsearch.core.SearchResponse;
import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import com.atguigu.tingshu.model.search.SuggestIndex;
import com.atguigu.tingshu.query.search.AlbumIndexQuery;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
import com.atguigu.tingshu.vo.search.AlbumSearchResponseVo;
@ -37,4 +38,6 @@ public interface SearchService {
void updateLatelyAlbumRanking(Integer top);
List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension);
void updateAlbumStat(TrackStatMqVo mqVo);
}

View File

@ -9,6 +9,7 @@ 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.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -28,6 +29,8 @@ public class ItemServiceImpl implements ItemService {
private AlbumFeignClient albumFeignClient;
@Autowired
private Executor threadPoolTaskExecutor;
@Autowired
private RedissonClient redissonClient;
@Override

View File

@ -17,10 +17,12 @@ import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import co.elastic.clients.json.JsonData;
import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.album.*;
import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import com.atguigu.tingshu.model.search.AttributeValueIndex;
@ -30,12 +32,15 @@ import com.atguigu.tingshu.search.repository.AlbumInfoIndexRepository;
import com.atguigu.tingshu.search.repository.SuggestIndexRepository;
import com.atguigu.tingshu.search.service.SearchService;
import com.atguigu.tingshu.user.client.UserFeignClient;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
import com.atguigu.tingshu.vo.search.AlbumSearchResponseVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.suggest.Completion;
@ -68,17 +73,21 @@ public class SearchServiceImpl implements SearchService {
private UserFeignClient userFeignClient;
@Autowired
private ElasticsearchClient elasticsearchClient;
private static final String SUGGEST_INDEX = "suggestinfo";
@Autowired
private SuggestIndexRepository suggestIndexRepository;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
private static final String SUGGEST_INDEX = "suggestinfo";
@Override
public void upperAlbum(Long albumId) {
//1.创建专辑索引对象
AlbumInfoIndex albumInfoIndex = new AlbumInfoIndex();
//2.远程调用专辑服务查询专辑信息封装专辑以及专辑标签列表
CompletableFuture<AlbumInfo> albumInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
@ -137,14 +146,18 @@ public class SearchServiceImpl implements SearchService {
BaseCategoryViewCompletableFuture,
userInfoCompletableFuture,
statCompletableFuture
).orTimeout(5, TimeUnit.SECONDS)
).orTimeout(1, TimeUnit.SECONDS)
.join();
//7.保存专辑到索引库
albumInfoIndexRepository.save(albumInfoIndex);
//8.将专辑标题作为提示词文档存入提示词索引库
this.saveSuggestIndex(albumInfoIndex);
//9.将专辑ID存入布隆过滤器用于解决缓存穿透问题
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
bloomFilter.add(albumId);
}
@ -483,16 +496,18 @@ public class SearchServiceImpl implements SearchService {
@Override
public void updateLatelyAlbumRanking(Integer top) {
try {
//1.从Redis检索获取分类下五种排序维度专辑TOPN(n=top)记录
//1.从Redis检索获取分类下五种排序维度专辑TOPN记录
//1.1 远程调用专辑服务获取所有1级分类列表
List<BaseCategory1> baseCategory1List = albumFeignClient.findAllCategory1().getData();
Assert.notNull(baseCategory1List, "1级分类列表为空");
//1.2 遍历1级分类
for (BaseCategory1 baseCategory1 : baseCategory1List) {
//从内容中找出id 设定一个key
Long category1Id = baseCategory1.getId();
//基于基于1级分类ID构建Redis中hash结构的Key
String key = RedisConstant.RANKING_KEY_PREFIX + category1Id;
//绑定大key
//基于Key创建绑定hash操作对象
BoundHashOperations<String, String, List<AlbumInfoIndex>> hashOps = redisTemplate.boundHashOps(key);
//1.3 遍历5种排序维度
String[] rankingDimensionArray
@ -525,31 +540,67 @@ public class SearchServiceImpl implements SearchService {
}
}
} catch (IOException e) {
log.error("更新Redis小时排行数据异常", e);
log.error("更新Redis小时数据异常", e);
throw new RuntimeException(e);
}
}
@Override
public List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension) {
//基于1级分类ID构建Redis中hash结构的Key
String key =RedisConstant.RANKING_KEY_PREFIX+category1Id;
BoundHashOperations<String,String,List<AlbumInfoIndex>> hashOps = redisTemplate.boundHashOps(key);
//基于基于1级分类ID构建Redis中hash结构的Key
String key = RedisConstant.RANKING_KEY_PREFIX + category1Id;
BoundHashOperations<String, String, List<AlbumInfoIndex>> hashOps = redisTemplate.boundHashOps(key);
//构建Hash排行榜中field 当前排序方式
String field = dimension;
Boolean flag = hashOps.hasKey(field);
if(flag){
if (flag) {
//List<AlbumInfoIndex> list = (List<AlbumInfoIndex>) redisTemplate.opsForHash().get(key, field);
List<AlbumInfoIndex> list = hashOps.get(field);
if (CollUtil.isNotEmpty(list)) {
List<AlbumInfoIndexVo> albumInfoIndexVoList = list.stream()
.map(albumInfoIndex -> BeanUtil.copyProperties(albumInfoIndex, AlbumInfoIndexVo.class))
.map(albumInfoIndex -> BeanUtil.copyProperties(albumInfoIndex,AlbumInfoIndexVo.class))
.collect(Collectors.toList());
return albumInfoIndexVoList;
}
}
return null;
}
/**
* #增量更新
* POST /albuminfo/_update/1
* {
* "script": {
* "source": "ctx._source.playStatNum += params.increment",
* "lang": "painless",
* "params": {
* "increment": 1
* }
* }
* }
*/
@Override
public void updateAlbumStat(TrackStatMqVo mqVo) {
try {
Long albumId = mqVo.getAlbumId();
String incrementField = "";
if (SystemConstant.TRACK_STAT_PLAY.equals(mqVo.getStatType())) {
incrementField = "playStatNum";
} else if (SystemConstant.TRACK_STAT_COMMENT.equals(mqVo.getStatType())) {
incrementField = "commentStatNum";
}
String finalIncrementField = incrementField;
elasticsearchClient.update(
u -> u.index(INDEX_NAME).id(albumId.toString())
.script(s -> s.inline(i -> i.source("ctx._source." + finalIncrementField + " += params.increment").lang("painless").params(Map.of("increment", JsonData.of(mqVo.getCount())))))
, AlbumInfoIndex.class
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -1,7 +1,10 @@
package com.atguigu.tingshu.search;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.search.service.SearchService;
import org.junit.jupiter.api.Test;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@ -10,6 +13,8 @@ import org.springframework.boot.test.context.SpringBootTest;
public class test01 {
@Autowired
private SearchService searchService;
@Autowired
private RedissonClient redissonClient;
@Test
public void test01() {
@ -19,8 +24,17 @@ public class test01 {
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Test
public void test02() {
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
for (long i = 1; i <= 1629; i++) {
try {
bloomFilter.add(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@ -19,38 +19,72 @@ import java.util.Map;
@SuppressWarnings({"all"})
public class UserInfoApiController {
@Autowired
private UserInfoService userInfoService;
/**
* 根据用户ID查询用户信息
* @param userId
* @return
*/
@Operation(summary = "根据用户ID查询用户信息")
@GetMapping("/userInfo/getUserInfoVo/{userId}")
public Result<UserInfoVo> getUserInfoVo(@PathVariable Long userId){
UserInfoVo userInfoVo = userInfoService.getUserInfo(userId);
return Result.ok(userInfoVo);
}
/**
* 获取每一页声音列表提交声音ID集合对应购买状态
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param needCheckPayStateTrackIdList 需要检查购买状态的声音ID集合
* @return {声音ID:1(已购),声音ID:0(未购)}
*/
@GuiGuLogin //对内微服务访问接口上游系统调用是否只要携带token 下游系统就可以获取用户ID
@Operation(summary = "获取每一页声音列表提交声音ID集合对应购买状态")
@PostMapping("/userInfo/userIsPaidTrack/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(
@PathVariable("albumId") Long albumId,
@RequestBody List<Long> needCheckPayStateTrackIdList
) {
Long userId = AuthContextHolder.getUserId();
Map<Long, Integer> map = userInfoService.userIsPaidTrack(userId, albumId, needCheckPayStateTrackIdList);
return Result.ok(map);
}
@Autowired
private UserInfoService userInfoService;
/**
* 根据用户ID查询用户信息
*
* @param userId
* @return
*/
@Operation(summary = "根据用户ID查询用户信息")
@GetMapping("/userInfo/getUserInfoVo/{userId}")
public Result<UserInfoVo> getUserInfoVo(@PathVariable Long userId) {
UserInfoVo userInfoVo = userInfoService.getUserInfo(userId);
return Result.ok(userInfoVo);
}
/**
* 获取每一页声音列表提交声音ID集合对应购买状态
*
* @param userId 用户ID
* @param albumId 专辑ID
* @param needCheckPayStateTrackIdList 需要检查购买状态的声音ID集合
* @return {声音ID:1(已购),声音ID:0(未购)}
*/
@GuiGuLogin //对内微服务访问接口上游系统调用是否只要携带token 下游系统就可以获取用户ID
@Operation(summary = "获取每一页声音列表提交声音ID集合对应购买状态")
@PostMapping("/userInfo/userIsPaidTrack/{albumId}")
public Result<Map<Long, Integer>> userIsPaidTrack(
@PathVariable("albumId") Long albumId,
@RequestBody List<Long> needCheckPayStateTrackIdList
) {
Long userId = AuthContextHolder.getUserId();
Map<Long, Integer> map = userInfoService.userIsPaidTrack(userId, albumId, needCheckPayStateTrackIdList);
return Result.ok(map);
}
/**
* 判断当前用户是否已购买指定专辑
*
* @param albumId
* @return
*/
@GuiGuLogin
@Operation(summary = "判断当前用户是否已购买指定专辑")
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId) {
Long userId = AuthContextHolder.getUserId();
Boolean flag = userInfoService.isPaidAlbum(userId, albumId);
return Result.ok(flag);
}
/**
* 查询当前用户指定专辑下已购声音ID列表
*
* @param albumId
* @return
*/
@GuiGuLogin
@Operation(summary = "查询当前用户指定专辑下已购声音ID列表")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> findUserPaidTrackList(@PathVariable Long albumId) {
Long userId = AuthContextHolder.getUserId();
List<Long> list = userInfoService.findUserPaidTrackList(userId, albumId);
return Result.ok(list);
}
}

View File

@ -26,9 +26,16 @@ public class UserListenProcessApiController {
@Operation(summary = "获取声音播放进度")
@GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}")
public Result<BigDecimal> getTrackBreakSecond (Long trackId){
//1.尝试获取用户ID
Long userId = AuthContextHolder.getUserId();
BigDecimal second =userListenProcessService.getTrackBreakSecond(userId, trackId);
return Result.ok(second);
//2.查询用户对于指定声音播放进度
if (userId != null) {
//3.返回
BigDecimal breakSecond = userListenProcessService.getTrackBreakSecond(userId, trackId);
return Result.ok(breakSecond);
}
//3.返回
return Result.ok(BigDecimal.valueOf(0));
}
/**

View File

@ -1,11 +1,19 @@
package com.atguigu.tingshu.user.api;
import com.atguigu.tingshu.common.cache.GuiGuCache;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.service.VipServiceConfigService;
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.List;
@Tag(name = "VIP服务配置管理接口")
@RestController
@RequestMapping("api/user")
@ -15,5 +23,23 @@ public class VipServiceConfigApiController {
@Autowired
private VipServiceConfigService vipServiceConfigService;
@Operation(summary = "获取平台套餐列表")
@GetMapping("/vipServiceConfig/findAll")
@GuiGuCache(prefix = "user:vipconfig:list")
public Result<List<VipServiceConfig>> findAll(){
List<VipServiceConfig> list = vipServiceConfigService.list();
return Result.ok(list);
}
@Operation(summary = "根据id获取套餐内容")
@GuiGuCache(prefix = "user:vipconfig:")
@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id){
VipServiceConfig content = vipServiceConfigService.getById(id);
return Result.ok(content);
}
}

View File

@ -17,4 +17,7 @@ public interface UserInfoService extends IService<UserInfo> {
Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needCheckPayStateTrackIdList);
Boolean isPaidAlbum(Long userId, Long albumId);
List<Long> findUserPaidTrackList(Long userId, Long albumId);
}

View File

@ -46,124 +46,161 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
private RedisTemplate redisTemplate;
@Autowired
private RabbitService rabbitService;
@Autowired
private UserPaidAlbumMapper userPaidAlbumMapper;
@Autowired
private UserPaidTrackMapper userPaidTrackMapper;
@Override
public Map<String, String> wxLogin(String code) {
try {
//1.根据小程序提交的临时票据code调用微信接口服务获取当前微信用户唯一标识openid
WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
String wxOpenid = sessionInfo.getOpenid();
//2.根据openid查询数据库用户表判断微信账户是否关联用户
UserInfo userInfo = userInfoMapper.selectOne(
new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getWxOpenId, wxOpenid)
);//3.如果不存在用户记录说明该微信是首次登录
if (userInfo == null) {
//3.1 新增用户记录
userInfo = new UserInfo();
userInfo.setWxOpenId(wxOpenid);
userInfo.setNickname("听友" + IdUtil.nanoId());
userInfo.setAvatarUrl("http://192.168.200.6:9000/tingshu/2025-07-21/1ce194e3-4212-4ff8-948b-babd9a7b8a02.jpg");
userInfoMapper.insert(userInfo);
//3.2 TODO 隐式初始化账户记录用于后续订单消费,采用MQ
//3.2.1 创建初始化账户MQ消息对象 注意如果使用的是VO对象该对象必须得实现序列化接口 Serializable
Map<String, Object> mqData = new HashMap<>();
mqData.put("userId", userInfo.getId());
mqData.put("amount", BigDecimal.valueOf(100));
mqData.put("orderNo", "ZS"+ DateUtil.today().replaceAll("-", "")+IdUtil.getSnowflakeNextId());
mqData.put("title", "首次注册,赠送体验金活动");
//3.2.2 发送消息到RabbitMQ交换机 todo 使用RabbitService
rabbitService.sendMessage(MqConst.EXCHANGE_USER, MqConst.ROUTING_USER_REGISTER, mqData);
}
//4.无论是否首次登录都需要生成令牌返回给小程序
//4.1 生成随机令牌
String token = IdUtil.randomUUID();
//4.2 将用户令牌作为Key用户基本信息UserInfoVo作为Value保存到Redis中
String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
redisTemplate.opsForValue().set(loginKey, userInfoVo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
//4.3 返回用户令牌
return Map.of("token", token);
} catch (WxErrorException e) {
log.error("微信登录失败:{}", e.getMessage());
throw new RuntimeException(e);
}
}
@Autowired
private UserPaidAlbumMapper userPaidAlbumMapper;
@Autowired
private UserPaidTrackMapper userPaidTrackMapper;
@Override
@GuiGuCache(prefix = "user:info:")
public UserInfoVo getUserInfo(Long userId) {
UserInfo userInfo = userInfoMapper.selectById(userId);
return BeanUtil.copyProperties(userInfo, UserInfoVo.class);
public Map<String, String> wxLogin(String code) {
try {
//1.根据小程序提交的临时票据code调用微信接口服务获取当前微信用户唯一标识openid
WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
String wxOpenid = sessionInfo.getOpenid();
//2.根据openid查询数据库用户表判断微信账户是否关联用户
UserInfo userInfo = userInfoMapper.selectOne(
new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getWxOpenId, wxOpenid)
);//3.如果不存在用户记录说明该微信是首次登录
if (userInfo == null) {
//3.1 新增用户记录
userInfo = new UserInfo();
userInfo.setWxOpenId(wxOpenid);
userInfo.setNickname("听友" + IdUtil.nanoId());
userInfo.setAvatarUrl("http://192.168.200.6:9000/tingshu/2025-07-21/1ce194e3-4212-4ff8-948b-babd9a7b8a02.jpg");
userInfoMapper.insert(userInfo);
//3.2 TODO 隐式初始化账户记录用于后续订单消费,采用MQ
//3.2.1 创建初始化账户MQ消息对象 注意如果使用的是VO对象该对象必须得实现序列化接口 Serializable
Map<String, Object> mqData = new HashMap<>();
mqData.put("userId", userInfo.getId());
mqData.put("amount", BigDecimal.valueOf(100));
mqData.put("orderNo", "ZS" + DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId());
mqData.put("title", "首次注册,赠送体验金活动");
//3.2.2 发送消息到RabbitMQ交换机 todo 使用RabbitService
rabbitService.sendMessage(MqConst.EXCHANGE_USER, MqConst.ROUTING_USER_REGISTER, mqData);
}
//4.无论是否首次登录都需要生成令牌返回给小程序
//4.1 生成随机令牌
String token = IdUtil.randomUUID();
//4.2 将用户令牌作为Key用户基本信息UserInfoVo作为Value保存到Redis中
String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
redisTemplate.opsForValue().set(loginKey, userInfoVo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
//4.3 返回用户令牌
return Map.of("token", token);
} catch (WxErrorException e) {
log.error("微信登录失败:{}", e.getMessage());
throw new RuntimeException(e);
}
}
@Override
@GuiGuCache(prefix = "user:info:")
public UserInfoVo getUserInfo(Long userId) {
UserInfo userInfo = userInfoMapper.selectById(userId);
return BeanUtil.copyProperties(userInfo, UserInfoVo.class);
}
@Override
public void updateUser(UserInfoVo userInfoVo, Long userId) {
UserInfo userInfo = new UserInfo();
userInfo.setId(userId);
userInfo.setAvatarUrl(userInfoVo.getAvatarUrl());
userInfo.setNickname(userInfoVo.getNickname());
userInfoMapper.updateById(userInfo);
UserInfo userInfo = new UserInfo();
userInfo.setId(userId);
//更换头像
userInfo.setAvatarUrl(userInfoVo.getAvatarUrl());
//更换昵称
userInfo.setNickname(userInfoVo.getNickname());
userInfoMapper.updateById(userInfo);
}
@Override
public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needCheckPayStateTrackIdList) {
Map<Long, Integer> payStatusMap = new HashMap<>();
//1.根据用户ID+专辑ID查询已购专辑记录如果存在专辑购买记录将提交声音ID购买状态全部设置为1返回
Long count = userPaidAlbumMapper.selectCount(
new LambdaQueryWrapper<UserPaidAlbum>()
.eq(UserPaidAlbum::getUserId, userId)
.eq(UserPaidAlbum::getAlbumId, albumId)
);
if (count > 0) {
for (Long trackId : needCheckPayStateTrackIdList) {
payStatusMap.put(trackId, 1);
}
return payStatusMap;
}
@Override
public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needCheckPayStateTrackIdList) {
Map<Long, Integer> payStatusMap = new HashMap<>();
//1.根据用户ID+专辑ID查询已购专辑记录如果存在专辑购买记录将提交声音ID购买状态全部设置为1返回
Long count = userPaidAlbumMapper.selectCount(
new LambdaQueryWrapper<UserPaidAlbum>()
.eq(UserPaidAlbum::getUserId, userId)
.eq(UserPaidAlbum::getAlbumId, albumId)
);
if (count > 0) {
for (Long trackId : needCheckPayStateTrackIdList) {
payStatusMap.put(trackId, 1);
}
return payStatusMap;
}
//2.如果未购买专辑根据用户ID+专辑ID查询已购声音记录
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(
new LambdaQueryWrapper<UserPaidTrack>()
.eq(UserPaidTrack::getUserId, userId)
.eq(UserPaidTrack::getAlbumId, albumId)
.select(UserPaidTrack::getTrackId)
);
//2.如果未购买专辑根据用户ID+专辑ID查询已购声音记录
//查询出来的是购买的声音的id
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(
new LambdaQueryWrapper<UserPaidTrack>()
.eq(UserPaidTrack::getUserId, userId)
.eq(UserPaidTrack::getAlbumId, albumId)
.select(UserPaidTrack::getTrackId)
);
//2.1 如果不存在购买声音记录将提交声音ID购买状态全部设置为0返回
if (CollUtil.isEmpty(userPaidTrackList)) {
for (Long trackID : needCheckPayStateTrackIdList) {
payStatusMap.put(trackID, 0);
}
return payStatusMap;
}
//2.1 如果不存在购买声音记录将提交声音ID购买状态全部设置为0返回
if (CollUtil.isEmpty(userPaidTrackList)) {
for (Long trackID : needCheckPayStateTrackIdList) {
payStatusMap.put(trackID, 0);
}
return payStatusMap;
}
//2.2 如果存在购买声音记录将提交声音ID已购设置1未购买设置为0返回
//2.2.1 获取已购声音ID集合
List<Long> userPaidTrackIdList =
userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
//2.2.2 判断哪些是已购设置为1哪些是未购买设置为0
for (Long trackId : needCheckPayStateTrackIdList) {
if(userPaidTrackIdList.contains(trackId)){
//已购买
payStatusMap.put(trackId, 1);
}else{
//未购买
payStatusMap.put(trackId, 0);
}
}
return payStatusMap;
}
//2.2 如果存在购买声音记录将提交声音ID已购设置1未购买设置为0返回
//2.2.1 获取已购声音ID集合
//这个stream流就是将userPaidTrackList这个中的id单独取出来然后做成一个List
List<Long> userPaidTrackIdList =
userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
//2.2.2 判断哪些是已购设置为1哪些是未购买设置为0
for (Long trackId : needCheckPayStateTrackIdList) {
if (userPaidTrackIdList.contains(trackId)) {
//已购买
payStatusMap.put(trackId, 1);
} else {
//未购买
payStatusMap.put(trackId, 0);
}
}
return payStatusMap;
}
@Override
public Boolean isPaidAlbum(Long userId, Long albumId) {
//根据用户的id去查询购买的专辑 返回一个数值,如果有的话说明买过了
Long count = userPaidAlbumMapper.selectCount(
new LambdaQueryWrapper<UserPaidAlbum>()
.eq(UserPaidAlbum::getUserId, userId)
.eq(UserPaidAlbum::getAlbumId, albumId)
);
return count > 0;
}
}
/**
* 查询指定用户指定专辑下已购声音ID列表
*
* @param userId
* @param albumId
* @return
*/
@Override
public List<Long> findUserPaidTrackList(Long userId, Long albumId) {
List<UserPaidTrack> userPaidTracks = userPaidTrackMapper.selectList(
new LambdaQueryWrapper<UserPaidTrack>()
.eq(UserPaidTrack::getUserId, userId)
.eq(UserPaidTrack::getAlbumId, albumId)
.select(UserPaidTrack::getTrackId)
);
if (CollUtil.isNotEmpty(userPaidTracks)) {
List<Long> userPaidTrackIdList = userPaidTracks.stream()
.map(u -> u.getTrackId())
.collect(Collectors.toList());
return userPaidTrackIdList;
}
return null;
}
}