**谷粒随享** ## 第7章 订单 **学习目标:** - 订单结算(页面渲染)对价格计算 - 购买项目-VIP(允许重复购买) - 购买项目-专辑(只能购买一次) - 购买项目-声音(只能购买一次) - 提交订单(余额付款)-->确保余额安全性 - 分布式事务解决方案-Seata - 我的订单 - 订单延迟关单 - RabbitMQ提供延迟任务 # 1、订单结算 购买包含分为: - 购买VIP会员 - 购买专辑 - 购买声音 - vip余额购买 **购买VIP入口:** ![](assets/image-20231013085845826.png) - **购买专辑入口:专辑表的price_type 价格类型 0202 购买整张专辑** ![](assets/image-20231013085656350.png) ## 1.1 获取账户余额 ### 1.1.1 初始化账户 在新用户第一次登录的时候,就进行了初始化账户余额信息操作! ### 1.1.2 显示账户余额 当刷新主页的时候,会加载当前余额数据。 ![订单-我的钱包](assets/订单-我的钱包.gif) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/93 `service-account` 微服务中**UserAccountApiController**控制器 ```java 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") @SuppressWarnings({"all"}) public class UserAccountApiController { @Autowired private UserAccountService userAccountService; /** * 查询当前用户账户可用余额 * * @return 可用余额 */ @GuiGuLogin @Operation(summary = "查询当前用户账户可用余额") @GetMapping("/userAccount/getAvailableAmount") public Result getAvailableAmount() { //1.获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.获取可用余额 BigDecimal availableAmount = userAccountService.getAvailableAmount(userId); return Result.ok(availableAmount); } } ``` **UserAccountService**接口: ```java /** * 查询当前用户账户可用余额 * @param userId * @return */ BigDecimal getAvailableAmount(Long userId); ``` **UserAccountServiceImpl**实现类: ```java /** * 查询当前用户账户可用余额 * * @param userId * @return */ @Override public BigDecimal getAvailableAmount(Long userId) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserAccount::getUserId, userId); queryWrapper.select(UserAccount::getAvailableAmount); UserAccount userAccount = userAccountMapper.selectOne(queryWrapper); if (userAccount != null) { return userAccount.getAvailableAmount(); } return new BigDecimal("0.00"); } ``` 在**首页**点击专辑封面标识为:**VIP免费** 会出现访问VIP服务配置管理接口 ![](assets/tingshu026-1696666009359.png) ## 1.2 VIP会员结算 ![订单-VIP购买入口](assets/订单-VIP购买入口.gif) 思路: 1. 专辑订单 1. 判断用户是否购买过专辑 1. 如果已购买抛出异常 2. 未购买{要给订单明细,减免金额,总金额等属性赋值} 1. 判断用户是否属于vip 1. vip 计算订单总价与折扣价 赋值订单明细与减免明细 2. 非vip 计算订单总价与折扣价 赋值订单明细与减免明细 2. vip 订单 1. 根据Id 获取到vip 配置信息 2. 赋值原始金额,减免金额,总金额,订单明细,减免金额 3. 生成一个流水号存储到缓存,防止用户重复提交订单 4. 给OrderInfoVo 实体类赋值 1. 防止用户非法操作订单金额,将订单对象OrderInfoVo变为字符串,在转换为map。在通过工具类SignHelper将map 变为字符串赋值给签名字段 5. 最后返回OrderInfoVo 对象 ### 1.2.1 获取VIP套餐列表 ![订单-获取Vip套餐列表](assets/订单-获取Vip套餐列表.gif) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/85 在`service-user` 微服务模块中的 **VipServiceConfigApiController** 控制器添加映射路径,返回 VipServiceConfig 实体类的集合数据。也就是查询**vip_service_config**表中的集合数据。 ```java 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; /** * 获取平台套餐列表 * * @return */ @Operation(summary = "获取平台套餐列表") @GetMapping("/vipServiceConfig/findAll") public Result> findAll() { List list = vipServiceConfigService.list(); return Result.ok(list); } } ``` ### 1.2.2 根据套餐ID获取套餐详情 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/91 在`VipServiceConfigApiController`控制器中添加 ```java /** * 根据套餐ID查询套餐详情 * @param id * @return */ @Operation(summary = "根据套餐ID查询套餐详情") @GetMapping("/vipServiceConfig/getVipServiceConfig/{id}") public Result getVipServiceConfig(@PathVariable Long id) { VipServiceConfig vipServiceConfig = vipServiceConfigService.getById(id); return Result.ok(vipServiceConfig); } ``` 在`service-user-client` 模块**UserFeignClient**中添加 ```java /** * 根据套餐ID查询套餐详情 * @param id * @return */ @GetMapping("/vipServiceConfig/getVipServiceConfig/{id}") public Result getVipServiceConfig(@PathVariable Long id); ``` **UserDegradeFeignClient**熔断类: ```java @Override public Result getVipServiceConfig(Long id) { log.error("[用户服务]远程调用getVipServiceConfig执行服务降级"); return null; } ``` ### 1.2.3 VIP会员结算页数据汇总 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/92 #### 1.2.2.1 控制器 ```java 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.AuthContextHolder; 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 jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; 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 OrderInfoVo订单VO对象,将来订单提交时,需要将订单VO对象提交到服务端 */ @GuiGuLogin @Operation(summary = "用户选择不同虚拟商品进行下单结算,封装渲染订单结算页所需要参数") @PostMapping("/orderInfo/trade") public Result tradeData(@Valid @RequestBody TradeVo tradeVo) { //1.获取用户ID Long userId = AuthContextHolder.getUserId(); //2.封装订单VO对象 OrderInfoVo orderInfoVo = orderInfoService.tradeData(userId, tradeVo); return Result.ok(orderInfoVo); } } ``` #### 1.2.2.2 业务层 ```java 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 { /** * 订单确认数据汇总 * @param userId 用户ID * @param tradeVo 选择购买商品交易vo对象 * @return */ OrderInfoVo tradeData(Long userId, TradeVo tradeVo); } ``` ```java 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.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 implements OrderInfoService { @Autowired private OrderInfoMapper orderInfoMapper; @Autowired private UserFeignClient userFeignClient; @Autowired private AlbumFeignClient albumFeignClient; @Autowired private RedisTemplate redisTemplate; /** * 订单确认数据汇总 * * @param userId 用户ID * @param tradeVo 选择购买商品交易vo对象 * @return */ @Override public OrderInfoVo tradeData(Long userId, TradeVo tradeVo) { //1.初始化OrderInfoVo OrderInfoVo orderInfoVo = new OrderInfoVo(); //1.1. 初始化相关金额:0.00 此时金额必须是字符串"0.00" BigDecimal originalAmount = new BigDecimal("0.00"); BigDecimal orderAmount = new BigDecimal("0.00"); BigDecimal derateAmount = new BigDecimal("0.00"); //1.2. 初始化商品及优惠列表 List orderDetailVoList = new ArrayList<>(); List orderDerateVoList = new ArrayList<>(); //付款项目类型: 1001-专辑 1002-声音 1003-vip会员 String itemType = tradeVo.getItemType(); //2.处理购买商品类型为:VIP套餐 if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) { //2.1 远程调用“用户”服务获取套餐信息 VipServiceConfig vipServiceConfig = userFeignClient.getVipServiceConfig(tradeVo.getItemId()).getData(); Assert.notNull(vipServiceConfig, "选择套餐:{}有误", tradeVo.getItemId()); //2.2 封装价格:原价 减免价 定价价 originalAmount = vipServiceConfig.getPrice(); orderAmount = vipServiceConfig.getDiscountPrice(); if (orderAmount.compareTo(originalAmount) != 0) { //减免价格=原价-订单价 derateAmount = originalAmount.subtract(orderAmount); } //2.3 封装订单中商品明细列表 OrderDetailVo orderDetailVo = new OrderDetailVo(); orderDetailVo.setItemId(tradeVo.getItemId()); orderDetailVo.setItemName("会员:" + vipServiceConfig.getName()); orderDetailVo.setItemPrice(vipServiceConfig.getPrice()); orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl()); orderDetailVoList.add(orderDetailVo); //2.4 封装商品优惠列表 if (orderAmount.compareTo(originalAmount) != 0) { OrderDerateVo orderDerateVo = new OrderDerateVo(); orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT); orderDerateVo.setDerateAmount(derateAmount); orderDerateVo.setRemarks("会员限时优惠:" + derateAmount); orderDerateVoList.add(orderDerateVo); } } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) { //3.TODO 处理购买商品类型为:专辑 } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) { //4.TODO 处理购买商品类型为:声音 } //5.封装共有订单VO信息:价格、商品及优惠 TODO 、杂项信息(购买项目类型、流水号、时间戳签名) orderInfoVo.setOriginalAmount(originalAmount); orderInfoVo.setOrderAmount(orderAmount); orderInfoVo.setDerateAmount(derateAmount); orderInfoVo.setOrderDetailVoList(orderDetailVoList); orderInfoVo.setOrderDerateVoList(orderDerateVoList); orderInfoVo.setItemType(itemType); //5.1 为本次订单产生唯一流水号且存入Redis,将来在提交订单验证流水号 String tradeNo = IdUtil.randomUUID(); String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId; redisTemplate.opsForValue().set(tradeKey, tradeNo, RedisConstant.ORDER_TRADE_EXPIRE, TimeUnit.MINUTES); orderInfoVo.setTradeNo(tradeNo); //5.2 生成时间戳及订单签名 orderInfoVo.setTimestamp(System.currentTimeMillis()); //5.2.1 将订单VO转为Map TODO:将付款方式payWay空值属性排除掉 不需要参与签名计算 Map orderInfoMap = BeanUtil.beanToMap(orderInfoVo, false, true); //5.2.2 调用工具类生成签名值 String sign = SignHelper.getSign(orderInfoMap); orderInfoVo.setSign(sign); //6.响应订单VO对象 return orderInfoVo; } } ``` ## 1.3 专辑结算 ![订单-专辑购买入口](assets/订单-专辑购买入口.gif) ### 1.3.1 Feign请求头丢失解决 在`service-util` 微服务中添加远程 feign 远程调用拦截器,来获取token 数据。 ![](assets/tingshu063.jpg) 如上图:因为微服务之间并没有传递头文件,所以我们可以定义一个拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的用户信息再放入到header中,再调用其他微服务即可。 ```java 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); } } } ``` ### 1.3.2 判断用户是否购买过专辑 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/90 `service-user` 微服务的**UserInfoApiController**控制器 ```java /** * 查询当前用户是否购买指定专辑 * @param albumId * @return true:已购买 false:未购买 */ @GuiGuLogin @Operation(summary = "查询当前用户是否购买指定专辑") @GetMapping("/userInfo/isPaidAlbum/{albumId}") public Result isPaidAlbum(@PathVariable Long albumId){ Boolean isPaid = userInfoService.isPaidAlbum(albumId); return Result.ok(isPaid); } ``` **UserInfoService**接口 ```java /** * 查询当前用户是否购买指定专辑 * @param albumId * @return */ Boolean isPaidAlbum(Long albumId); ``` **UserInfoServiceImpl实现类** ```java /** * 查询当前用户是否购买指定专辑 * * @param albumId * @return */ @Override public Boolean isPaidAlbum(Long albumId) { Long userId = AuthContextHolder.getUserId(); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserPaidAlbum::getUserId, userId); queryWrapper.eq(UserPaidAlbum::getAlbumId, albumId); return userPaidAlbumMapper.selectCount(queryWrapper) > 0; } ``` `service-user-client` 远程调用 **UserFeignClient** 添加 ```java /** * 查询当前用户是否购买指定专辑 * @param albumId * @return true:已购买 false:未购买 */ @GetMapping("/userInfo/isPaidAlbum/{albumId}") public Result isPaidAlbum(@PathVariable Long albumId); ``` **UserDegradeFeignClient**熔断类: ```java @Override public Result isPaidAlbum(Long albumId) { log.error("[用户服务]提供远程调用isPaidAlbum服务降级"); return null; } ``` ### 1.3.3 专辑结算数据汇总 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/92 #### 1.3.3.3 OrderInfoServiceImpl实现类 ```java /** * 订单确认数据汇总 * * @param userId 用户ID * @param tradeVo 选择购买商品交易vo对象 * @return */ @Override public OrderInfoVo tradeData(Long userId, TradeVo tradeVo) { //1.初始化OrderInfoVo OrderInfoVo orderInfoVo = new OrderInfoVo(); //1.1. 初始化相关金额:0.00 此时金额必须是字符串"0.00" BigDecimal originalAmount = new BigDecimal("0.00"); BigDecimal orderAmount = new BigDecimal("0.00"); BigDecimal derateAmount = new BigDecimal("0.00"); //1.2. 初始化商品及优惠列表 List orderDetailVoList = new ArrayList<>(); List orderDerateVoList = new ArrayList<>(); //付款项目类型: 1001-专辑 1002-声音 1003-vip会员 String itemType = tradeVo.getItemType(); //2.处理购买商品类型为:VIP套餐 if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) { //2.1...省略代码 } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) { //3. 处理购买商品类型为:专辑 //3.1 远程调用“用户”服务是否已购专辑 Long albumId = tradeVo.getItemId(); Boolean isPaid = userFeignClient.isPaidAlbum(albumId).getData(); if (isPaid) { throw new RuntimeException("用户已购买专辑:" + albumId); } //3.2 远程调用"专辑"服务获取专辑信息 AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData(); Assert.notNull(albumInfo, "专辑:{}不存在", albumId); //3.3 远程调用用户服务获取用户身份,封装"商品"相关价格 UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData(); Boolean isVIP = false; if (1 == userInfoVo.getIsVip().intValue() && userInfoVo.getVipExpireTime().after(new Date())) { isVIP = true; } originalAmount = albumInfo.getPrice(); orderAmount = originalAmount; //减免价=原价*折扣 if (!isVIP && albumInfo.getDiscount().intValue() != -1) { //普通用户 且 专辑有普通用户折扣 orderAmount = originalAmount.multiply(albumInfo.getDiscount()).divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP); } if (isVIP && albumInfo.getVipDiscount().intValue() != -1) { //VIP用户 且 专辑有VIP用户折扣 orderAmount = originalAmount.multiply(albumInfo.getVipDiscount()).divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP); } if (orderAmount.compareTo(originalAmount) != 0) { derateAmount = originalAmount.subtract(orderAmount); } //3.4 封装"商品"列表及商品优惠列表 OrderDetailVo orderDetailVo = new OrderDetailVo(); orderDetailVo.setItemId(albumId); orderDetailVo.setItemName("专辑:" + albumInfo.getAlbumTitle()); orderDetailVo.setItemUrl(albumInfo.getCoverUrl()); orderDetailVo.setItemPrice(originalAmount); orderDetailVoList.add(orderDetailVo); if (orderAmount.compareTo(originalAmount) != 0) { 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(itemType)) { //4.TODO 处理购买商品类型为:声音 } //5.封装共有订单VO信息:价格、商品及优惠 TODO 、杂项信息(购买项目类型、流水号、时间戳签名) orderInfoVo.setOriginalAmount(originalAmount); orderInfoVo.setOrderAmount(orderAmount); orderInfoVo.setDerateAmount(derateAmount); orderInfoVo.setOrderDetailVoList(orderDetailVoList); orderInfoVo.setOrderDerateVoList(orderDerateVoList); orderInfoVo.setItemType(itemType); //5.1 为本次订单产生唯一流水号且存入Redis,将来在提交订单验证流水号 String tradeNo = IdUtil.randomUUID(); String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId; redisTemplate.opsForValue().set(tradeKey, tradeNo, RedisConstant.ORDER_TRADE_EXPIRE, TimeUnit.MINUTES); orderInfoVo.setTradeNo(tradeNo); //5.2 生成时间戳及订单签名 orderInfoVo.setTimestamp(System.currentTimeMillis()); //5.2.1 将订单VO转为Map TODO:将付款方式payWay空值属性排除掉 不需要参与签名计算 Map orderInfoMap = BeanUtil.beanToMap(orderInfoVo, false, true); //5.2.2 调用工具类生成签名值 String sign = SignHelper.getSign(orderInfoMap); orderInfoVo.setSign(sign); //6.响应订单VO对象 return orderInfoVo; } ``` ## 1.4 声音结算 ### 1.4.1 声音分集购买列表 ![订单-声音结算](assets/订单-声音结算.gif) #### 1.4.1.1 获取已购声音Id列表Feign接口 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/94 `service-user`模块 ##### **UserInfoApiController**控制器 ```java /** * 查询当前用户某个专辑下已购声音ID列表 * @param albumId 专辑ID * @return 已购声音ID列表 */ @GuiGuLogin @Operation(summary = "查询当前用户某个专辑下已购声音ID列表") @GetMapping("/userInfo/findUserPaidTrackList/{albumId}") public Result> getUserPaidTrackIdList(@PathVariable Long albumId){ //1.获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.调用业务逻辑获取已购声音ID列表 List trackIdList = userInfoService.getUserPaidTrackIdList(userId, albumId); return Result.ok(trackIdList); } ``` ##### **UserInfoService接口** ```java /** * 查询当前用户某个专辑下已购声音ID列表 * @param albumId 专辑ID * @return 已购声音ID列表 */ List getUserPaidTrackIdList(Long userId, Long albumId); ``` ##### **UserInfoServiceImpl**实现类 ```java /** * 查询当前用户某个专辑下已购声音ID列表 * @param albumId 专辑ID * @return 已购声音ID列表 */ @Override public List getUserPaidTrackIdList(Long userId, Long albumId) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserPaidTrack::getUserId, userId); queryWrapper.eq(UserPaidTrack::getAlbumId, albumId); //只需要获取已购声音ID queryWrapper.select(UserPaidTrack::getTrackId); List userPaidTrackList = userPaidTrackMapper.selectList(queryWrapper); if(CollectionUtil.isNotEmpty(userPaidTrackList)){ return userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList()); } return null; } ``` `service-user-client`模块中**UserFeignClient**提供Feign接口 ```java /** * 查询当前用户某个专辑下已购声音ID列表 * @param albumId 专辑ID * @return 已购声音ID列表 */ @GetMapping("/userInfo/findUserPaidTrackList/{albumId}") public Result> getUserPaidTrackIdList(@PathVariable Long albumId); ``` **UserDegradeFeignClient**熔断类 ```java @Override public Result> getUserPaidTrackIdList(Long albumId) { log.error("[用户服务]远程调用getuserPaidTrackIdList执行服务降级"); return null; } ``` #### 1.4.1.2 声音分集购买列表 ![](assets/tingshu035-1696666009360.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/95 ##### AlbumInfoApiController控制器 控制器返回数据格式如下: 因为当前专辑只支持单集购买!专辑的价格是 album_info.price --> 当前专辑中的一条声音的价格 **声音总价= album_info.price*trackCount** album_info 表中 price_type类型分为: **0201-单集** 0202-整专辑 ```java Map map = new HashMap<>(); map.put("name","本集"); // 显示文本 map.put("price",albumInfo.getPrice()); // 专辑声音对应的价格 map.put("trackCount",1); // 记录购买集数 list.add(map); ``` ```java /** * 基于当前选择购买起始声音得到分集购买列表 * * @param trackId * @return [{name:"本集",price:1,trackCount:1},{name:"后10集",price:10,trackCount:10}...{name:"全集/45集",price:45,trackCount:45}] */ @GuiGuLogin @Operation(summary = "基于当前选择购买起始声音得到分集购买列表") @GetMapping("/trackInfo/findUserTrackPaidList/{trackId}") public Result>> getUserTrackPaidList(@PathVariable Long trackId) { //1.从ThreadLocal中获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.调用业务逻辑获取分集购买列表 List> list = albumInfoService.getUserTrackPaidList(userId, trackId); return Result.ok(list); } ``` ##### AlbumInfoService接口 ```java /** * 基于当前选择购买起始声音得到分集购买列表 * @param userId 用户ID * @param trackId 选择购买声音ID(起始计算参照) * @return */ List> getUserTrackPaidList(Long userId, Long trackId); ``` ##### AlbumInfoServiceImpl实现类 思路: 1. 先根据专辑Id+ **{用户Id}**获取到专辑对应的声音Id集合列表 2. 获取到当前专辑{albumId}中大于等于{orderNum}当前声音Id的集合列表 3. 获取到当前要支付的声音Id列表{ 2 与 1 做一个排除即可 } 4. 构造声音分集购买列表 ![image-20231103002225859](assets/image-20231103002225859.png) ```java /** * 基于当前选择购买起始声音得到分集购买列表 * * @param userId 用户ID * @param trackId 选择购买声音ID(起始计算参照) * @return [{name:"本集",price:1,trackCount:1}] */ @Override public List> getUserTrackPaidList(Long userId, Long trackId) { //1.创建分集购买列表 List> list = new ArrayList<>(); //2.根据声音ID查询到“起始”待购声音记录,得到当前声音徐浩、所属专辑ID TrackInfo trackInfo = trackInfoMapper.selectById(trackId); Integer orderNum = trackInfo.getOrderNum(); Long albumId = trackInfo.getAlbumId(); //3.根据"起始"声音序号获取所有待购买声音列表(可能包含用户已购声音) LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(TrackInfo::getAlbumId, albumId); queryWrapper.ge(TrackInfo::getOrderNum, orderNum); queryWrapper.select(TrackInfo::getId); List trackInfoList = trackInfoMapper.selectList(queryWrapper); //4.远程调用"用户服务"获取已购买声音ID列表 List userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(albumId).getData(); //5.如果存在已购声音ID,将已购声音ID排除掉得到真正未购买声音列表 if (CollectionUtil.isNotEmpty(userPaidTrackIdList)) { trackInfoList = trackInfoList.stream() .filter(track -> !userPaidTrackIdList.contains(track.getId())).collect(Collectors.toList()); } //6.根据未购买声音列表长度动态构建分集购买列表 //6.0 根据专辑ID查询专辑价格=声音价格 BigDecimal price = albumInfoMapper.selectById(albumId).getPrice(); //6.1 构建"本集"分集购买对象 Map currMap = new HashMap<>(); currMap.put("name", "本集"); currMap.put("price", price); currMap.put("trackCount", 1); list.add(currMap); //6.2 根据未购买声音数量循环最多构建6个分集购买对象(50集) int size = trackInfoList.size(); //例如:size=12 展示 后10集,全集 size=35 展示 后10集 后20集 后30集 全集 for (int i = 10; i <= 50; i += 10) { if (size > i) { Map map = new HashMap<>(); map.put("name", "后" + i + "集"); map.put("price", price.multiply(BigDecimal.valueOf(i))); map.put("trackCount", i); list.add(map); } else { Map map = new HashMap<>(); map.put("name", "全集(" + size + "集)"); map.put("price", price.multiply(BigDecimal.valueOf(size))); map.put("trackCount", size); list.add(map); break; } } return list; } ``` ### 1.4.2 获取待购声音列表Feign接口 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/96 #### 控制器 `service-album` 微服务中的**TrackInfoApiController**控制器添加代码 ```java /** * 查询当前用户未购买声音列表 * * @param trackId 点击付费标识声音ID,将该声音作为起始标准 * @param trackCount 购买数量 * @return 待购声音列表 */ @GuiGuLogin @Operation(summary = "查询当前用户未购买声音列表") @GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}") public Result> getWaitBuyTrackList(@PathVariable Long trackId, @PathVariable Integer trackCount) { //1.从ThreadLocal中获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.调用业务逻辑获取待购声音列表 List list = albumInfoService.getWaitBuyTrackList(userId, trackId, trackCount); return Result.ok(list); } ``` #### TrackInfoService接口 ```java /** * 查询当前用户未购买声音列表 * @param userId 用户ID * @param trackId 点击付费标识声音ID,将该声音作为起始标准 * @param trackCount 购买数量 * @return 待购声音列表 */ List getWaitBuyTrackList(Long userId, Long trackId, Integer trackCount); ``` #### 实现类 思路: 1. 根据声音Id 获取到当前声音对象 2. 远程调用用户服务获取已购买声音ID列表 3. 根据声音ID+当前声音序号查询待购声音列表,将已购声音进行排除 4. 返回待购声音集合对象返回 ```java /** * 查询当前用户未购买声音列表 * * @param userId 用户ID * @param trackId 点击付费标识声音ID,将该声音作为起始标准 * @param trackCount 购买数量 * @return 待购声音列表 */ @Override public List getWaitBuyTrackList(Long userId, Long trackId, Integer trackCount) { //1.根据声音ID查询到“起始”待购声音记录,得到当前声音序号、所属专辑ID TrackInfo trackInfo = trackInfoMapper.selectById(trackId); Long albumId = trackInfo.getAlbumId(); Integer orderNum = trackInfo.getOrderNum(); //2.远程调用用户服务获取已购买声音ID列表 List userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(albumId).getData(); //3.获取待购声音列表 查询条件:专辑ID,序号,已购声音ID 。排序字段:序号 。返回数量:购买数量 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); //3.1 等值查询专辑标题 queryWrapper.eq(TrackInfo::getAlbumId, albumId); //3.2 大于等于当前选购声音序号 queryWrapper.ge(TrackInfo::getOrderNum, orderNum); if (CollectionUtil.isNotEmpty(userPaidTrackIdList)) { //3.3 如果存在已购买声音,去除已购买声音ID queryWrapper.notIn(TrackInfo::getId, userPaidTrackIdList); } //3.4 按照声音序号升序 queryWrapper.orderByAsc(TrackInfo::getOrderNum); //3.5 按用户选择购买数量获取声音列表 queryWrapper.last("LIMIT " + trackCount); //3.6 执行查询列:多查询了专辑ID,需要在订单服务进行算价格需要获取专辑中价格 queryWrapper.select(TrackInfo::getId, TrackInfo::getTrackTitle, TrackInfo::getCoverUrl, TrackInfo::getAlbumId); List list = trackInfoMapper.selectList(queryWrapper); return list; } ``` **AlbumFeignClient** ```java /** * 查询当前用户未购买声音列表 * * @param trackId 点击付费标识声音ID,将该声音作为起始标准 * @param trackCount 购买数量 * @return 待购声音列表 */ @GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}") public Result> getWaitBuyTrackList(@PathVariable Long trackId, @PathVariable Integer trackCount); ``` 熔断类: ```java @Override public Result> getWaitBuyTrackList(Long trackId, Integer trackCount) { log.error("[专辑服务]远程调用getWaitBuyTrackList执行服务降级"); return null; } ``` ### 1.4.3 改造订单确认方法 ```java /** * 订单确认数据汇总 * * @param userId 用户ID * @param tradeVo 选择购买商品交易vo对象 * @return */ @Override public OrderInfoVo tradeData(Long userId, TradeVo tradeVo) { //1.初始化OrderInfoVo OrderInfoVo orderInfoVo = new OrderInfoVo(); //1.1. 初始化相关金额:0.00 此时金额必须是字符串"0.00" BigDecimal originalAmount = new BigDecimal("0.00"); BigDecimal orderAmount = new BigDecimal("0.00"); BigDecimal derateAmount = new BigDecimal("0.00"); //1.2. 初始化商品及优惠列表 List orderDetailVoList = new ArrayList<>(); List orderDerateVoList = new ArrayList<>(); //付款项目类型: 1001-专辑 1002-声音 1003-vip会员 String itemType = tradeVo.getItemType(); //2.处理购买商品类型为:VIP套餐 if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) { //2.1 ...省略 } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) { //3. 处理购买商品类型为:专辑 ...省略 } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) { //4.TODO 处理购买商品类型为:声音 Long trackId = tradeVo.getItemId(); Integer trackCount = tradeVo.getTrackCount(); //4.1 远程调用"专辑服务"获取待购买声音列表 List waitBuyTrackList = albumFeignClient.getWaitBuyTrackList(trackId, trackCount).getData(); Assert.notNull(waitBuyTrackList, "不存在待结算声音"); //4.2 远程调用专辑服务得到专辑信息-得到声音单价 Long albumId = waitBuyTrackList.get(0).getAlbumId(); AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData(); Assert.notNull(albumInfo, "专辑:{}不存在", albumId); //4.3 封装"商品"相关价格 声音单价*列表长度 注意:声音不支持折扣 BigDecimal price = albumInfo.getPrice(); originalAmount = price.multiply(BigDecimal.valueOf(waitBuyTrackList.size())); orderAmount = originalAmount; //4.4 封装"商品"列表 将集合泛型从TrackInfo转为OrderDetailVo orderDetailVoList = waitBuyTrackList .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信息:价格、商品及优惠 TODO 、杂项信息(购买项目类型、流水号、时间戳签名) orderInfoVo.setOriginalAmount(originalAmount); orderInfoVo.setOrderAmount(orderAmount); orderInfoVo.setDerateAmount(derateAmount); orderInfoVo.setOrderDetailVoList(orderDetailVoList); orderInfoVo.setOrderDerateVoList(orderDerateVoList); orderInfoVo.setItemType(itemType); //5.1 为本次订单产生唯一流水号且存入Redis,将来在提交订单验证流水号 String tradeNo = IdUtil.randomUUID(); String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId; redisTemplate.opsForValue().set(tradeKey, tradeNo, RedisConstant.ORDER_TRADE_EXPIRE, TimeUnit.MINUTES); orderInfoVo.setTradeNo(tradeNo); //5.2 生成时间戳及订单签名 orderInfoVo.setTimestamp(System.currentTimeMillis()); //5.2.1 将订单VO转为Map TODO:将付款方式payWay空值属性排除掉 不需要参与签名计算 Map orderInfoMap = BeanUtil.beanToMap(orderInfoVo, false, true); //5.2.2 调用工具类生成签名值 String sign = SignHelper.getSign(orderInfoMap); orderInfoVo.setSign(sign); //6.响应订单VO对象 return orderInfoVo; } ``` # 2、提交订单 **声音购买-仅支持余额付款**: ![](assets/tingshu035-1696666009360.png) **专辑购买**: ![](assets/image-20231013085656350.png) **VIP 购买**: ![](assets/image-20231013085845826.png) ## 2.1 提交订单业务分析 - 分集购买**声音**只支持**余额支付** - 购买**VIP**或**专辑**分为**余额**,**微信支付** 1. 业务校验 1. 校验签名 2. 验证交易号,防止重复提交订单 2. 下单保存订单 1. 微信支付(调用保存订单) 2. **余额支付** 1. 远程调用账户服务扣减余额 2. 远程调用用户服务新增购买记录 3. 如果出现异常,则进行全局事务回滚 ## 2.2 整合分布式事务Seata 这里采用**Seata**提供**AT模式**进行分布式事务管理。 1. 分别在`tingshu_account`、`tingshu_order`、`tingshu_user`三个数据库中新增undo_log日志表 ```sql 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; ``` 2. 分别在`service-order`、`service-account`、`service-user`三个模块pom.xml中增加Seata启动依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-spring-boot-starter io.seata seata-spring-boot-starter ``` 3. 订单微服务`service-order`模块新增Seata配置 ```yaml 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集群名称 ``` 4. 账户微服务`service-account`模块新增Seata配置 ```yaml 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集群名称 ``` 5. 用户微服务`service-user`模块新增Seata配置 ```yaml 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集群名称 ``` ## 2.3 服务提供方 ### 2.3.1 检查并扣减账户余额 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/127 `service-account`账户服务**UserAccountApiController** ```java /** * 扣减账户余额 * * @param accountDeductVo * @return */ @Operation(summary = "扣减账户余额") @PostMapping("/userAccount/checkAndDeduct") public Result checkAndDeduct(@RequestBody AccountDeductVo accountDeductVo) { //调用业务层完成余额扣减 userAccountService.checkAndDeduct(accountDeductVo); return Result.ok(); } ``` 业务接口**UserAccountService** ```java /** * 扣减账户余额 * * @param accountDeductVo * @return */ void checkAndDeduct(AccountDeductVo accountDeductVo); ``` 业务实现类**UserAccountService** ```java /** * 扣减账户余额 * * @param accountDeductVo * @return */ @Override public void checkAndDeduct(AccountDeductVo accountDeductVo) { //1.扣减账户余额 int rows = userAccountMapper.checkAndDeduct(accountDeductVo); if (rows == 0) { throw new RuntimeException("账户余额不足"); } //2.新增账户变动日志 this.saveUserAccountDetail(accountDeductVo.getUserId(), accountDeductVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountDeductVo.getAmount(), accountDeductVo.getOrderNo()); } ``` **UserAccountMapper** ```java package com.atguigu.tingshu.account.mapper; import com.atguigu.tingshu.model.account.UserAccount; import com.atguigu.tingshu.vo.account.AccountDeductVo; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface UserAccountMapper extends BaseMapper { /** * 扣减余额 * @param accountDeductVo */ int checkAndDeduct(@Param("vo") AccountDeductVo accountDeductVo); } ``` **UserAccountMapper.xml** ```xml UPDATE user_account SET total_amount = total_amount - #{vo.amount}, available_amount = available_amount - #{vo.amount}, total_pay_amount = total_pay_amount + #{vo.amount} WHERE user_id = #{vo.userId} AND available_amount >= #{vo.amount} ``` 在`service-account-client`模块中提供Feign接口:**AccountFeignClient** ```java 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; /** *

