谷粒随享
学习目标:
购买包含分为:
购买VIP入口:
在新用户第一次登录的时候,就进行了初始化账户余额信息操作!
当刷新主页的时候,会加载当前余额数据。
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/93
service-account
微服务中UserAccountApiController控制器
package com.atguigu.tingshu.account.api;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.cache.GuiGuCache;
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;
import java.math.BigInteger;
@Tag(name = "用户账户管理")
@RestController
@RequestMapping("api/account")
@SuppressWarnings({"all"})
public class UserAccountApiController {
@Autowired
private UserAccountService userAccountService;
/**
* 获取当前登录用户账户可用余额
* @return
*/
@GuiGuLogin
@Operation(summary = "获取当前登录用户账户可用余额")
@GetMapping("/userAccount/getAvailableAmount")
public Result<BigDecimal> getAvailableAmount(){
Long userId = AuthContextHolder.getUserId();
BigDecimal availableAmount = userAccountService.getAvailableAmount(userId);
return Result.ok(availableAmount);
}
}
UserAccountService接口:
/**
* 获取指定用户账户可用余额
* @return
*/
BigDecimal getAvailableAmount(Long userId);
UserAccountServiceImpl实现类:
/**
* 获取指定用户账户可用余额
*
* @return
*/
@Override
public BigDecimal getAvailableAmount(Long userId) {
LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserAccount::getUserId, userId);
queryWrapper.select(UserAccount::getAvailableAmount);
UserAccount userAccount = userAccountMapper.selectOne(queryWrapper);
Assert.notNull(userAccount, "账户有误请联系管理员!");
return userAccount.getAvailableAmount();
}
在首页点击专辑封面标识为:VIP免费 会出现访问VIP服务配置管理接口
思路:
专辑订单
vip 订单
生成一个流水号存储到缓存,防止用户重复提交订单
给OrderInfoVo 实体类赋值
最后返回OrderInfoVo 对象
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/85
在service-user
微服务模块中的 VipServiceConfigApiController 控制器添加映射路径,返回 VipServiceConfig 实体类的集合数据。也就是查询vip_service_config表中的集合数据。
package com.atguigu.tingshu.user.api;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "VIP服务配置管理接口")
@RestController
@RequestMapping("api/user")
@SuppressWarnings({"all"})
public class VipServiceConfigApiController {
@Autowired
private VipServiceConfigService vipServiceConfigService;
/**
*查询VIP套餐列表
* @return
*/
@Operation(summary = "查询VIP套餐列表")
@GetMapping("/vipServiceConfig/findAll")
public Result<List<VipServiceConfig>> getAllVipConfig(){
List<VipServiceConfig> list = vipServiceConfigService.list();
return Result.ok(list);
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/91
在VipServiceConfigApiController
控制器中添加
/**
* 根据套餐ID查询套餐信息
*
* @param id
* @return
*/
@GuiGuCache(prefix = "vipservice:config:")
@Operation(summary = "根据套餐ID查询套餐信息")
@GetMapping("vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id) {
VipServiceConfig vipServiceConfig = vipServiceConfigService.getById(id);
return Result.ok(vipServiceConfig);
}
在service-user-client
模块UserFeignClient中添加
/**
* 根据套餐ID查询套餐信息
*
* @param id
* @return
*/
@GetMapping("vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id);
UserDegradeFeignClient熔断类:
@Override
public Result<VipServiceConfig> getVipServiceConfig(Long id) {
log.error("[用户服务]提供远程调用getVipServiceConfig服务降级");
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/92
package com.atguigu.tingshu.order.api;
import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.ResponseUtil;
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;
@Tag(name = "订单管理")
@RestController
@RequestMapping("api/order")
@SuppressWarnings({"all"})
public class OrderInfoApiController {
@Autowired
private OrderInfoService orderInfoService;
/**
* 用户下单返回渲染订单确认页面数据
*
* @param tradeVo
* @return
*/
@GuiGuLogin
@Operation(summary = "用户下单返回渲染订单确认页面数据")
@PostMapping("/orderInfo/trade")
public Result<OrderInfoVo> getOrderTradeData(@RequestBody TradeVo tradeVo) {
OrderInfoVo orderInfoVo = orderInfoService.getOrderTradeData(tradeVo);
return Result.ok(orderInfoVo);
}
}
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> {
/**
* 用户下单返回渲染订单确认页面数据
*
* @param tradeVo
* @return
*/
OrderInfoVo getOrderTradeData(TradeVo tradeVo);
}
package com.atguigu.tingshu.order.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.order.helper.SignHelper;
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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Autowired
private OrderInfoMapper orderInfoMapper;
@Autowired
private UserFeignClient userFeignClient;
@Autowired
private RedisTemplate redisTemplate;
/**
* 用户下单返回渲染订单确认页面数据
* 为用户选择购买不同商品返回订单确认数据
* 1.VIP会员 2.专辑 3.声音
*
* @param tradeVo itemType->项目类型: 1001-专辑 1002-声音 1003-vip会员
* @return
*/
@Override
public OrderInfoVo getOrderTradeData(TradeVo tradeVo) {
Long userId = AuthContextHolder.getUserId();
//1.创建响应VO对象,将VO中需要相关价格,商品明细,优惠明细定义
OrderInfoVo orderInfoVo = new OrderInfoVo();
//1.1 原价:初始0.00
BigDecimal originalAmount = new BigDecimal("0.00");
//1.2 减免:初始0.00
BigDecimal derateAmount = new BigDecimal("0.00");
//1.3 订单:初始0.00
BigDecimal orderAmount = new BigDecimal("0.00");
//1.4 订单明细(商品)列表
List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
//1.5 订单优惠列表
List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
//2.处理购买项目类型为VIP会员 itemType=1003-vip会员 允许多次仅购买
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
//2.1 远程调用"用户服务"获取套餐信息
VipServiceConfig vipServiceConfig = userFeignClient.getVipServiceConfig(tradeVo.getItemId()).getData();
Assert.notNull(vipServiceConfig, "VIP套餐:{}不存在!", tradeVo.getItemId());
//2.2 封装订单价格 (原价,减免价,订单价)
originalAmount = vipServiceConfig.getPrice();
orderAmount = vipServiceConfig.getDiscountPrice();
derateAmount = originalAmount.subtract(orderAmount);
//2.3 封装订单中商品明细列表
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(tradeVo.getItemId());
orderDetailVo.setItemName("VIP会员:" + vipServiceConfig.getName());
orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl());
//订单明细价格为原价
orderDetailVo.setItemPrice(originalAmount);
orderDetailVoList.add(orderDetailVo);
//2.4 封装优惠明细列表
if (originalAmount.compareTo(orderAmount) != 0) {
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
orderDerateVo.setDerateAmount(derateAmount);
orderDerateVo.setRemarks("VIP限时优惠价:" + orderAmount);
orderDerateVoList.add(orderDerateVo);
}
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
//3.TODO 处理购买项目类型为专辑 1001-专辑
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
//4.TODO 处理购买项目类型为声音 1002-声音
}
//5.封装订单确认页所需价格,商品,优惠,以及"其他信息"
//5.1 封装订单确认页中基本信息
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//封装其他信息
//TODO 付款方式暂不确定,不设置
//5.2 购买项目类型
orderInfoVo.setItemType(tradeVo.getItemType());
//5.3 本次订单流水号-后续提交订单避免订单重复提交
//5.3.1 生成流水号
String tradeNo = IdUtil.randomUUID();
//5.3.2 生成本次用户当前订单流水号key
String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
//5.3.3 存入Redis
redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES);
//5.3.4 封装本次订单流水号
orderInfoVo.setTradeNo(tradeNo);
//5.4 生成本次订单时间戳以及签名
orderInfoVo.setTimestamp(DateUtil.current());
//5.4.1 将目前订单VO对象转为Map对象 将付款方式payType参数排除掉
Map<String, Object> orderInfoVoMapParams = BeanUtil.beanToMap(orderInfoVo);
String sign = SignHelper.getSign(orderInfoVoMapParams);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
}
在service-util
微服务中添加远程 feign 远程调用拦截器,来获取token 数据。
如上图:因为微服务之间并没有传递头文件,所以我们可以定义一个拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的用户信息再放入到header中,再调用其他微服务即可。
package com.atguigu.tingshu.common.feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
public class FeignInterceptor implements RequestInterceptor {
public void apply(RequestTemplate requestTemplate){
// 获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//异步编排 与 MQ消费者端 为 null 避免出现空指针
if(null != requestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String token = request.getHeader("token");
requestTemplate.header("token", token);
}
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/90
service-user
微服务的UserInfoApiController控制器
/**
* 验证当前用户是否已购指定专辑ID
* @param albumId
* @return true:已购 false:未购
*/
@GuiGuLogin
@Operation(summary = "验证当前用户是否已购指定专辑ID")
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId){
//1.获取用户ID
Long userId = AuthContextHolder.getUserId();
//2.查询用户是否已购专辑
Boolean isPaid = userInfoService.isPaidAlbum(userId, albumId);
return Result.ok(isPaid);
}
UserInfoService接口
/**
* 判断指定用户ID是否购买过指定专辑
* @param userId
* @param albumId
* @return
*/
Boolean isPaidAlbum(Long userId, Long albumId);
UserInfoServiceImpl实现类
/**
* 判断指定用户ID是否购买过指定专辑
*
* @param userId
* @param albumId
* @return
*/
@Override
public Boolean isPaidAlbum(Long userId, Long albumId) {
//1.构建查询条件:用户ID+专辑ID
LambdaQueryWrapper<UserPaidAlbum> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidAlbum::getUserId, userId);
queryWrapper.eq(UserPaidAlbum::getAlbumId, albumId);
Long count = userPaidAlbumMapper.selectCount(queryWrapper);
//2.查询已购专辑数量判断
return count > 0;
}
service-user-client
远程调用 UserFeignClient 添加
/**
* 验证当前用户是否已购指定专辑ID
* @param albumId
* @return true:已购 false:未购
*/
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId);
UserDegradeFeignClient熔断类:
@Override
public Result<Boolean> isPaidAlbum(Long albumId) {
log.error("[用户服务]提供远程调用isPaidAlbum服务降级");
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/92
/**
* 用户下单返回渲染订单确认页面数据
* 为用户选择购买不同商品返回订单确认数据
* 1.VIP会员 2.专辑 3.声音
*
* @param tradeVo itemType->项目类型: 1001-专辑 1002-声音 1003-vip会员
* @return
*/
@Override
public OrderInfoVo getOrderTradeData(TradeVo tradeVo) {
Long userId = AuthContextHolder.getUserId();
//1.创建响应VO对象,将VO中需要相关价格,商品明细,优惠明细定义
OrderInfoVo orderInfoVo = new OrderInfoVo();
//1.1 原价:初始0.00
BigDecimal originalAmount = new BigDecimal("0.00");
//1.2 减免:初始0.00
BigDecimal derateAmount = new BigDecimal("0.00");
//1.3 订单:初始0.00
BigDecimal orderAmount = new BigDecimal("0.00");
//1.4 订单明细(商品)列表
List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
//1.5 订单优惠列表
List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
//2.处理购买项目类型为VIP会员 itemType=1003-vip会员 允许多次仅购买
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
//2.1 ...省略代码
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
//3.处理购买项目类型为专辑 itemType=1001-专辑
Long albumId = tradeVo.getItemId();
//3.1 远程调用“用户”服务判断是否已购专辑 已购买则抛出异常 true
Boolean isPaid = userFeignClient.isPaidAlbum(albumId).getData();
Assert.state(!isPaid, "请勿重复购买!");
//3.2 远程调用“`用户”服务判获取用户信息断是否VIP
UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData();
Assert.notNull(userInfoVo, "用户信息有误!");
Boolean isVIP = false;
if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(new Date())) {
isVIP = true;
}
//3.3 远程调用“专辑”服务获取专辑信息(价格,不同用户折扣)
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
Assert.notNull(albumInfo, "专辑信息有误!");
//3.4 根据专辑不同折扣计算不同用户身份价格
//原价
originalAmount = albumInfo.getPrice();
//暂时认为无折扣,订单价=原价
orderAmount = originalAmount;
//3.4.1 判断当前专辑是否有普通用户折扣 订单价=原价*折扣 100 * 8 / 10
if (albumInfo.getDiscount().intValue() != -1 && !isVIP) {
// 折扣存在*.**避免精度问题 设置保留小数位,设置四舍五入
orderAmount = originalAmount.multiply(albumInfo.getDiscount())
.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
}
//3.4.2 判断当前专辑是否有VIP用户折扣
if (albumInfo.getVipDiscount().intValue() != -1 && isVIP) {
orderAmount = originalAmount.multiply(albumInfo.getVipDiscount())
.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);
//3.6 封装商品优惠列表
if (originalAmount.compareTo(orderAmount) != 0) {
derateAmount = originalAmount.subtract(orderAmount);
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_ALBUM_DISCOUNT);
orderDerateVo.setDerateAmount(derateAmount);
orderDerateVo.setRemarks("专辑折扣,减免:" + derateAmount);
orderDerateVoList.add(orderDerateVo);
}
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
//4.TODO 处理购买项目类型为声音 1002-声音
}
//5.封装订单确认页所需价格,商品,优惠,以及"其他信息"
//5.1 封装订单确认页中基本信息
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//封装其他信息
//TODO 付款方式暂不确定,不设置
//5.2 购买项目类型
orderInfoVo.setItemType(tradeVo.getItemType());
//5.3 本次订单流水号-后续提交订单避免订单重复提交
//5.3.1 生成流水号
String tradeNo = IdUtil.randomUUID();
//5.3.2 生成本次用户当前订单流水号key
String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
//5.3.3 存入Redis
redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES);
//5.3.4 封装本次订单流水号
orderInfoVo.setTradeNo(tradeNo);
//5.4 生成本次订单时间戳以及签名
orderInfoVo.setTimestamp(DateUtil.current());
//5.4.1 将目前订单VO对象转为Map对象 将付款方式payType参数值为空排除掉
Map<String, Object> orderInfoVoMapParams = BeanUtil.beanToMap(orderInfoVo, false, true);
//5.4.2 调用工具类对以后参数进行签名得到签名值
String sign = SignHelper.getSign(orderInfoVoMapParams);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/94
service-user
模块
/**
* 查询用户某个专辑已购声音ID列表
* @param albumId
* @return
*/
@GuiGuLogin
@Operation(summary = "查询用户某个专辑已购声音ID列表")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackIdList(@PathVariable Long albumId){
//1.获取用户ID
Long userId = AuthContextHolder.getUserId();
//2.根据用户ID+专辑ID查询已购声音列表列表
List<Long> list = userInfoService.getUserPaidTrackIdList(userId, albumId);
return Result.ok(list);
}
/**
* 查询指定用户某个专辑已购声音ID列表
* @param albumId
* @return
*/
List<Long> getUserPaidTrackIdList(Long userId, Long albumId);
/**
* 查询指定用户某个专辑已购声音ID列表
* @param albumId
* @return
*/
@Override
public List<Long> getUserPaidTrackIdList(Long userId, Long albumId) {
//1.构建查询条件 用户ID+专辑ID查询声音列 故新建复合索引:包含用户ID,专辑ID,声音ID
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getUserId, userId);
queryWrapper.eq(UserPaidTrack::getAlbumId, albumId);
queryWrapper.select(UserPaidTrack::getTrackId);
//2.得到已购声音列表
List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(queryWrapper);
if(CollectionUtil.isNotEmpty(userPaidTrackList)){
//3.获取已购声音ID列表
List<Long> userPaidTrackIdList = userPaidTrackList.stream()
.map(UserPaidTrack::getTrackId)
.collect(Collectors.toList());
return userPaidTrackIdList;
}
return null;
}
service-user-client
模块中UserFeignClient提供Feign接口
/**
* 查询用户某个专辑已购声音ID列表
* @param albumId
* @return
*/
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackIdList(@PathVariable Long albumId);
UserDegradeFeignClient熔断类
@Override
public Result<List<Long>> getUserPaidTrackIdList(Long albumId) {
log.error("[用户服务]提供远程调用getUserPaidTrackIdList服务降级");
return null;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/95
控制器返回数据格式如下: 因为当前专辑只支持单集购买!专辑的价格是 album_info.price --> 当前专辑中的一条声音的价格 声音总价= album_info.price*trackCount
album_info 表中 price_type类型分为: 0201-单集 0202-整专辑
Map<String, Object> map = new HashMap<>();
map.put("name","本集"); // 显示文本
map.put("price",albumInfo.getPrice()); // 专辑声音对应的价格
map.put("trackCount",1); // 记录购买集数
list.add(map);
/**
* 根据选择待购声音作为购买起始声音得到分集购买列表
*
* @param trackId 选择付费标识声音-作为起始购买声音
* @return [{name:"本集",price:1,trackCount:1},{name:"后10集",price:10,trackCount:10},{}.,{name:"全集",price:33,trackCount:33}]
*/
@GuiGuLogin
@Operation(summary = "根据选择待购声音作为购买起始声音得到分集购买列表")
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> getFenJiBuyList(@PathVariable Long trackId) {
Long userId = AuthContextHolder.getUserId();
List<Map<String, Object>> list = trackInfoService.getFenJiBuyList(userId, trackId);
return Result.ok(list);
}
/**
* 根据选择待购声音作为购买起始声音得到分集购买列表
*
* @param trackId 选择付费标识声音-作为起始购买声音
* @return [{name:"本集",price:1,trackCount:1},{name:"后10集",price:10,trackCount:10},{}.,{name:"全集",price:33,trackCount:33}]
*/
List<Map<String, Object>> getFenJiBuyList(Long userId, Long trackId);
思路:
@Autowired
private UserFeignClient userFeignClient;
/**
* 根据选择待购声音作为购买起始声音得到分集购买列表
*
* @param trackId 选择付费标识声音-作为起始购买声音
* @return [{name:"本集",price:1,trackCount:1},{name:"后10集",price:10,trackCount:10},{}.,{name:"全集",price:33,trackCount:33}]
*/
@Override
public List<Map<String, Object>> getFenJiBuyList(Long userId, Long trackId) {
//1.根据起始购买声音ID查询选中待购声音信息-得到专辑ID,声音序号
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
Assert.notNull(trackId, "选择声音有误!");
Long albumId = trackInfo.getAlbumId();
Integer orderNum = trackInfo.getOrderNum();
//2.根据专辑ID(等值)+序号(大于等于)得到“待购买”声音列表(可能包含用户已购声音)
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getAlbumId, albumId);
queryWrapper.ge(TrackInfo::getOrderNum, orderNum);
queryWrapper.select(TrackInfo::getId);
List<TrackInfo> userWaitBuyTrackList = trackInfoMapper.selectList(queryWrapper);
Assert.notNull(userWaitBuyTrackList, "不存在待购声音");
//3.远程调用“用户服务”得到已购声音ID列表
//3.1 获取“待购买”声音ID列表(可能包含用户已购声音ID)
List<Long> userWaitBuyTrackIdList = userWaitBuyTrackList.stream().map(TrackInfo::getId).collect(Collectors.toList());
//3.2 远程调用用户服务得到已购声音ID列表
List<Long> userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(albumId).getData();
//4.将"待购买"声音列表中用户已购声音排除掉得到真正“待购买”声音列表
if (CollectionUtil.isNotEmpty(userPaidTrackIdList)) {
userWaitBuyTrackIdList = userWaitBuyTrackIdList
.stream()
.filter(userWaitBuyTrackId -> !userPaidTrackIdList.contains(userWaitBuyTrackId))
.collect(Collectors.toList());
}
//5.根据待购声音列表长度构建分集购买对象集合
List<Map<String, Object>> list = new ArrayList<>();
int count = userWaitBuyTrackIdList.size();
//5.1 获取声音价格(专辑中价格)
AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
BigDecimal price = albumInfo.getPrice();
//5.1 构建本集分集购买对象
Map<String, Object> currentMap = new HashMap<>();
currentMap.put("name", "本集");
currentMap.put("price", price);
currentMap.put("trackCount", 1);
list.add(currentMap);
//5.2 构建其他分集购买对象 count=25 本集 后10集 后20集 全集 cuont=20 本集 后10集 全集 count=45 本集 后10|20|30|40集 全集
//第一次循环i=10 第二次循环i=20
for (int i = 10; i <= 50; i += 10) {
if (count > i) {
//待购买声音数量>i 显示 后i集
Map<String, Object> map = new HashMap<>();
map.put("name", "后" + i + "集");
map.put("price", price.multiply(BigDecimal.valueOf(i)));
map.put("trackCount", i);
list.add(map);
} else {
//显示 全集
Map<String, Object> map = new HashMap<>();
map.put("name", "全(" + count + ")集");
map.put("price", price.multiply(BigDecimal.valueOf(count)));
map.put("trackCount", count);
list.add(map);
break;
}
}
return list;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/96
service-album
微服务中的TrackInfoApiController控制器添加代码
/**
* 基于选择购买声音ID返回当前用户待结算声音列表
* @param trackId
* @param trackCount
* @return
*/
@GuiGuLogin
@Operation(summary = "基于选择购买声音ID返回当前用户待结算声音列表")
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getWaitBuyTrackInfoList(@PathVariable Long trackId, @PathVariable int trackCount){
Long userId = AuthContextHolder.getUserId();
List<TrackInfo> list = trackInfoService.getWaitBuyTrackInfoList(userId, trackId, trackCount);
return Result.ok(list);
}
/**
* 基于选择购买声音ID返回当前用户待结算声音列表
* @param userId 用户ID
* @param trackId 选中声音ID(起始购买声音ID)
* @param trackCount 购买数量
* @return 与购买数量对应数量声音集合
*/
List<TrackInfo> getWaitBuyTrackInfoList(Long userId, Long trackId, int trackCount);
思路:
1. 根据声音Id 获取到当前声音对象
2. 远程调用用户服务获取已购买声音ID列表
3. 根据声音ID+当前声音序号查询待购声音列表,将已购声音进行排除
4. 返回待购声音集合对象返回
/**
* 基于选择购买声音ID返回当前用户待结算声音列表
*
* @param userId 用户ID
* @param trackId 选中声音ID(起始购买声音ID)
* @param trackCount 购买数量
* @return 与购买数量对应数量声音集合
*/
@Override
public List<TrackInfo> getWaitBuyTrackInfoList(Long userId, Long trackId, int trackCount) {
//1.根据选中购买声音ID得到声音所属专辑ID&序号
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
Long albumId = trackInfo.getAlbumId();
Integer orderNum = trackInfo.getOrderNum();
//2.远程调用“用户服务”得到已购声音ID列表
List<Long> userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(albumId).getData();
//3.根据专辑ID(等值)&序号(大于等于)& 声音ID(不存在已购声音ID列表),序号(升序)限制trackCount条返回待结算声音列表
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
// 3.1 专辑等值查询
queryWrapper.eq(TrackInfo::getAlbumId, albumId);
// 3.1 序号大于等于用户选择购买声音序号
queryWrapper.ge(TrackInfo::getOrderNum, orderNum);
if (CollectionUtil.isNotEmpty(userPaidTrackIdList)) {
//说明该专辑下存在已购声音
queryWrapper.notIn(TrackInfo::getId, userPaidTrackIdList);
}
//todo limit 后加空格 3.3 限制返回数量
queryWrapper.last("limit " + trackCount);
//3.4 按照序号进行升序
queryWrapper.orderByAsc(TrackInfo::getOrderNum);
//3.5 指定返回列
queryWrapper.select(TrackInfo::getId, TrackInfo::getTrackTitle, TrackInfo::getAlbumId, TrackInfo::getCoverUrl);
List<TrackInfo> trackInfoList = trackInfoMapper.selectList(queryWrapper);
return trackInfoList;
}
AlbumFeignClient
/**
* 基于选择购买声音ID返回当前用户待结算声音列表
* @param trackId
* @param trackCount
* @return
*/
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getWaitBuyTrackInfoList(@PathVariable Long trackId, @PathVariable int trackCount);
熔断类:
@Override
public Result<List<TrackInfo>> getWaitBuyTrackInfoList(Long trackId, int trackCount) {
log.error("[专辑服务]远程调用getWaitBuyTrackInfoList执行服务降级");
return null;
}
/**
* 获取订单确认页面所需数据
* - 购买VIP套餐
* - 购买专辑
* - 购买声音
*
* @param tradeVo {itemType:"项目类型",:itemId:"项目",trackCount:"声音数量"}
* @return 订单vo对象->为后续提交订单准备
*/
@Override
public OrderInfoVo getOrderTradeData(TradeVo tradeVo) {
Long userId = AuthContextHolder.getUserId();
//1.创建订单信息VO对象
OrderInfoVo orderInfoVo = new OrderInfoVo();
//1.1 声明默认价格 TODO:一定要传入字符串必须是 "0.00"
//1.1.1 "商品"原价
BigDecimal originalAmount = new BigDecimal("0.00");
//1.1.2 "商品"减免价
BigDecimal derateAmount = new BigDecimal("0.00");
//1.1.3 "商品"订单价
BigDecimal orderAmount = new BigDecimal("0.00");
//1.2 声明商品列表集合
List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
//1.3 声明优惠列表集合
List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
//2.处理VIP会员(允许重复) 1001-专辑 1002-声音 1003-vip会员
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
//省略代码...
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
//3. 处理专辑(不允许重复购买)省略代码...
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
//4.处理购买项目类型为声音 1002-声音
//4.1 远程调用专辑服务得到待结算声音列表
Long trackId = tradeVo.getItemId();
List<TrackInfo> waitBuyTrackInfoList = albumFeignClient.getWaitBuyTrackInfoList(trackId, tradeVo.getTrackCount()).getData();
Assert.notNull(waitBuyTrackInfoList, "无符合要求结算商品");
//4.2 远程调用“专辑服务”获取专辑信息得到声音价格
Long albumId = waitBuyTrackInfoList.get(0).getAlbumId();
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
BigDecimal price = albumInfo.getPrice();
//4.3 基于待结算声音列表 计算订单价格
originalAmount = price.multiply(BigDecimal.valueOf(waitBuyTrackInfoList.size()));
orderAmount = originalAmount;
//4.4 基于待结算声音列表 封装商品明细列表
orderDetailVoList = waitBuyTrackInfoList
.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.封装订单确认页所需价格,商品,优惠,以及"其他信息"
//5.1 封装订单确认页中基本信息
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//封装其他信息
//TODO 付款方式暂不确定,不设置
//5.2 购买项目类型
orderInfoVo.setItemType(tradeVo.getItemType());
//5.3 本次订单流水号-后续提交订单避免订单重复提交
//5.3.1 生成流水号
String tradeNo = IdUtil.randomUUID();
//5.3.2 生成本次用户当前订单流水号key
String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
//5.3.3 存入Redis
redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES);
//5.3.4 封装本次订单流水号
orderInfoVo.setTradeNo(tradeNo);
//5.4 生成本次订单时间戳以及签名
orderInfoVo.setTimestamp(DateUtil.current());
//5.4.1 将目前订单VO对象转为Map对象 将付款方式payType参数值为空排除掉
Map<String, Object> orderInfoVoMapParams = BeanUtil.beanToMap(orderInfoVo, false, true);
//5.4.2 调用工具类对以后参数进行签名得到签名值
String sign = SignHelper.getSign(orderInfoVoMapParams);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
声音购买-仅支持余额付款:
专辑购买:
VIP 购买:
分集购买声音只支持余额支付
购买VIP或专辑分为余额,微信支付
这里采用Seata提供AT模式进行分布式事务管理。
分别在tingshu_account
、tingshu_order
、tingshu_user
三个数据库中新增undo_log日志表
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3;
分别在service-order
、service-account
、service-user
三个模块pom.xml中增加Seata启动依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!-- 默认seata客户端版本比较低,排除后重新引入指定版本-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
订单微服务service-order
模块新增Seata配置
seata:
enabled: true
tx-service-group: ${spring.application.name}-group # 事务组名称
service:
vgroup-mapping:
#指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
service-order-group: default
registry:
type: nacos # 使用nacos作为注册中心
nacos:
server-addr: 192.168.200.6:8848 # nacos服务地址
group: DEFAULT_GROUP # 默认服务分组
namespace: "" # 默认命名空间
cluster: default # 默认TC集群名称
账户微服务service-account
模块新增Seata配置
seata:
enabled: true
tx-service-group: ${spring.application.name}-group # 事务组名称
service:
vgroup-mapping:
#指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
service-account-group: default
registry:
type: nacos # 使用nacos作为注册中心
nacos:
server-addr: 192.168.200.6:8848 # nacos服务地址
group: DEFAULT_GROUP # 默认服务分组
namespace: "" # 默认命名空间
cluster: default # 默认TC集群名称
用户微服务service-user
模块新增Seata配置
seata:
enabled: true
tx-service-group: ${spring.application.name}-group # 事务组名称
service:
vgroup-mapping:
#指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
service-user-group: default
registry:
type: nacos # 使用nacos作为注册中心
nacos:
server-addr: 192.168.200.6:8848 # nacos服务地址
group: DEFAULT_GROUP # 默认服务分组
namespace: "" # 默认命名空间
cluster: default # 默认TC集群名称
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/127
service-account
账户服务UserAccountApiController
/**
* @param accountDeductVo
* @return
*/
@GuiGuLogin(required = false)
@Operation(summary = "检查并扣减账户余额")
@PostMapping("/userAccount/checkAndDeduct")
public Result checkAndDeduct(@RequestBody AccountDeductVo accountDeductVo) {
//1.优先使用VO中用户ID
if (accountDeductVo.getUserId() == null) {
accountDeductVo.setUserId(AuthContextHolder.getUserId());
}
//2.调用业务逻辑检查扣减余额
userAccountService.checkAndDeduct(accountDeductVo);
return Result.ok();
}
业务接口UserAccountService
/**
* 检查并扣减账户余额
* @param accountDeductVo
*/
void checkAndDeduct(AccountDeductVo accountDeductVo);
业务实现类UserAccountService
/**
* 检查并扣减账户余额
*
* @param accountDeductVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void checkAndDeduct(AccountDeductVo accountDeductVo) {
//1.检查并扣减账户余额
int count = userAccountMapper.checkAndDeduct(accountDeductVo.getUserId(), accountDeductVo.getAmount());
if (count == 0) {
//分之事务发生异常,上报异常到TC
throw new GuiguException(ResultCodeEnum.ACCOUNT_MINUSLOCK_ERROR);
}
//2.新增账户变动日志
this.saveUserAccountDetail(accountDeductVo.getUserId(), accountDeductVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountDeductVo.getAmount(), accountDeductVo.getOrderNo());
}
UserAccountMapper
package com.atguigu.tingshu.account.mapper;
import com.atguigu.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface UserAccountMapper extends BaseMapper<UserAccount> {
/**
* 检查并扣减账户余额
* @param userId
* @param amount
* @return
*/
int checkAndDeduct(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}
UserAccountMapper.xml
<!--扣减余额-->
<update id="checkAndDeduct">
UPDATE user_account set total_amount = total_amount - #{amount}, available_amount = available_amount - #{amount}, total_pay_amount = total_pay_amount + #{amount}
WHERE user_id = #{userId} and available_amount >= #{amount} and is_deleted = 0
</update>
在service-account-client
模块中提供Feign接口:AccountFeignClient
package com.atguigu.tingshu.account;
import com.atguigu.tingshu.account.impl.AccountDegradeFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountDeductVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* <p>
* 账号模块远程调用API接口
* </p>
*
* @author atguigu
*/
@FeignClient(value = "service-account",path = "api/account", fallback = AccountDegradeFeignClient.class)
public interface AccountFeignClient {
/**
* @param accountDeductVo
* @return
*/
@PostMapping("/userAccount/checkAndDeduct")
public Result checkAndDeduct(@RequestBody AccountDeductVo accountDeductVo);
}
服务降级类:AccountDegradeFeignClient
package com.atguigu.tingshu.account.impl;
import com.atguigu.tingshu.account.AccountFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountDeductVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AccountDegradeFeignClient implements AccountFeignClient {
@Override
public Result checkAndDeduct(AccountDeductVo accountDeductVo) {
log.error("[账户服务]提供远程调用checkAndDeduct服务降级");
return null;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/118
service-user
模块中UserInfoApiController提供处理购买记录Restful接口
UserInfoApiController控制器
/**
* 支付(余额,微信)成功后虚拟物品发货
*
* @param userPaidRecordVo
* @return
*/
@GuiGuLogin(required = false)
@Operation(summary = "支付(余额,微信)成功后虚拟物品发货")
@PostMapping("/userInfo/savePaidRecord")
public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo) {
//1.优先使用VO对象中用户ID
if (userPaidRecordVo.getItemIdList() == null) {
userPaidRecordVo.setUserId(AuthContextHolder.getUserId());
}
//2.调用业务逻辑虚拟物品发货(新增购买记录)
userInfoService.savePaidRecord(userPaidRecordVo);
return Result.ok();
}
UserInfoService业务接口
/**
* 支付(余额,微信)成功后虚拟物品发货
*
* @param userPaidRecordVo
* @return
*/
void savePaidRecord(UserPaidRecordVo userPaidRecordVo);
业务实现类UserInfoServiceImpl
@Autowired
private AlbumFeignClient albumFeignClient;
@Autowired
private UserVipServiceMapper userVipServiceMapper;
@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;
/**
* 支付(余额,微信)成功后虚拟物品发货
* 可能会存在三种项目类型:1001-专辑 1002-声音 1003-vip会员
*
* @param userPaidRecordVo
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
String itemType = userPaidRecordVo.getItemType();
//1.处理购买项目类型为:专辑
if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
//1.1 已购专辑表中 订单编号 有唯一约束,避免某比订单购买记录被重复保存
UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
//1.2 保存专辑购买记录
userPaidAlbumMapper.insert(userPaidAlbum);
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) {
//2.处理购买项目类型为:声音 一个订单包含多个声音
//2.1 根据订单编号查询购买记录判断是否已处理
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
queryWrapper.last("limit 1");
queryWrapper.select(UserPaidTrack::getOrderNo);
Long count = userPaidTrackMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//2.2 创建已购声音对象集合保存
TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
for (Long trackId : userPaidRecordVo.getItemIdList()) {
UserPaidTrack userPaidTrack = new UserPaidTrack();
userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
userPaidTrack.setUserId(userPaidRecordVo.getUserId());
userPaidTrack.setAlbumId(trackInfo.getAlbumId());
userPaidTrack.setTrackId(trackId);
userPaidTrackMapper.insert(userPaidTrack);
}
} else if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
//3.处理购买项目类型为:VIP会员
Long vipConfigId = userPaidRecordVo.getItemIdList().get(0);
VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(vipConfigId);
//得到会员月
Integer serviceMonth = vipServiceConfig.getServiceMonth();
//3.1 获取当前用户身份,确定新开会员,续费会员
Boolean isVIP = false;
UserInfoVo userInfoVo = this.getUserInfo(userPaidRecordVo.getUserId());
if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(new Date())) {
//当前用户是VIP
isVIP = true;
}
//3.2 创建VIP购买记录将记录保存
UserVipService userVipService = new UserVipService();
//3.2.1 计算本次会员开始及过期时间
if (isVIP) {
//续费会员,本次会员开始时间=用户表中会员过期时间
userVipService.setStartTime(userInfoVo.getVipExpireTime());
userVipService.setExpireTime(DateUtil.offsetMonth(userInfoVo.getVipExpireTime(), serviceMonth));
} else {
//新开会员
userVipService.setStartTime(new Date());
userVipService.setExpireTime(DateUtil.offsetMonth(new Date(), serviceMonth));
}
//3.2.2 封装其他属性
userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
userVipService.setUserId(userPaidRecordVo.getUserId());
//3.2.3 保存VIP购买记录
userVipServiceMapper.insert(userVipService);
//3.3 更新用户会员标识及过期时间
userInfoVo.setIsVip(1);
userInfoVo.setVipExpireTime(userVipService.getExpireTime());
userInfoMapper.updateById(BeanUtil.copyProperties(userInfoVo, UserInfo.class));
}
}
在service-user
模块中Feign接口提供远程调用方法:UserFeignClient
/**
* 提交订单支付(余额,支付宝)成功-虚拟物品发货
*
* @param userPaidRecordVo
* @return
*/
@PostMapping("/userInfo/savePaidRecord")
public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo);
服务降级类:UserDegradeFeignClient
@Override
public Result savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.error("远程调用用户服务savePaidRecord服务降级");
return null;
}
策略模式(Strategy Pattern)又叫政策模式(Policy Pattern),它是将定义的算法家族分别封装起来,让它们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户。属于行为型模式。
策略模式使用的就是面向对象的继承和多态机制,从而实现同一行为在不同场景下具备不同实现。
优点:
缺点:
策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
环境(Context)类:用来操作策略的上下文环境,屏蔽高层模块(客户端)对策略、算法的直接访问,封装可能存在的变化。
策略接口
package com.atguigu.tingshu.user.pattern;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
/**
* 抽象策略接口
*/
public interface UserPaidStrategy {
/**
* 处理购买记录接口方法
* @param userPaidRecordVo
*/
void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo);
}
VIP购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致
package com.atguigu.tingshu.user.pattern.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.model.user.UserVipService;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.mapper.UserVipServiceMapper;
import com.atguigu.tingshu.user.mapper.VipServiceConfigMapper;
import com.atguigu.tingshu.user.pattern.UserPaidStrategy;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author: atguigu
* @create: 2024-04-23 14:48
*/
@Slf4j
@Component(SystemConstant.ORDER_ITEM_TYPE_VIP) //beanid=类名(首字母小写)
public class VIPPaidStrategy implements UserPaidStrategy {
@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;
@Autowired
private UserVipServiceMapper userVipServiceMapper;
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 处理VIP会员购买记录
*
* @param userPaidRecordVo
*/
@Override
public void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.info("处理VIP会员购买记录");
//3.处理购买项目类型为:VIP会员
Long vipConfigId = userPaidRecordVo.getItemIdList().get(0);
VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(vipConfigId);
//得到会员月
Integer serviceMonth = vipServiceConfig.getServiceMonth();
//3.1 获取当前用户身份,确定新开会员,续费会员
Boolean isVIP = false;
UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
if (userInfo.getIsVip().intValue() == 1 && userInfo.getVipExpireTime().after(new Date())) {
//当前用户是VIP
isVIP = true;
}
//3.2 创建VIP购买记录将记录保存
UserVipService userVipService = new UserVipService();
//3.2.1 计算本次会员开始及过期时间
if (isVIP) {
//续费会员,本次会员开始时间=用户表中会员过期时间
userVipService.setStartTime(userInfo.getVipExpireTime());
userVipService.setExpireTime(DateUtil.offsetMonth(userInfo.getVipExpireTime(), serviceMonth));
} else {
//新开会员
userVipService.setStartTime(new Date());
userVipService.setExpireTime(DateUtil.offsetMonth(new Date(), serviceMonth));
}
//3.2.2 封装其他属性
userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
userVipService.setUserId(userPaidRecordVo.getUserId());
//3.2.3 保存VIP购买记录
userVipServiceMapper.insert(userVipService);
//3.3 更新用户会员标识及过期时间
userInfo.setIsVip(1);
userInfo.setVipExpireTime(userVipService.getExpireTime());
userInfoMapper.updateById(userInfo);
}
}
专辑购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致
package com.atguigu.tingshu.user.pattern.impl;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.user.UserPaidAlbum;
import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper;
import com.atguigu.tingshu.user.pattern.UserPaidStrategy;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: atguigu
* @create: 2024-04-23 14:48
*/
@Slf4j
@Component(SystemConstant.ORDER_ITEM_TYPE_ALBUM) //beanid=类名(首字母小写)
public class AlbumPaidStrategy implements UserPaidStrategy {
@Autowired
private UserPaidAlbumMapper userPaidAlbumMapper;
/**
* 处理专辑购买记录
*
* @param userPaidRecordVo
*/
@Override
public void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.info("处理专辑购买记录");
//1.1 已购专辑表中 订单编号 有唯一约束,避免某比订单购买记录被重复保存
UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
//1.2 保存专辑购买记录
userPaidAlbumMapper.insert(userPaidAlbum);
}
}
声音购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致
package com.atguigu.tingshu.user.pattern.impl;
import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.user.UserPaidTrack;
import com.atguigu.tingshu.user.mapper.UserPaidTrackMapper;
import com.atguigu.tingshu.user.pattern.UserPaidStrategy;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: atguigu
* @create: 2024-04-23 14:48
*/
@Slf4j
@Component(SystemConstant.ORDER_ITEM_TYPE_TRACK) //beanid=类名(首字母小写)
public class TrackPaidStrategy implements UserPaidStrategy {
@Autowired
UserPaidTrackMapper userPaidTrackMapper;
@Autowired
AlbumFeignClient albumFeignClient;
/**
* 处理声音购买记录
*
* @param userPaidRecordVo
*/
@Override
public void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo) {
log.info("处理声音购买记录");
//2.处理购买项目类型为:声音 一个订单包含多个声音
//2.1 根据订单编号查询购买记录判断是否已处理
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
queryWrapper.last("limit 1");
queryWrapper.select(UserPaidTrack::getOrderNo);
Long count = userPaidTrackMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//2.2 创建已购声音对象集合保存
TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
for (Long trackId : userPaidRecordVo.getItemIdList()) {
UserPaidTrack userPaidTrack = new UserPaidTrack();
userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
userPaidTrack.setUserId(userPaidRecordVo.getUserId());
userPaidTrack.setAlbumId(trackInfo.getAlbumId());
userPaidTrack.setTrackId(trackId);
userPaidTrackMapper.insert(userPaidTrack);
}
}
}
提供工厂类组装所有的策略实现类
package com.atguigu.tingshu.user.pattern.fac;
import com.atguigu.tingshu.user.pattern.UserPaidStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author: atguigu
* @create: 2024-04-23 14:52
*/
@Slf4j
@Component
public class StrategyFactory {
/**
* Spring注入类型如果是Map 自动将Map中Value类型对象注入到Map
* Key:BeanID,Value:实现类对象
*/
@Autowired
private Map<String, UserPaidStrategy> strategyMap;
/**
* 根据购买项目类型获取不同策略实现类对象
* @param itemType
* @return
*/
public UserPaidStrategy getPaidStrategy(String itemType){
if (strategyMap.containsKey(itemType)) {
return strategyMap.get(itemType);
}
throw new RuntimeException("该项目类型不支持");
}
}
UserInfoServiceImpl业务调用工厂获取不同策略实现类完成方法调用
@Autowired
private StrategyFactory strategyFactory;
/**
* 支付(余额,微信)成功后虚拟物品发货
* 可能会存在三种项目类型:1001-专辑 1002-声音 1003-vip会员
*
* @param userPaidRecordVo
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
String itemType = userPaidRecordVo.getItemType();
//1.根据购买项目类型获取该购买项目策略实现类对象
UserPaidStrategy paidStrategy = strategyFactory.getPaidStrategy(itemType);
//2.处理不同购买项目类型
paidStrategy.handlerPaidRecord(userPaidRecordVo);
}
在service-order
微服务中添加提交订单控制器
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/99
OrderInfoApiController
/**
* 当前用户提交订单
*
* @param orderInfoVo 订单VO信息
* @return {"orderNo":"订单唯一编号"}
*/
@GuiGuLogin
@Operation(summary = "提交订单")
@PostMapping("/orderInfo/submitOrder")
public Result<Map<String, String>> submitOrder(@RequestBody OrderInfoVo orderInfoVo) {
Long userId = AuthContextHolder.getUserId();
Map<String, String> map = orderInfoService.submitOrder(userId, orderInfoVo);
return Result.ok(map);
}
/**
* 当前用户提交订单
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return {"orderNo":"订单唯一编号"}
*/
Map<String, String> submitOrder(Long userId, OrderInfoVo orderInfoVo);
/**
* 保存订单,订单明细,优惠明细
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return
*/
OrderInfo saveOrderInfo(Long userId, OrderInfoVo orderInfoVo);
@Autowired
private AccountFeignClient accountFeignClient;
/**
* 当前用户提交订单
*
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return {"orderNo":"订单唯一编号"}
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Map<String, String> submitOrder(Long userId, OrderInfoVo orderInfoVo) {
//1.业务校验-验证流水号-避免回退导致重复提交
//1.1 从Redis中获取当前用户订单流水号
String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
//1.2 判断用户提交流水号跟存在Redis中是否一致
String scriptText = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(scriptText, Boolean.class);
Boolean flag = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(tradeKey), orderInfoVo.getTradeNo());
if (!flag) {
throw new GuiguException(500, "流水号参数有误!");
}
//2.业务校验-验证签名-避免订单中数据被篡改
//2.1 将提交OrderInfoVO对象转为Map 当初在生成签名"payWay"没有参与签名 故也要将支付方式排除掉
Map<String, Object> mapParams = BeanUtil.beanToMap(orderInfoVo);
mapParams.remove("payWay");
//2.2 对再次提交订单参数进行一次加密得到新签名 跟 提交参数中旧签名比较
SignHelper.checkSign(mapParams);
//3.保存订单及订单明细以及优惠明细(未支付)
OrderInfo orderInfo = this.saveOrderInfo(userId, orderInfoVo);
//4.判断用户选择支付方式(VIP/专辑选择余额。声音仅支持余额支付)
if (SystemConstant.ORDER_PAY_ACCOUNT.equals(orderInfoVo.getPayWay())) {
//5.远程调用"账户"服务扣减余额
//5.1 构建扣减余额VO对象
AccountDeductVo accountDeductVo = new AccountDeductVo();
accountDeductVo.setOrderNo(orderInfo.getOrderNo());
accountDeductVo.setUserId(orderInfo.getUserId());
accountDeductVo.setAmount(orderInfo.getOrderAmount());
accountDeductVo.setContent(orderInfo.getOrderTitle());
//5.2 远程调用完成扣减余额
Result result = accountFeignClient.checkAndDeduct(accountDeductVo);
//5.3 所有模块都采用全局异常处理方法,即使目标接口发生异常 全局异常处理类 仍然会返回Result,系统无法感知异常
if (!ResultCodeEnum.SUCCESS.getCode().equals(result.getCode())) {
throw new GuiguException(result.getCode(), result.getMessage());
}
//6.修改订单支付状态:已支付
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
orderInfoMapper.updateById(orderInfo);
//7.远程调用"用户"服务完成虚拟物品发货
//7.1 构建虚拟物品发货VO对象
UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
userPaidRecordVo.setOrderNo(orderInfo.getOrderNo());
userPaidRecordVo.setUserId(orderInfo.getUserId());
userPaidRecordVo.setItemType(orderInfoVo.getItemType());
List<Long> itemIdList = orderInfoVo.getOrderDetailVoList().stream().map(OrderDetailVo::getItemId).collect(Collectors.toList());
userPaidRecordVo.setItemIdList(itemIdList);
//7.2 远程调用完成虚拟物品发货
result = userFeignClient.savePaidRecord(userPaidRecordVo);
if (!ResultCodeEnum.SUCCESS.getCode().equals(result.getCode())) {
throw new GuiguException(result.getCode(), result.getMessage());
}
} else {
//8.TODO 选择微信支付,发送延迟任务,延迟关闭超时未支付订单
}
//9.返回订单编号
Map<String, String> map = new HashMap<>();
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
业务处理:
OrderInfoService接口
/**
* 保存订单相关信息
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return
*/
OrderInfo saveOrder(Long userId, OrderInfoVo orderInfoVo);
OrderInfoServiceImpl实现类
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private OrderDerateMapper orderDerateMapper;
/**
* 保存订单,订单明细,优惠明细
*
* @param userId 用户ID
* @param orderInfoVo 订单VO信息
* @return
*/
@Override
public OrderInfo saveOrderInfo(Long userId, OrderInfoVo orderInfoVo) {
//1.保存订单
//1.1 将订单VO转为订单PO对象
OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class);
//1.2 封装简单属性 :用户ID 订单状态(未支付) 订单内容
orderInfo.setUserId(userId);
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
if (CollectionUtil.isNotEmpty(orderDetailVoList)) {
String itemName = orderDetailVoList.get(0).getItemName();
orderInfo.setOrderTitle(itemName);
}
//1.3 产生订单唯一编号
String today = DateUtil.today().replaceAll("-", "");
String orderNo = today + IdUtil.getSnowflakeNextId();
orderInfo.setOrderNo(orderNo);
//1.4 保存订单得到订单ID
orderInfoMapper.insert(orderInfo);
Long orderId = orderInfo.getId();
//2.保存订单明细
if (CollectionUtil.isNotEmpty(orderDetailVoList)) {
orderDetailVoList.
stream()
.forEach(orderDetailVo -> {
OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class);
orderDetail.setOrderId(orderId);
orderDetailMapper.insert(orderDetail);
});
}
//3.保存优惠明细
List<OrderDerateVo> orderDerateVoList = orderInfoVo.getOrderDerateVoList();
if (CollectionUtil.isNotEmpty(orderDerateVoList)) {
orderDerateVoList.
stream()
.forEach(orderDerateVo -> {
OrderDerate orderDerate = BeanUtil.copyProperties(orderDerateVo, OrderDerate.class);
orderDerate.setOrderId(orderId);
orderDerateMapper.insert(orderDerate);
});
}
return orderInfo;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/100
当支付成功之后,点击查看订单
在service-order
微服务OrderInfoApiController中添加
/**
* 根据订单编号查询订单信息(包含订单明细及优惠明细)
*
* @param orderNo
* @return
*/
@Operation(summary = "根据订单编号查询订单信息")
@GetMapping("/orderInfo/getOrderInfo/{orderNo}")
public Result<OrderInfo> getOrderInfo(@PathVariable String orderNo) {
OrderInfo orderInfo = orderInfoService.getOrderInfo(orderNo);
return Result.ok(orderInfo);
}
OrderInfoService接口
/**
* 根据订单编号查询订单信息
* @param orderNo
* @return
*/
OrderInfo getOrderInfo(String orderNo);
OrderInfoServiceImpl实现
/**
* 根据订单编号查询订单信息
*
* @param orderNo
* @return
*/
@Override
public OrderInfo getOrderInfo(String orderNo) {
//1.根据订单编号查询订单信息
LambdaUpdateWrapper<OrderInfo> orderInfoLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
orderInfoLambdaUpdateWrapper.eq(OrderInfo::getOrderNo, orderNo);
OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoLambdaUpdateWrapper);
if (orderInfo != null) {
//2.根据订单ID查询订单明细列表
LambdaUpdateWrapper<OrderDetail> orderDetailLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
orderDetailLambdaUpdateWrapper.eq(OrderDetail::getOrderId, orderInfo.getId());
List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailLambdaUpdateWrapper);
if (CollectionUtil.isNotEmpty(orderDetailList)) {
orderInfo.setOrderDetailList(orderDetailList);
}
//3.根据订单ID查询订单优惠列表
LambdaUpdateWrapper<OrderDerate> orderDerateLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
orderDerateLambdaUpdateWrapper.eq(OrderDerate::getOrderId, orderInfo.getId());
List<OrderDerate> orderDerateList = orderDerateMapper.selectList(orderDerateLambdaUpdateWrapper);
if (CollectionUtil.isNotEmpty(orderDerateList)) {
orderInfo.setOrderDerateList(orderDerateList);
}
}
return orderInfo;
}
需要展示订单支付状态、支付方式,固在OrderInfo中增加对应属性get方法即可保证响应JSON中有对应属性
public String getOrderStatusName() {
if("0901".equals(orderStatus)){
return "未支付";
} else if ("0902".equals(orderStatus)) {
return "已支付";
} else if ("0903".equals(orderStatus)) {
return "已取消";
}
return null;
}
public String getPayWayName() {
if ("1101".equals(payWay)) {
return "微信";
} else if ("1102".equals(payWay)) {
return "支付宝";
} else if ("1103".equals(payWay)) {
return "余额";
}
return "";
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/136
service-order
的OrderInfoApiController 控制器 添加
/**
* 查询当前用户订单分页列表
*
* @param page
* @param limit
* @return
*/
@Operation(summary = "查询当前用户订单分页列表")
@GuiGuLogin
@GetMapping("/orderInfo/findUserPage/{page}/{limit}")
public Result<Page<OrderInfo>> getUserOrderByPage(@PathVariable int page, @PathVariable int limit) {
//1.创建分页对象
Page<OrderInfo> pageInfo = new Page<>(page, limit);
//2.获取用户ID
Long userId = AuthContextHolder.getUserId();
//3.业务层调用
pageInfo = orderInfoService.getUserOrderByPage(pageInfo, userId);
return Result.ok(pageInfo);
}
OrderInfoService接口
/**
* 查询当前用户订单分页列表
*
* @param pageInfo 分页对象
* @param userId 用户ID
* @return
*/
Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId);
OrderInfoServiceImpl实现
/**
* 查询当前用户订单分页列表
*
* @param pageInfo 分页对象
* @param userId 用户ID
* @return
*/
@Override
public Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId) {
return orderInfoMapper.getUserOrderByPage(pageInfo, userId);
}
OrderInfoMapper 接口
package com.atguigu.tingshu.order.mapper;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
/**
* 查询当前用户订单分页列表
*
* @param pageInfo 分页对象
* @param userId 用户ID
* @return
*/
Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, @Param("userId") Long userId);
}
OrderInfoMapper.xml 映射文件
<resultMap id="orderInfoResultMap" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
<id property="id" column="id"></id>
<!--订单明细列表 select指定查询ID(查询订单明细)-->
<collection property="orderDetailList" column="id" select="getOrderDetailById"></collection>
</resultMap>
<!--根据订单ID查询订单明细列表-->
<select id="getOrderDetailById" resultType="com.atguigu.tingshu.model.order.OrderDetail">
select id,item_id,item_name,item_price,item_url from order_detail where order_id = #{id}
</select>
<!--分页查询订单列表-->
<select id="getUserOrderByPage" resultMap="orderInfoResultMap">
select oi.id,
oi.order_no,
oi.order_status,
oi.original_amount,
oi.derate_amount,
oi.order_amount,
oi.pay_way
from order_info oi
where user_id = #{userId}
and oi.is_deleted = 0
order by oi.id desc
</select>
应用场景:
场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;
场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止
技术选择:
service-util
利用redissonClient 发送延迟消息,提供公共业务
package com.atguigu.tingshu.common.delay;
import com.atguigu.tingshu.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.concurrent.TimeUnit;
/**
* @author: atguigu
* @create: 2024-04-24 10:45
*/
@Slf4j
@Component
public class DelayMessageService {
@Autowired
private RedissonClient redissonClient;
/**
* 发送延迟消息
* @param queueName
* @param data
* @param ttl
*/
public void sendDelayMsg(String queueName, String data, int ttl) {
//1.基于Redisson客户端对象创建阻塞队列
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(queueName);
//2.基于Redisson客户端对象将阻塞队列作为参数创建延迟队列
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
//3.调用阻塞队列对象发送延迟消息
delayedQueue.offer(data, ttl, TimeUnit.SECONDS);
log.info("发送延迟消息到队列:{}成功:{},延迟时间:{}s", queueName, data, ttl);
}
}
service-order
订单模块中提交订单方法中
@Autowired
private DelayMsgService delayMsgService;
/**
* 提交订单
*
* @param userId
* @param orderInfoVo
* @return
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public String submitOrder(Long userId, OrderInfoVo orderInfoVo) {
//...省略
//8.TODO 选择微信支付,发送延迟任务,延迟关闭超时未支付订单 测试:30s
delayMessageService.sendDelayMsg(KafkaConstant.QUEUE_ORDER_CANCEL, orderInfo.getId().toString(), 30);
}
监听消息:
package com.atguigu.tingshu.order.reciever;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.order.service.OrderInfoService;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @author: atguigu
* @create: 2024-04-24 10:50
*/
@Slf4j
@Component
public class OrderCancelHandler {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderInfoService orderInfoService;
@PostConstruct
public void delayCloseOrder() {
//1.基于Redisson客户端对象创建阻塞队列
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(KafkaConstant.QUEUE_ORDER_CANCEL);
//2.JUC线程池工具类中提供创建单一线程线程池方法
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
while (true) {
String data = null;
try {
data = blockingQueue.poll(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (StringUtils.isNotBlank(data)) {
log.info("监听延迟关单消息订单ID:{}", data);
orderInfoService.cancelOrder(Long.valueOf(data));
}
}
});
}
}
OrderInfoService接口:
/**
* 检查订单状态及关闭订单
* @param orderId
*/
void cancelOrder(Long orderId);
OrderInfoServiceImpl实现类:
/**
* 检查订单状态及关闭订单
*
* @param orderId
*/
@Override
public void cancelOrder(Long orderId) {
//1.根据订单ID查询订单状态
OrderInfo orderInfo = orderInfoMapper.selectById(orderId);
if (orderInfo != null && SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus())) {
//2.判断订单状态 如果是未支付,修改为已取消
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_CANCEL);
orderInfoMapper.updateById(orderInfo);
log.info("[订单服务]订单:{}超时未支付,取消订单", orderId);
}
}