* 账号模块远程调用API接口 *

* * @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** ```java 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; @Component @Slf4j public class AccountDegradeFeignClient implements AccountFeignClient { @Override public Result checkAndDeduct(AccountDeductVo accountDeductVo) { return null; } } ``` ### 2.3.2 新增购买记录 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/118 `service-user`模块中**UserInfoApiController**提供处理购买记录Restful接口 **UserInfoApiController控制器** ```java /** * 用户付款成功后虚拟物品(VIP,专辑,声音)发货 * @param userPaidRecordVo * @return */ @Operation(summary = "用户付款成功后虚拟物品发货") @PostMapping("/userInfo/savePaidRecord") public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo){ userInfoService.savePaidRecord(userPaidRecordVo); return Result.ok(); } ``` **UserInfoService业务接口** ```java /** * 保存用户不同购买项目类型的购买记录 * @param userPaidRecordVo */ void savePaidRecord(UserPaidRecordVo userPaidRecordVo); ``` **业务实现类UserInfoServiceImpl** ```java @Autowired private AlbumFeignClient albumFeignClient; @Autowired private UserVipServiceMapper userVipServiceMapper; @Autowired private VipServiceConfigMapper vipServiceConfigMapper; /** * 保存用户不同购买项目类型的购买记录 * * @param userPaidRecordVo */ @Override public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) { //1001-专辑 1002-声音 1003-vip会员 String itemType = userPaidRecordVo.getItemType(); //1.处理购买项目类型-声音 if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) { //1.根据订单编号查询是否处理过声音购买记录 LambdaQueryWrapper userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>(); userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo()); userPaidTrackLambdaQueryWrapper.select(UserPaidTrack::getId); if (userPaidTrackMapper.selectCount(userPaidTrackLambdaQueryWrapper) > 0) { return; } //2.保存声音购买记录 //2.1 根据声音ID查询声音信息获取所属专辑ID List trackIdList = userPaidRecordVo.getItemIdList(); if (CollectionUtil.isNotEmpty(trackIdList)) { TrackInfo trackInfo = albumFeignClient.getTrackInfo(trackIdList.get(0)).getData(); Long albumId = trackInfo.getAlbumId(); for (Long trackId : trackIdList) { UserPaidTrack userPaidTrack = new UserPaidTrack(); userPaidTrack.setAlbumId(albumId); userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo()); userPaidTrack.setTrackId(trackId); userPaidTrack.setUserId(userPaidRecordVo.getUserId()); userPaidTrackMapper.insert(userPaidTrack); } } } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) { //2.TODO 处理购买项目类型-专辑 //2.1 根据订单编号查询是否处理过专辑购买记录 LambdaQueryWrapper paidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>(); paidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo()); if (userPaidAlbumMapper.selectCount(paidAlbumLambdaQueryWrapper) > 0) { return; } //2.2 保存专辑购买记录 if (CollectionUtil.isNotEmpty(userPaidRecordVo.getItemIdList())) { Long albumId = userPaidRecordVo.getItemIdList().get(0); UserPaidAlbum userPaidAlbum = new UserPaidAlbum(); userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo()); userPaidAlbum.setUserId(userPaidRecordVo.getUserId()); userPaidAlbum.setAlbumId(albumId); userPaidAlbumMapper.insert(userPaidAlbum); } } else if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) { //3.处理购买项目类型-VIP会员 //3.1 根据订单编号查询是否处理过VIP购买记录 LambdaQueryWrapper userVipServiceLambdaQueryWrapper = new LambdaQueryWrapper<>(); userVipServiceLambdaQueryWrapper.eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo()); Long count = userVipServiceMapper.selectCount(userVipServiceLambdaQueryWrapper); if (count > 0) { return; } //3.2 保存本次会员购买记录 UserVipService userVipService = new UserVipService(); //3.2.1 获取当前用户VIP身份 Boolean isVIP = false; UserInfoVo userInfoVo = getUserInfo(userPaidRecordVo.getUserId()); if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(new Date())) { isVIP = true; } //3.2.2 计算本次会员开始及过期时间 //3.2.3 获取VIP套餐信息 VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0)); Integer serviceMonth = vipServiceConfig.getServiceMonth(); Date startTime = new Date(); if (!isVIP) { //普通用户 userVipService.setStartTime(startTime); userVipService.setExpireTime(DateUtil.offsetMonth(startTime, serviceMonth)); } else { startTime = DateUtil.offsetDay(userInfoVo.getVipExpireTime(), 1); userVipService.setStartTime(startTime); userVipService.setExpireTime(DateUtil.offsetMonth(startTime, serviceMonth)); } userVipService.setOrderNo(userPaidRecordVo.getOrderNo()); userVipService.setUserId(userPaidRecordVo.getUserId()); userVipServiceMapper.insert(userVipService); //3.3 修改用户VIP标识及过期时间 UserInfo userInfo = new UserInfo(); userInfo.setId(userInfoVo.getId()); userInfo.setIsVip(1); userInfo.setVipExpireTime(userVipService.getExpireTime()); userInfoMapper.updateById(userInfo); } } ``` 在`service-user`模块中Feign接口提供远程调用方法:**UserFeignClient** ```java /** * 用户付款成功后虚拟物品(VIP,专辑,声音)发货 * @param userPaidRecordVo * @return */ @PostMapping("/userInfo/savePaidRecord") public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo); ``` 服务降级类:**UserDegradeFeignClient** ```java @Override public Result savePaidRecord(UserPaidRecordVo userPaidRecordVo) { log.error("[用户服务]远程调用savePaidRecord执行服务降级"); return null; } ``` ### 2.3.3 代码优化 ​ 策略模式(Strategy Pattern)又叫政策模式(Policy Pattern),它是将定义的算法家族分别封装起来,让它们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户。属于行为型模式。 **策略模式**使用的就是面向对象的继承和多态机制,从而实现**同一行为在不同场景下具备不同实现**。 优点: - 策略类之间可以自由切换:由于策略类都实现同一个接口,所以使它们之间可以自由切换。 - 易于扩展:增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“ - 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。 缺点: - 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。 策略模式的主要角色如下: - 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。 - 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。 - 环境(Context)类:用来操作策略的上下文环境,屏蔽高层模块(客户端)对策略、算法的直接访问,封装可能存在的变化。 **策略接口** ```java package com.atguigu.tingshu.user.strategy; import com.atguigu.tingshu.vo.user.UserPaidRecordVo; /** * 处理不同类型商品购买记录的策略接口 */ public interface PaidRecordStrategy { /** * 抽象出公用处理不同购买项目类型接口方法 * @param userPaidRecordVo */ public void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo); } ``` **VIP购买项处理策略实现类**:其中Bean对象ID要跟前端提交项目类型一致 ```java package com.atguigu.tingshu.user.strategy.impl; import cn.hutool.core.date.DateUtil; 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.strategy.PaidRecordStrategy; import com.atguigu.tingshu.vo.user.UserInfoVo; 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; import java.util.Date; /** * @author: atguigu * @create: 2024-08-23 10:32 */ @Slf4j @Component("1003") public class VIPPaidRecordStrategy implements PaidRecordStrategy { @Autowired private UserVipServiceMapper userVipServiceMapper; @Autowired private VipServiceConfigMapper vipServiceConfigMapper; @Autowired private UserInfoMapper userInfoMapper; /** * 处理购买商品类型为会员-发货逻辑 * @param userPaidRecordVo */ @Override public void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo) { log.info("[用户服务]处理购买商品类型为会员发货逻辑"); //3.处理购买项目类型-VIP会员 //3.1 根据订单编号查询是否处理过VIP购买记录 LambdaQueryWrapper userVipServiceLambdaQueryWrapper = new LambdaQueryWrapper<>(); userVipServiceLambdaQueryWrapper.eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo()); Long count = userVipServiceMapper.selectCount(userVipServiceLambdaQueryWrapper); if (count > 0) { return; } //3.2 保存本次会员购买记录 UserVipService userVipService = new UserVipService(); //3.2.1 获取当前用户VIP身份 Boolean isVIP = false; UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId()); if (userInfo.getIsVip().intValue() == 1 && userInfo.getVipExpireTime().after(new Date())) { isVIP = true; } //3.2.2 计算本次会员开始及过期时间 //3.2.3 获取VIP套餐信息 VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0)); Integer serviceMonth = vipServiceConfig.getServiceMonth(); Date startTime = new Date(); if (!isVIP) { //普通用户 userVipService.setStartTime(startTime); userVipService.setExpireTime(DateUtil.offsetMonth(startTime, serviceMonth)); } else { startTime = DateUtil.offsetDay(userInfo.getVipExpireTime(), 1); userVipService.setStartTime(startTime); userVipService.setExpireTime(DateUtil.offsetMonth(startTime, serviceMonth)); } userVipService.setOrderNo(userPaidRecordVo.getOrderNo()); userVipService.setUserId(userPaidRecordVo.getUserId()); userVipServiceMapper.insert(userVipService); //3.3 修改用户VIP标识及过期时间 userInfo.setIsVip(1); userInfo.setVipExpireTime(userVipService.getExpireTime()); userInfoMapper.updateById(userInfo); } } ``` **专辑购买项处理策略实现类**:其中Bean对象ID要跟前端提交项目类型一致 ```java package com.atguigu.tingshu.user.strategy.impl; import cn.hutool.core.collection.CollectionUtil; import com.atguigu.tingshu.model.user.UserPaidAlbum; import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper; import com.atguigu.tingshu.user.strategy.PaidRecordStrategy; 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-08-23 10:32 */ @Slf4j @Component("1001") public class AlbumPaidRecordStrategy implements PaidRecordStrategy { @Autowired private UserPaidAlbumMapper userPaidAlbumMapper; /** * 处理购买商品类型为专辑-发货逻辑 * @param userPaidRecordVo */ @Override public void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo) { log.info("[用户服务]处理购买商品类型为专辑发货逻辑"); //2.TODO 处理购买项目类型-专辑 //2.1 根据订单编号查询是否处理过专辑购买记录 LambdaQueryWrapper paidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>(); paidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo()); if (userPaidAlbumMapper.selectCount(paidAlbumLambdaQueryWrapper) > 0) { return; } //2.2 保存专辑购买记录 if (CollectionUtil.isNotEmpty(userPaidRecordVo.getItemIdList())) { Long albumId = userPaidRecordVo.getItemIdList().get(0); UserPaidAlbum userPaidAlbum = new UserPaidAlbum(); userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo()); userPaidAlbum.setUserId(userPaidRecordVo.getUserId()); userPaidAlbum.setAlbumId(albumId); userPaidAlbumMapper.insert(userPaidAlbum); } } } ``` **声音购买项处理策略实现类**:其中Bean对象ID要跟前端提交项目类型一致 ```java package com.atguigu.tingshu.user.strategy.impl; import cn.hutool.core.collection.CollectionUtil; import com.atguigu.tingshu.album.AlbumFeignClient; 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.strategy.PaidRecordStrategy; 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; import java.util.List; /** * @author: atguigu * @create: 2024-08-23 10:32 */ @Slf4j @Component("1002") public class TrackPaidRecordStrategy implements PaidRecordStrategy { @Autowired private AlbumFeignClient albumFeignClient; @Autowired private UserPaidTrackMapper userPaidTrackMapper; /** * 处理购买商品类型为声音-发货逻辑 * @param userPaidRecordVo */ @Override public void handlerPaidRecord(UserPaidRecordVo userPaidRecordVo) { log.info("[用户服务]处理购买商品类型为声音发货逻辑"); //1.根据订单编号查询是否处理过声音购买记录 LambdaQueryWrapper userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>(); userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo()); userPaidTrackLambdaQueryWrapper.select(UserPaidTrack::getId); if (userPaidTrackMapper.selectCount(userPaidTrackLambdaQueryWrapper) > 0) { return; } //2.保存声音购买记录 //2.1 根据声音ID查询声音信息获取所属专辑ID List trackIdList = userPaidRecordVo.getItemIdList(); if (CollectionUtil.isNotEmpty(trackIdList)) { TrackInfo trackInfo = albumFeignClient.getTrackInfo(trackIdList.get(0)).getData(); Long albumId = trackInfo.getAlbumId(); for (Long trackId : trackIdList) { UserPaidTrack userPaidTrack = new UserPaidTrack(); userPaidTrack.setAlbumId(albumId); userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo()); userPaidTrack.setTrackId(trackId); userPaidTrack.setUserId(userPaidRecordVo.getUserId()); userPaidTrackMapper.insert(userPaidTrack); } } } } ``` **提供工厂类**组装所有的策略实现类 ```java package com.atguigu.tingshu.user.strategy.fac; import com.atguigu.tingshu.user.strategy.PaidRecordStrategy; import com.atguigu.tingshu.user.strategy.impl.AlbumPaidRecordStrategy; import com.atguigu.tingshu.user.strategy.impl.TrackPaidRecordStrategy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.swing.*; import java.util.Map; /** * @author: atguigu * @create: 2024-08-23 10:35 */ @Slf4j @Component public class StrategyFactory { /** * 通过Spring注入机制将PaidRecordStrategy接口下所有实现类对象自动注入到Map * Map中Key 为策略类型-策略实现类Bean名称(1001,1002,1003) * Map中Value 具体策略实现类对象 */ @Autowired private Map strategyMap; /** * 根据策略类型获取对应策略实现类对象 * * @param strategy * @return */ public PaidRecordStrategy getStrategy(String strategy) { if (strategyMap.containsKey(strategy)) { return strategyMap.get(strategy); } throw new RuntimeException("策略类型不存在"); } } ``` **UserInfoServiceImpl**业务调用工厂获取不同策略实现类完成方法调用 ```java @Autowired private StrategyFactory strategyFactory; /** * 保存用户不同购买项目类型的购买记录 * * @param userPaidRecordVo */ @Override public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) { //1001-专辑 1002-声音 1003-vip会员 String itemType = userPaidRecordVo.getItemType(); PaidRecordStrategy strategy = strategyFactory.getStrategy(itemType); strategy.handlerPaidRecord(userPaidRecordVo); } ``` ## 2.3 提交订单 在`service-order` 微服务中添加提交订单控制器 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/99 ### 2.4.1 控制器 **OrderInfoApiController** ```java /** * 提交订单 * * @param orderInfoVo * @return */ @GuiGuLogin @Operation(summary = "提交订单") @PostMapping("/orderInfo/submitOrder") public Result> submitOrder(@RequestBody @Validated OrderInfoVo orderInfoVo) { //1.获取当前登录用户ID Long userId = AuthContextHolder.getUserId(); //2.调用业务逻辑提交订单 Map map = orderInfoService.submitOrder(userId, orderInfoVo); return Result.ok(map); } ``` ### 2.4.2 接口与实现 ```java /** * 提交订单 * @param userId 用户ID * @param orderInfoVo 订单vo信息 * @return {orderNo:"订单编号"} */ Map submitOrder(Long userId, OrderInfoVo orderInfoVo); /** * 保存订单相关信息 * @param userId * @param orderInfoVo * @return */ OrderInfo saveOrderInfo(Long userId, OrderInfoVo orderInfoVo); ``` ```java @Autowired private AccountFeignClient accountFeignClient; /** * 提交订单 * * @param userId 用户ID * @param orderInfoVo 订单vo信息 * @return {orderNo:"订单编号"} */ @Override @GlobalTransactional(rollbackFor = Exception.class) public Map submitOrder(Long userId, OrderInfoVo orderInfoVo) { //1.业务校验:验证流水号是否一致 避免用户误触回退按钮导致订单重复提交 //1.1 确保判断流水号删除流水号原子性采用lua脚本来实现 String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId; //1.2 创建Lua脚本 String script = "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 redisScript = new DefaultRedisScript<>(script, Boolean.class); Boolean flag = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(tradeKey), orderInfoVo.getTradeNo()); if (!flag) { throw new GuiguException(500, "流水号校验失败请重试"); } //2.业务校验:验证签名是否一致 避免用户篡改订单vo中参数导致商户利益损害 //2.1 将提交订单VO转为Map用于验证签名 Map orderInfoMap = BeanUtil.beanToMap(orderInfoVo); //2.2 将Map中的payWay属性去除 在订单确认页生成签名没有加入payWay属性 orderInfoMap.remove("payWay"); SignHelper.checkSign(orderInfoMap); //3.核心业务1:保存订单相关信息(订单、订单明细、订单减免) 订单状态:未支付 OrderInfo orderInfo = this.saveOrderInfo(userId, orderInfoVo); //4.判断付款方式-如果是账户余额付款 if (SystemConstant.ORDER_PAY_ACCOUNT.equals(orderInfoVo.getPayWay())) { //4.1.核心业务2:余额扣减 //4.1.1 构建扣减余额VO对象 AccountDeductVo accountDeductVo = new AccountDeductVo(); accountDeductVo.setOrderNo(orderInfo.getOrderNo()); accountDeductVo.setUserId(orderInfo.getUserId()); accountDeductVo.setAmount(orderInfo.getOrderAmount()); accountDeductVo.setContent(orderInfo.getOrderTitle()); //4.1.2 远程调用账户服务扣减余额 一定要判断Feign调用结果业务状态码 Result result = accountFeignClient.checkAndDeduct(accountDeductVo); if (!result.getCode().equals(200)) { throw new GuiguException(500, "账户余额不足"); } //扣减余额成功后,修改订单状态:已支付 orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID); orderInfoMapper.updateById(orderInfo); //4.2.核心业务3:虚拟物品发货 //4.2.1 构建虚拟物品发货VO对象 UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo(); userPaidRecordVo.setOrderNo(orderInfo.getOrderNo()); userPaidRecordVo.setUserId(orderInfo.getUserId()); userPaidRecordVo.setItemType(orderInfo.getItemType()); List itemIdList = orderInfoVo.getOrderDetailVoList().stream().map(OrderDetailVo::getItemId).collect(Collectors.toList()); userPaidRecordVo.setItemIdList(itemIdList); //4.2.2 远程调用用户服务虚拟物品发货 一定要判断Feign调用结果业务状态码 result = userFeignClient.savePaidRecord(userPaidRecordVo); if (!result.getCode().equals(200)) { throw new GuiguException(500, "虚拟物品发货异常"); } } //5.TODO 判断付款方式-如果是微信付款,采用延迟消息完成延迟关单 if (SystemConstant.ORDER_PAY_WAY_WEIXIN.equals(orderInfoVo.getPayWay())) { } Map map = new HashMap<>(); map.put("orderNo", orderInfo.getOrderNo()); return map; } ``` ### 2.4.3 保存订单方法 业务处理: * 1.将订单相关信息封装为订单对象,向订单表增加一条记录 * 2.将提交订单明细封装为订单明细集合,批量向订单明细表新增若干条记录 * 3.将提交优惠明细封装为优惠明细集合,批量向优惠明细表新增若干条记录 **OrderInfoService接口** ```java /** * 保存订单相关信息 * @param userId * @param orderInfoVo * @return */ OrderInfo saveOrderInfo(Long userId, OrderInfoVo orderInfoVo); ``` **OrderInfoServiceImpl实现类** ```java @Autowired private OrderDetailMapper orderDetailMapper; @Autowired private OrderDerateMapper orderDerateMapper; /** * 保存订单相关信息(订单、订单明细、订单减免) * * @param userId * @param orderInfoVo * @return */ @Override public OrderInfo saveOrderInfo(Long userId, OrderInfoVo orderInfoVo) { //1.保存订单信息 OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class); //1.1 设置订单状态设置为:未支付 orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID); //1.2 设置用户ID orderInfo.setUserId(userId); //1.3 设置订单名称(从订单项获取名称) List orderDetailVoList = orderInfoVo.getOrderDetailVoList(); if (CollectionUtil.isNotEmpty(orderDetailVoList)) { orderInfo.setOrderTitle(orderDetailVoList.get(0).getItemName()); } //1.4 设置订单编号(确保全局唯一用于对接支付平台) 年月日+雪花算法 String oderNo = DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflake(1, 1).nextIdStr(); orderInfo.setOrderNo(oderNo); orderInfoMapper.insert(orderInfo); Long orderId = orderInfo.getId(); //2.保存订单明细列表 //2.1 获取订单明细列表 if (CollectionUtil.isNotEmpty(orderDetailVoList)) { for (OrderDetailVo orderDetailVo : orderDetailVoList) { OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class); //2.1 关联订单 orderDetail.setOrderId(orderId); //2.2 保存订单明细 orderDetailMapper.insert(orderDetail); } } //3.保存订单减免列表 List orderDerateVoList = orderInfoVo.getOrderDerateVoList(); if (CollectionUtil.isNotEmpty(orderDerateVoList)) { for (OrderDerateVo orderDerateVo : orderDerateVoList) { OrderDerate orderDerate = BeanUtil.copyProperties(orderDerateVo, OrderDerate.class); //3.1 关联订单 orderDerate.setOrderId(orderId); //3.2 保存订单减免 orderDerateMapper.insert(orderDerate); } } return orderInfo; } ``` # 3、我的订单 ## 3.1 订单明细信息 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/100 当支付成功之后,点击查看订单 ![订单-我的订单](assets/订单-我的订单.gif) 在`service-order` 微服务**OrderInfoApiController**中添加 ```java /** * 根据订单编号查询订单信息(包含明细) 考虑到在支付成功后收到微信异步回调需要获取订单信息固该接口不需要登录访问 * * @param orderNo * @return */ @Operation(summary = "根据订单编号查询订单信息") @GetMapping("/orderInfo/getOrderInfo/{orderNo}") public Result getOrderInfo(@PathVariable String orderNo) { OrderInfo orderInfo = orderInfoService.getOrderInfo(orderNo); return Result.ok(orderInfo); } ``` **OrderInfoService**接口 ```java /** * 根据订单编号查询订单信息 * @param orderNo * @return */ OrderInfo getOrderInfo(String orderNo); ``` **OrderInfoServiceImpl**实现 ```java /** * 根据订单编号查询订单信息 * * @param orderNo * @return */ @Override public OrderInfo getOrderInfo(String orderNo) { //1.根据订单编号查询订单信息 LambdaUpdateWrapper orderInfoLambdaUpdateWrapper = new LambdaUpdateWrapper<>(); orderInfoLambdaUpdateWrapper.eq(OrderInfo::getOrderNo, orderNo); OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoLambdaUpdateWrapper); if (orderInfo != null) { //2.根据订单ID查询订单明细列表 LambdaUpdateWrapper orderDetailLambdaUpdateWrapper = new LambdaUpdateWrapper<>(); orderDetailLambdaUpdateWrapper.eq(OrderDetail::getOrderId, orderInfo.getId()); List orderDetailList = orderDetailMapper.selectList(orderDetailLambdaUpdateWrapper); if (CollectionUtil.isNotEmpty(orderDetailList)) { orderInfo.setOrderDetailList(orderDetailList); } //3.根据订单ID查询订单优惠列表 LambdaUpdateWrapper orderDerateLambdaUpdateWrapper = new LambdaUpdateWrapper<>(); orderDerateLambdaUpdateWrapper.eq(OrderDerate::getOrderId, orderInfo.getId()); List orderDerateList = orderDerateMapper.selectList(orderDerateLambdaUpdateWrapper); if (CollectionUtil.isNotEmpty(orderDerateList)) { orderInfo.setOrderDerateList(orderDerateList); } } return orderInfo; } ``` 需要展示订单支付状态、支付方式,固在**OrderInfo**中增加对应属性get方法即可保证响应JSON中有对应属性 ```java 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 ""; } ``` ## 3.2 我的订单列表 ![image-20231102121612056](assets/image-20231102121612056.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/136 `service-order` 的**OrderInfoApiController** 控制器 添加 ```java /** * 查询当前用户订单列表 * * @param page * @param limit * @return */ @GuiGuLogin @Operation(summary = "查询当前用户订单列表") @GetMapping("/orderInfo/findUserPage/{page}/{limit}") public Result> getOrderByPage(@PathVariable int page, @PathVariable int limit) { Long userId = AuthContextHolder.getUserId(); Page pageInfo = new Page<>(page, limit); pageInfo = orderInfoService.getOrderByPage(userId, pageInfo); return Result.ok(pageInfo); } ``` **OrderInfoService**接口 ```java /** * 查询当前用户订单列表 * * @param userId * @param pageInfo * @return */ Page getOrderByPage(Long userId, Page pageInfo); ``` **OrderInfoServiceImpl实现** ```java /** * 查询当前用户订单列表 * * @param userId * @param pageInfo * @return */ @Override public Page getOrderByPage(Long userId, Page pageInfo) { //1.调用持久层动态SQL查询订单及订单明细 pageInfo = orderInfoMapper.getOrderByPage(pageInfo, userId); return pageInfo; } ``` **OrderInfoMapper** 接口 ```java 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 { /** * 查询当前用户订单列表 * @param pageInfo * @param userId * @return */ Page getOrderByPage(Page pageInfo, @Param("userId") Long userId); } ``` **OrderInfoMapper.xml** 映射文件 ```xml ``` # 4、订单延迟关单 ## 4.1 什么是延迟任务 - 定时任务:有固定周期的,有明确的触发时间 - 延迟任务:没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件,任务可以立即执行,也可以延迟 ![image-20210513145942962](assets/image-20210513145942962.png) 应用场景: 场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单; 场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止 技术选择: - Redisson-底层基于Redis实现延迟消息 https://github.com/Redisson/Redisson/wiki/7.-distributed-collections - RabbitMQ-延迟消息(死信队列,延迟插件) ## 4.2 延迟关闭订单 `service-order`订单模块中提交订单方法中 ```java //获取Nacos中配置的订单超时时间 //注意:热部署更新配置需要在类上使用@RefreshScope注解 @Value("${order.cancel}") private int ttl; /** * 提交订单 * * @param userId 用户ID * @param orderInfoVo 订单vo信息 * @return {orderNo:"订单编号"} */ @Override @GlobalTransactional(rollbackFor = Exception.class) public Map submitOrder(Long userId, OrderInfoVo orderInfoVo) { //1.业务校验:验证流水号是否一致 避免用户误触回退按钮导致订单重复提交 //省略 //2.业务校验:验证签名是否一致 避免用户篡改订单vo中参数导致商户利益损害 //省略 //3.核心业务1:保存订单相关信息(订单、订单明细、订单减免) 订单状态:未支付 //省略 OrderInfo orderInfo = this.saveOrderInfo(userId, orderInfoVo); //4.判断付款方式-如果是账户余额付款 if (SystemConstant.ORDER_PAY_ACCOUNT.equals(orderInfoVo.getPayWay())) { //省略 } //5.TODO 判断付款方式-如果是微信付款,采用延迟消息完成延迟关单 if (SystemConstant.ORDER_PAY_WAY_WEIXIN.equals(orderInfoVo.getPayWay())) { rabbitService.sendDealyMessage(MqConst.EXCHANGE_CANCEL_ORDER, MqConst.ROUTING_CANCEL_ORDER, orderInfo.getId(), ttl); } Map map = new HashMap<>(); map.put("orderNo", orderInfo.getOrderNo()); return map; } ``` 配置延迟消息: ```java package com.atguigu.tingshu.order.config; @Configuration public class CanelOrderMqConfig { @Bean public Queue cancelQueue() { // 第一个参数是创建的queue的名字,第二个参数是是否支持持久化 return new Queue(MqConst.QUEUE_CANCEL_ORDER, true); } @Bean public CustomExchange cancelExchange() { Map args = new HashMap(); args.put("x-delayed-type", "direct"); return new CustomExchange(MqConst.EXCHANGE_CANCEL_ORDER, "x-delayed-message", true, false, args); } @Bean public Binding bindingCancel() { return BindingBuilder.bind(cancelQueue()).to(cancelExchange()).with(MqConst.ROUTING_CANCEL_ORDER).noargs(); } } ``` 监听延迟消息: ```java package com.atguigu.tingshu.order; import com.atguigu.tingshu.common.rabbit.constant.MqConst; import com.atguigu.tingshu.order.service.OrderInfoService; import com.rabbitmq.client.Channel; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.PipedReader; /** * @author: atguigu * @create: 2024-08-23 14:46 */ @Slf4j @Component public class OrderReceiver { @Autowired private OrderInfoService orderInfoService; /** * 监听到延迟关单消息 * * @param orderId 订单ID * @param message * @param channel */ @SneakyThrows @RabbitListener(queues = MqConst.QUEUE_CANCEL_ORDER) public void cancelOrder(Long orderId, Message message, Channel channel) { if (orderId != null) { log.info("[订单服务]监听到延迟关单消息:{}", orderId); orderInfoService.cancelOrder(orderId); } channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } } ``` **OrderInfoService**接口: ```java /** * 查询并且根据状态取消订单 * @param orderId */ void cancelOrder(Long orderId); ``` **OrderInfoServiceImpl**实现类: ```java /** * 查询并且根据状态取消订单 * * @param orderId */ @Override @Transactional(rollbackFor = Exception.class) public void cancelOrder(Long orderId) { //1.根据订单ID查询订单信息 OrderInfo orderInfo = orderInfoMapper.selectById(orderId); //2.判读支付状态 :如果是未支付,则取消订单 if (SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus())) { //2.1 设置订单状态为已取消 orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_CANCEL); //2.2 更新订单信息 orderInfoMapper.updateById(orderInfo); } } ```