第7章 订单.md 97 KB

谷粒随享

第7章 订单

学习目标:

  • 订单结算(页面渲染)对价格计算
    • 购买项目-VIP-(允许重复购买)
    • 购买项目-专辑(只能购买一次)
    • 购买项目-声音(只能购买一次)
  • 提交订单(余额付款)-->确保余额安全性
  • 我的订单
  • 订单延迟关单

1、订单结算

购买包含分为:

  • 购买VIP会员
  • 购买专辑
  • 购买声音
  • vip余额购买

购买VIP入口:

  • 购买专辑入口:专辑表的price_type 价格类型 0202 购买整张专辑

1.1 获取账户余额

1.1.1 初始化账户

在新用户第一次登录的时候,就进行了初始化账户余额信息操作!

1.1.2 显示账户余额

当刷新主页的时候,会加载当前余额数据。

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;

@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() {
        //获取当前登录用户ID
        Long userId = AuthContextHolder.getUserId();
        //调用业务逻辑获取账户可用金额
        BigDecimal availableAmount = userAccountService.getAvailableAmount(userId);
        return Result.ok(availableAmount);
    }
}

UserAccountService接口:

/**
 * 获取当前登录用户账户可用余额
 *
 * @param userId 用户ID
 * @return
 */
BigDecimal getAvailableAmount(Long userId);

UserAccountServiceImpl实现类:

/**
 * 获取当前登录用户账户可用余额
 *
 * @param userId 用户ID
 * @return
 */
@Override
public BigDecimal getAvailableAmount(Long userId) {
    //1.根据用户ID查询账户记录
    LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserAccount::getUserId, userId);
    UserAccount userAccount = userAccountMapper.selectOne(queryWrapper);
    Assert.notNull(userAccount, "账户为空!");
    //2.获取账户可用金额
    return userAccount.getAvailableAmount();
}

首页点击专辑封面标识为:VIP免费 会出现访问VIP服务配置管理接口

1.2 VIP会员结算

思路:

  1. 专辑订单

    1. 判断用户是否购买过专辑
      1. 如果已购买抛出异常
      2. 未购买{要给订单明细,减免金额,总金额等属性赋值}
        1. 判断用户是否属于vip
        2. vip 计算订单总价与折扣价 赋值订单明细与减免明细
        3. 非vip 计算订单总价与折扣价 赋值订单明细与减免明细
  2. vip 订单

    1. 根据Id 获取到vip 配置信息
    2. 赋值原始金额,减免金额,总金额,订单明细,减免金额
  3. 生成一个流水号存储到缓存,防止用户重复提交订单

  4. 给OrderInfoVo 实体类赋值

    1. 防止用户非法操作订单金额,将订单对象OrderInfoVo变为字符串,在转换为map。在通过工具类SignHelper将map 变为字符串赋值给签名字段
  5. 最后返回OrderInfoVo 对象

1.2.1 获取VIP套餐列表

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/85

访问订单控制器时,将提交的数据封装到当前实体对象中 TradeVo

@Schema(description = "订单确认对象")
public class TradeVo {

    @NotEmpty(message = "付款项目类型不能为空")
    @Schema(description = "付款项目类型: 1001-专辑 1002-声音 1003-vip会员", required = true)
    private String itemType;

    @Positive(message = "付款项目类型Id不能为空") //被标记的元素必须是正数
    @Schema(description = "付款项目类型Id", required = true)
    private Long itemId;

    @Schema(description = "针对购买声音,购买当前集往后多少集", required = false)
    private Integer trackCount;

}

将返回数据封装到OrderInfoVo

package com.atguigu.tingshu.vo.order;

import com.atguigu.tingshu.common.util.Decimal2Serializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

@Data
@Schema(description = "订单对象")
public class OrderInfoVo {

    @NotEmpty(message = "交易号不能为空")
    @Schema(description = "交易号", required = true)
    private String tradeNo;

    @NotEmpty(message = "支付方式不能为空")
    @Schema(description = "支付方式:1101-微信 1102-支付宝 1103-账户余额", required = true)
    private String payWay;

    @NotEmpty(message = "付款项目类型不能为空")
    @Schema(description = "付款项目类型: 1001-专辑 1002-声音 1003-vip会员", required = true)
    private String itemType;

    /**
     * value:最小值
     * inclusive:是否可以等于最小值,默认true,>= 最小值
     * message:错误提示(默认有一个错误提示i18n支持中文)
     *
     * @DecimalMax 同上
     * @Digits integer: 整数位最多几位
     * fraction:小数位最多几位
     * message:同上,有默认提示
     */
    @DecimalMin(value = "0.00", inclusive = false, message = "订单原始金额必须大于0.00")
    @DecimalMax(value = "9999.99", inclusive = true, message = "订单原始金额必须大于9999.99")
    @Digits(integer = 4, fraction = 2)
    @Schema(description = "订单原始金额", required = true)
    @JsonSerialize(using = Decimal2Serializer.class)
    private BigDecimal originalAmount;

    @DecimalMin(value = "0.00", inclusive = true, message = "减免总金额必须大于0.00")
    @DecimalMax(value = "9999.99", inclusive = true, message = "减免总金额必须大于9999.99")
    @Digits(integer = 4, fraction = 2)
    @Schema(description = "减免总金额", required = true)
    @JsonSerialize(using = Decimal2Serializer.class)
    private BigDecimal derateAmount;

    @DecimalMin(value = "0.00", inclusive = false, message = "订单总金额必须大于0.00")
    @DecimalMax(value = "9999.99", inclusive = true, message = "订单总金额必须大于9999.99")
    @Digits(integer = 4, fraction = 2)
    @Schema(description = "订单总金额", required = true)
    @JsonSerialize(using = Decimal2Serializer.class)
    private BigDecimal orderAmount;

    @Valid
    @NotEmpty(message = "订单明细列表不能为空")
    @Schema(description = "订单明细列表", required = true)
    private List<OrderDetailVo> orderDetailVoList;

    @Schema(description = "订单减免明细列表")
    private List<OrderDerateVo> orderDerateVoList;

    @Schema(description = "时间戳", required = true)
    private Long timestamp;
    
    @Schema(description = "签名", required = true)
    private String sign;
}

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套餐ID查询套餐详情
	 * @param id
	 * @return
	 */
	@Operation(summary = "根据VIP套餐ID查询套餐详情")
	@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
	public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id){
		VipServiceConfig serviceConfig = vipServiceConfigService.getById(id);
		return Result.ok(serviceConfig);
	}
}

1.2.2 根据套餐ID获取套餐详情

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/91

VipServiceConfigApiController控制器中添加

/**
 * 根据套餐ID查询套餐信息
 *
 * @param id
 * @return
 */
@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查询VIP套餐详情
 *
 * @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;
}

1.2.3 VIP会员结算页数据汇总

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/92

1.2.2.1 控制器

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.query.order.OrderInfoQuery;
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;


    /**
     * 订单结算页面数据汇总(VIP会员、专辑、声音)
     *
     * @param tradeVo
     * @return 订单信息VO对象
     */
    @GuiGuLogin
    @Operation(summary = "订单结算页面数据汇总(VIP会员、专辑、声音)")
    @PostMapping("/orderInfo/trade")
    public Result<OrderInfoVo> tradeOrderData(@RequestBody TradeVo tradeVo) {
        Long userId = AuthContextHolder.getUserId();
        OrderInfoVo orderInfoVo = orderInfoService.tradeOrderData(userId, tradeVo);
        return Result.ok(orderInfoVo);
    }

}

1.2.2.2 业务层

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> {


    /**
     * 处理订单结算页面数据汇总(VIP会员、专辑、声音)
     * @param userId 用户ID
     * @param tradeVo 选择下单购买项目信息
     * @return
     */
    OrderInfoVo tradeOrderData(Long userId, 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.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;


    /**
     * 处理订单结算页面数据汇总(VIP会员、专辑、声音)
     *
     * @param userId  用户ID
     * @param tradeVo 选择下单购买项目信息
     * @return
     */
    @Override
    public OrderInfoVo tradeOrderData(Long userId, TradeVo tradeVo) {
        //1.创建订单确认页数据VO对象
        OrderInfoVo orderInfoVo = new OrderInfoVo();
        //1.1 订单信息VO封装-购买项目类型
        orderInfoVo.setItemType(tradeVo.getItemType());
        //1.1.声明订单中相关价格-初始值设置为"0.00",在后续业务中赋值即可
        BigDecimal originalAmount = new BigDecimal("0.00");
        BigDecimal derateAmount = new BigDecimal("0.00");
        BigDecimal orderAmount = new BigDecimal("0.00");

        //1.2 声明初始化订单明细列表及订单优惠明细列表,在后续业务中向集合中新增对象
        List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
        List<OrderDerateVo> orderDerateVoList = new ArrayList<>();

        //2.处理订单确认页数据-选择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 封装订单中VIP会员价格 原价=减免价+订单价
            originalAmount = vipServiceConfig.getPrice();
            orderAmount = vipServiceConfig.getDiscountPrice();
            derateAmount = originalAmount.subtract(orderAmount);
            //2.3 封装订单中商品明细列表
            OrderDetailVo orderDetailVo = new OrderDetailVo();
            orderDetailVo.setItemPrice(originalAmount);
            orderDetailVo.setItemId(tradeVo.getItemId());
            orderDetailVo.setItemName(vipServiceConfig.getName());
            orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl());
            orderDetailVoList.add(orderDetailVo);

            //2.4 封装订单中优惠明细列表
            OrderDerateVo orderDerateVo = new OrderDerateVo();
            orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
            orderDerateVo.setDerateAmount(derateAmount);
            orderDerateVo.setRemarks("VIP限时优惠:" + derateAmount);
            orderDerateVoList.add(orderDerateVo);

        } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
            //3. TODO 处理订单确认页数据-选择专辑

        } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
            //4. TODO 处理订单确认页数据-选择声音

        }

        //5.所有订单确认都需要属性
        orderInfoVo.setOriginalAmount(originalAmount);
        orderInfoVo.setOrderAmount(orderAmount);
        orderInfoVo.setDerateAmount(derateAmount);
        orderInfoVo.setOrderDetailVoList(orderDetailVoList);
        orderInfoVo.setOrderDerateVoList(orderDerateVoList);
        //5.1 本次结算流水号-防止重复提交
        //5.1.1 构建当前用户本次订单流水号Key
        String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
        //5.1.2 生成本次订单流水号
        String tradeNo = IdUtil.fastSimpleUUID();
        //5.1.3 将流水号存入Redis
        redisTemplate.opsForValue().set(tradeNoKey, tradeNo, 5, TimeUnit.MINUTES);
        //5.1.4 封装订单VO中流水号
        orderInfoVo.setTradeNo(tradeNo);

        //5.2 本次结算时间戳
        orderInfoVo.setTimestamp(DateUtil.current());
        //5.3 本次结算签名--防止数据篡改
        //5.3.1 将订单VO转为Map-将VO中支付方式null值去掉
        Map<String, Object> paramsMap = BeanUtil.beanToMap(orderInfoVo, false, true);
        //5.3.2 调用签名API对现有订单所有数据进行签名
        String sign = SignHelper.getSign(paramsMap);
        orderInfoVo.setSign(sign);

        //6.返回订单确认页数据VO对象
        return orderInfoVo;
    }
}

1.3 专辑结算

1.3.1 Feign请求头丢失解决

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

1.3.2 判断用户是否购买过专辑

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/90

service-user 微服务的UserInfoApiController控制器

/**
 * 提供给订单服务调用,验证当前用户是否购买过专辑
 * @param albumId
 * @return
 */
@GuiGuLogin
@Operation(summary = "提供给订单服务调用,验证当前用户是否购买过专辑")
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId){
    Boolean isBuy = userInfoService.isPaidAlbum(albumId);
    return Result.ok(isBuy);
}

UserInfoService接口

/**
 * 验证当前用户是否购买过专辑
 * @param albumId
 * @return
 */
Boolean isPaidAlbum(Long albumId);

UserInfoServiceImpl实现类

/**
 * 验证当前用户是否购买过专辑
 *
 * @param albumId
 * @return
 */
@Override
public Boolean isPaidAlbum(Long albumId) {
    //1.获取当前用户ID
    Long userId = AuthContextHolder.getUserId();
    //2.构建查询条件:用户ID+专辑ID 查询专辑购买记录表
    LambdaQueryWrapper<UserPaidAlbum> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserPaidAlbum::getUserId, userId);
    queryWrapper.eq(UserPaidAlbum::getAlbumId, albumId);
    Long count = userPaidAlbumMapper.selectCount(queryWrapper);
    return count > 0;
}

service-user-client 远程调用 UserFeignClient 添加

/**
 * 提供给订单服务调用,验证当前用户是否购买过专辑
 * @param albumId
 * @return
 */
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId);

UserDegradeFeignClient熔断类:

@Override
public Result<Boolean> 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.1 控制器

service-order 微服务中编写确定订单控制器 ,

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


    /**
     * 订单结算页面渲染-数据汇总
     *
     * @return
     */
    @Operation(summary = "订单结算页面渲染-数据汇总")
    @GuiGuLogin
    @PostMapping("/orderInfo/trade")
    public Result<OrderInfoVo> trade(@RequestBody TradeVo tradeVo) {
        Long userId = AuthContextHolder.getUserId();
        OrderInfoVo orderInfoVo = orderInfoService.trade(userId, tradeVo);
        return Result.ok(orderInfoVo);
    }

}

1.3.3.2 接口

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> {


    /**
     * 订单结算页面渲染-数据汇总
     *
     * @return
     */
    OrderInfoVo trade(Long userId, TradeVo tradeVo);
}

1.3.3.3 实现类

/**
 * 处理订单结算页面数据汇总(VIP会员、专辑、声音)
 *
 * @param userId  用户ID
 * @param tradeVo 选择下单购买项目信息
 * @return
 */
@Override
public OrderInfoVo tradeOrderData(Long userId, TradeVo tradeVo) {
    //1.创建订单确认页数据VO对象
    OrderInfoVo orderInfoVo = new OrderInfoVo();
    //1.1 订单信息VO封装-购买项目类型
    orderInfoVo.setItemType(tradeVo.getItemType());
    //1.1.声明订单中相关价格-初始值设置为"0.00",在后续业务中赋值即可
    BigDecimal originalAmount = new BigDecimal("0.00");
    BigDecimal derateAmount = new BigDecimal("0.00");
    BigDecimal orderAmount = new BigDecimal("0.00");

    //1.2 声明初始化订单明细列表及订单优惠明细列表,在后续业务中向集合中新增对象
    List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
    List<OrderDerateVo> orderDerateVoList = new ArrayList<>();

    //2.处理订单确认页数据-选择VIP会员
    if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
        //...省略

    } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
        //3.处理订单确认页数据-选择专辑
        //3.1 远程调用"用户服务"-判断当前用户是否重复购买专辑
        Boolean isBuy = userFeignClient.isPaidAlbum(tradeVo.getItemId()).getData();
        if (isBuy) {
            throw new GuiguException(400, "当前用户已购买该专辑!");
        }
        //3.2 远程调用"用户服务"-获取当前用户信息(得到身份)
        UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData();
        Assert.notNull(userInfoVo, "用户信息为空!");
        Integer isVip = userInfoVo.getIsVip();
        //3.3 远程调用"专辑服务"-获取欲购买专辑信息
        AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(tradeVo.getItemId()).getData();
        Assert.notNull(albumInfo, "专辑信息为空");
        //3.4 计算当前用户购买专辑价格
        originalAmount = albumInfo.getPrice();
        orderAmount = originalAmount;
        //3.4.1 判断专辑是否有普通用户折扣
        BigDecimal discount = albumInfo.getDiscount();
        if (discount.intValue() != -1) {
            if (isVip.intValue() == 0) {
                //普通用户折扣(从0.1-9.9):原价*折扣   100*8/10 = 80 注意:保留小数位+四舍五入
                orderAmount = originalAmount.multiply(discount)
                        .divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
            }
            if (isVip.intValue() == 1 && new Date().after(userInfoVo.getVipExpireTime())) {
                //普通用户
                orderAmount = originalAmount.multiply(discount)
                        .divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
            }
        }
        //3.4.2 判断专辑是否有VIP用户折扣
        BigDecimal vipDiscount = albumInfo.getVipDiscount();
        if (vipDiscount.intValue() != -1) {
            if (isVip.intValue() == 1 && new Date().before(userInfoVo.getVipExpireTime())) {
                //VIP会员用户
                orderAmount = originalAmount.multiply(vipDiscount)
                        .divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
            }
        }
        derateAmount = originalAmount.subtract(orderAmount);
        //3.5 封装订单中商品明细列表(专辑)
        OrderDetailVo orderDetailVo = new OrderDetailVo();
        orderDetailVo.setItemId(tradeVo.getItemId());
        orderDetailVo.setItemName(albumInfo.getAlbumTitle());
        orderDetailVo.setItemUrl(albumInfo.getCoverUrl());
        orderDetailVo.setItemPrice(originalAmount);
        orderDetailVoList.add(orderDetailVo);

        //3.6 封装订单中优惠明细列表
        if (derateAmount.doubleValue() > 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(tradeVo.getItemType())) {
        //4.TODO 处理订单确认页数据-选择声音
    }
    //5.所有订单确认都需要属性
    orderInfoVo.setOriginalAmount(originalAmount);
    orderInfoVo.setOrderAmount(orderAmount);
    orderInfoVo.setDerateAmount(derateAmount);
    orderInfoVo.setOrderDetailVoList(orderDetailVoList);
    orderInfoVo.setOrderDerateVoList(orderDerateVoList);
    //5.1 本次结算流水号-防止重复提交
    //5.1.1 构建当前用户本次订单流水号Key
    String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
    //5.1.2 生成本次订单流水号
    String tradeNo = IdUtil.fastSimpleUUID();
    //5.1.3 将流水号存入Redis
    redisTemplate.opsForValue().set(tradeNoKey, tradeNo, 5, TimeUnit.MINUTES);
    //5.1.4 封装订单VO中流水号
    orderInfoVo.setTradeNo(tradeNo);

    //5.2 本次结算时间戳
    orderInfoVo.setTimestamp(DateUtil.current());
    //5.3 本次结算签名--防止数据篡改
    //5.3.1 将订单VO转为Map-将VO中支付方式null值去掉
    Map<String, Object> paramsMap = BeanUtil.beanToMap(orderInfoVo, false, true);
    //5.3.2 调用签名API对现有订单所有数据进行签名
    String sign = SignHelper.getSign(paramsMap);
    orderInfoVo.setSign(sign);

    //6.返回订单确认页数据VO对象
    return orderInfoVo;
}

1.4 声音结算

1.4.1 声音分集购买列表

1.4.1.1 获取用户已支付声音Id列表

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/94

service-user模块

UserInfoApiController控制器
/**
 * 提供给专辑服务调用,获取当前用户已购声音集合
 *
 * @param albumId
 * @return
 */
@GuiGuLogin
@Operation(summary = "提供给专辑服务调用,获取当前用户已购声音集合")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackIdList(@PathVariable Long albumId) {
    Long userId = AuthContextHolder.getUserId();
    List<Long> userPaidTrackIdList = userInfoService.getUserPaidTrackIdList(userId, albumId);
    return Result.ok(userPaidTrackIdList);
}
UserInfoService接口
/**
 * 根据用户ID+专辑ID查询已购声音集合
 * @param userId 用户ID
 * @param albumId 专辑ID
 * @return
 */
List<Long> getUserPaidTrackIdList(Long userId, Long albumId);
UserInfoServiceImpl实现类
/**
 * 根据用户ID+专辑ID查询已购声音集合
 *
 * @param userId  用户ID
 * @param albumId 专辑ID
 * @return
 */
@Override
public List<Long> getUserPaidTrackIdList(Long userId, Long albumId) {
    //1.根据用户ID+专辑ID查询已购声音集合
    LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserPaidTrack::getUserId, userId);
    queryWrapper.eq(UserPaidTrack::getAlbumId, albumId);
    List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(queryWrapper);
    if (CollectionUtil.isNotEmpty(userPaidTrackList)) {
        //2.获取已购声音ID集合
        List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
        return userPaidTrackIdList;
    }
    return null;
}

service-user-client模块中UserFeignClient提供Feign接口

/**
 * 提供给专辑服务调用,获取当前用户已购声音集合
 *
 * @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;
}

1.4.1.2 声音分集购买列表

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/95

控制器

TrackInfoApiController 控制器返回数据格式如下: 因为当前专辑只支持单集购买!专辑的价格是 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 声音ID
 * @return [{name:"本集", price:0.2, trackCount:1},{name:"后10集", price:2, trackCount:10},...,{name:"全集", price:*, trackCount:*}]
 */
@GuiGuLogin
@Operation(summary = "获取当前用户分集购买声音列表")
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> getUserWaitBuyTrackPayList(@PathVariable Long trackId) {
    Long userId = AuthContextHolder.getUserId();
    List<Map<String, Object>> list = trackInfoService.getUserWaitBuyTrackPayList(trackId);
    return Result.ok(list);
}
TrackInfoService接口
/**
 * 获取当前用户分集购买声音列表
 *
 * @param trackId 声音ID
 * @return [{name:"本集", price:0.2, trackCount:1},{name:"后10集", price:2, trackCount:10},...,{name:"全集", price:*, trackCount:*}]
 */
List<Map<String, Object>> getUserWaitBuyTrackPayList(Long trackId);
TrackInfoServiceImpl实现类

思路:

  1. 先根据专辑Id+ {用户Id}获取到专辑对应的声音Id集合列表
  2. 获取到当前专辑{albumId}中大于等于{orderNum}当前声音Id的集合列表
  3. 获取到当前要支付的声音Id列表{ 2 与 1 做一个排除即可 }
  4. 构造声音分集购买列表

image-20231103002225859

@Autowired
private UserFeignClient userFeignClient;

/**
 * 获取当前用户分集购买声音列表
 *
 * @param trackId 声音ID
 * @return [{name:"本集", price:0.2, trackCount:1},{name:"后10集", price:2, trackCount:10},...,{name:"全集", price:*, trackCount:*}]
 */
@Override
public List<Map<String, Object>> getUserWaitBuyTrackPayList(Long trackId) {
    //1.根据声音ID查询声音对象-得到专辑ID跟声音序号
    TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
    //2.根据专辑ID+当前声音序号查询大于当前声音待购买声音列表
    LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId());
    queryWrapper.ge(TrackInfo::getOrderNum, trackInfo.getOrderNum());
    List<TrackInfo> waitBuyTrackList = trackInfoMapper.selectList(queryWrapper);
    if (CollectionUtil.isEmpty(waitBuyTrackList)) {
        throw new GuiguException(400, "该专辑下没有符合购买要求声音");
    }
    //3.远程调用"用户服务"获取用户已购买声音ID集合
    List<Long> userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(trackInfo.getAlbumId()).getData();

    //4.将待购买声音列表中用户已购买声音排除掉-得到实际代购买声音列表
    if (CollectionUtil.isNotEmpty(userPaidTrackIdList)) {
        waitBuyTrackList = waitBuyTrackList.stream()
                .filter(waitTrackInfo -> !userPaidTrackIdList.contains(waitTrackInfo.getId())) //排除掉已购声音ID
                .collect(Collectors.toList());
    }
    //5.基于实际购买声音列表长度,动态构建分集购买对象
    List<Map<String, Object>> mapList = new ArrayList<>();
    if (CollectionUtil.isNotEmpty(waitBuyTrackList)) {
        //5.1 根据专辑ID查询专辑得到单集价格
        AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId());
        BigDecimal price = albumInfo.getPrice();
        //5.1 构建本集购买对象
        Map<String, Object> currMap = new HashMap<>();
        currMap.put("name", "本集");
        currMap.put("price", price);
        currMap.put("trackCount", 1);
        mapList.add(currMap);
        //5.2 判断待购买声音数量 数量<10 动态展示后count集合 价格=count*price 数量=count
        int count = waitBuyTrackList.size();

        //5.3 数量>=10 固定显示后10集 价格=10*price 数量=10
        //if (count >= 10) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后10集");
        //    map.put("price", price.multiply(new BigDecimal("10")));
        //    map.put("trackCount", 10);
        //    mapList.add(map);
        //}
        ////5.3 数量>10 and 数量<20 动态展示:后count集合(全集) 价格=count*price 数量=count  相当于全集
        //if (count > 10 && count < 20) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后"+count+"集(全集)");
        //    map.put("price", price.multiply(new BigDecimal(count)));
        //    map.put("trackCount", count);
        //    mapList.add(map);
        //}
        //
        ////5.4 数量>=20 固定显示后20集 价格=20*price 数量=20
        //if (count >= 20) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后20集");
        //    map.put("price", price.multiply(new BigDecimal("20")));
        //    map.put("trackCount", 20);
        //    mapList.add(map);
        //}
        ////5.4 数量>20 and 数量<30 动态展示:后count集合(全集) 价格=count*price 数量=count  相当于全集
        //if (count > 20 && count < 30) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后"+count+"集(全集)");
        //    map.put("price", price.multiply(new BigDecimal(count)));
        //    map.put("trackCount", count);
        //    mapList.add(map);
        //}
        ////5.5 数量>=30 固定显示后30集 价格=30*price 数量=30
        //if (count >= 30) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后30集");
        //    map.put("price", price.multiply(new BigDecimal("30")));
        //    map.put("trackCount", 30);
        //    mapList.add(map);
        //}
        ////5.5 数量>30 and 数量<40 动态展示:后count集合(全集) 价格=count*price 数量=count  相当于全集
        //if (count > 30 && count < 40) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后"+count+"集(全集)");
        //    map.put("price", price.multiply(new BigDecimal(count)));
        //    map.put("trackCount", count);
        //    mapList.add(map);
        //}
        //
        ////5.5 数量>=40 固定显示后40集 价格=40*price 数量=40
        //if (count >= 40) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后40集");
        //    map.put("price", price.multiply(new BigDecimal("40")));
        //    map.put("trackCount", 40);
        //    mapList.add(map);
        //}
        ////5.5 数量>40 and 数量<50 动态展示:后count集合(全集) 价格=count*price 数量=count  相当于全集
        //if (count > 40 && count < 50) {
        //    Map<String, Object> map = new HashMap<>();
        //    map.put("name", "后"+count+"集(全集)");
        //    map.put("price", price.multiply(new BigDecimal(count)));
        //    map.put("trackCount", count);
        //    mapList.add(map);
        //}

        for (int i = 10; i <= 50; i += 10) {
            //判断数量>i 固定显示后i集
            if (count > i) {
                Map<String, Object> map = new HashMap<>();
                map.put("name", "后" + i + "集");
                map.put("price", price.multiply(new BigDecimal(i)));
                map.put("trackCount", i);
                mapList.add(map);
            } else {
                //反之全集(动态构建后count集合)
                Map<String, Object> map = new HashMap<>();
                map.put("name", "后" + count + "集");
                map.put("price", price.multiply(new BigDecimal(count)));
                map.put("trackCount", count);
                mapList.add(map);
                break;
            }
        }
    }
    return mapList;
}

1.4.2 获取待结算声音列表

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/96

控制器

service-album 微服务中的TrackInfoApiController控制器添加代码

/**
 * 提供给订单服务渲染购买商品(声音)列表-查询当前用户待购买声音列表
 *
 * @param trackId    声音ID
 * @param trackCount 数量
 * @return
 */
@GuiGuLogin
@Operation(summary = "提供给订单服务渲染购买商品(声音)列表-查询当前用户待购买声音列表")
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getWaitBuyTrackInfoList(@PathVariable Long trackId, @PathVariable int trackCount) {
    Long userId = AuthContextHolder.getUserId();
    List<TrackInfo> trackInfoList = trackInfoService.getWaitBuyTrackInfoList(userId, trackId, trackCount);
    return Result.ok(trackInfoList);
}

TrackInfoService接口

/**
 * 查询当前用户待购买声音列表(加用户已购买声音排除掉)
 *
 * @param userId 用户ID
 * @param trackId    声音ID
 * @param trackCount 数量
 * @return
 */
List<TrackInfo> getWaitBuyTrackInfoList(Long userId, Long trackId, int trackCount);

实现类

思路:

1. 根据声音Id 获取到当前声音对象
2. 远程调用用户服务获取已购买声音ID列表
3. 根据声音ID+当前声音序号查询待购声音列表,将已购声音进行排除
4. 返回待购声音集合对象返回
/**
 * 查询当前用户待购买声音列表(加用户已购买声音排除掉)
 *
 * @param userId 用户ID
 * @param trackId    声音ID
 * @param trackCount 数量
 * @return
 */
@Override
public List<TrackInfo> getWaitBuyTrackInfoList(Long userId, Long trackId, int trackCount) {
    //1.根据声音ID查询声音对象-得到专辑ID跟声音序号
    TrackInfo trackInfo = trackInfoMapper.selectById(trackId);

    //2.远程调用"用户服务"获取用户已购买声音ID集合
    List<Long> userPaidTrackIdList = userFeignClient.getUserPaidTrackIdList(trackInfo.getAlbumId()).getData();

    //3.根据专辑ID+当前声音序号查询大于当前声音待购买声音列表
    LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId());
    queryWrapper.ge(TrackInfo::getOrderNum, trackInfo.getOrderNum());
    //3.1 去掉已购买过声音
    if(CollectionUtil.isNotEmpty(userPaidTrackIdList)){
        queryWrapper.notIn(TrackInfo::getId, userPaidTrackIdList);
    }
    //3.2 限制购买数量(用户选择购买数量)
    queryWrapper.last("limit "+trackCount);
    //3.3 只查询指定列:封面图片、声音名称、声音ID、所属专辑ID
    queryWrapper.select(TrackInfo::getId, TrackInfo::getTrackTitle, TrackInfo::getCoverUrl, TrackInfo::getAlbumId);
    //3.4 对声音进行排序:按照序号升序
    queryWrapper.orderByAsc(TrackInfo::getOrderNum);
    List<TrackInfo> waitBuyTrackList = trackInfoMapper.selectList(queryWrapper);
    if (CollectionUtil.isEmpty(waitBuyTrackList)) {
        throw new GuiguException(400, "该专辑下没有符合购买要求声音");
    }
    return waitBuyTrackList;
}

AlbumFeignClient

/**
 * 提供给订单服务渲染购买商品(声音)列表-查询当前用户待购买声音列表
 *
 * @param trackId    声音ID
 * @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;
}

1.4.3 改造订单确认方法

/**
 * 处理订单结算页面数据汇总(VIP会员、专辑、声音)
 *
 * @param userId  用户ID
 * @param tradeVo 选择下单购买项目信息
 * @return
 */
@Override
public OrderInfoVo tradeOrderData(Long userId, TradeVo tradeVo) {
    //1.创建订单确认页数据VO对象
    OrderInfoVo orderInfoVo = new OrderInfoVo();
    //1.1 订单信息VO封装-购买项目类型
    orderInfoVo.setItemType(tradeVo.getItemType());
    //1.1.声明订单中相关价格-初始值设置为"0.00",在后续业务中赋值即可
    BigDecimal originalAmount = new BigDecimal("0.00");
    BigDecimal derateAmount = new BigDecimal("0.00");
    BigDecimal orderAmount = new BigDecimal("0.00");

    //1.2 声明初始化订单明细列表及订单优惠明细列表,在后续业务中向集合中新增对象
    List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
    List<OrderDerateVo> orderDerateVoList = new ArrayList<>();

    //2.处理订单确认页数据-选择VIP会员
    if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
        //2.1 省略。。。
    } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
        //3.处理订单确认页数据-选择专辑 省略。。。
    } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
        //4. 处理订单确认页数据-选择声音
        //4.1 远程调用"专辑服务"-获取待购买声音列表
        List<TrackInfo> waitBuyTrackInfoList = albumFeignClient.getWaitBuyTrackInfoList(tradeVo.getItemId(), tradeVo.getTrackCount()).getData();
        if(CollectionUtil.isEmpty(waitBuyTrackInfoList)){
            throw new GuiguException(400, "无符合要求声音");
        }
        //4.2 远程调用"专辑服务"获取专辑信息(得到单集价格)
        AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(waitBuyTrackInfoList.get(0).getAlbumId()).getData();
        BigDecimal price = albumInfo.getPrice();

        //4.3 计算价格 数量*单价 声音没有折扣
        originalAmount = price.multiply(new BigDecimal(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.所有订单确认都需要属性
    orderInfoVo.setOriginalAmount(originalAmount);
    orderInfoVo.setOrderAmount(orderAmount);
    orderInfoVo.setDerateAmount(derateAmount);
    orderInfoVo.setOrderDetailVoList(orderDetailVoList);
    orderInfoVo.setOrderDerateVoList(orderDerateVoList);
    //5.1 本次结算流水号-防止重复提交
    //5.1.1 构建当前用户本次订单流水号Key
    String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
    //5.1.2 生成本次订单流水号
    String tradeNo = IdUtil.fastSimpleUUID();
    //5.1.3 将流水号存入Redis
    redisTemplate.opsForValue().set(tradeNoKey, tradeNo, 5, TimeUnit.MINUTES);
    //5.1.4 封装订单VO中流水号
    orderInfoVo.setTradeNo(tradeNo);

    //5.2 本次结算时间戳
    orderInfoVo.setTimestamp(DateUtil.current());
    //5.3 本次结算签名--防止数据篡改
    //5.3.1 将订单VO转为Map-将VO中支付方式null值去掉
    Map<String, Object> paramsMap = BeanUtil.beanToMap(orderInfoVo, false, true);
    //5.3.2 调用签名API对现有订单所有数据进行签名
    String sign = SignHelper.getSign(paramsMap);
    orderInfoVo.setSign(sign);

    //6.返回订单确认页数据VO对象
    return orderInfoVo;
}

2、提交订单

声音购买-仅支持余额付款

专辑购买

VIP 购买

2.1 提交订单业务分析

  • 分集购买声音只支持余额支付

  • 购买VIP专辑分为余额微信支付

  1. 业务校验
    1. 校验签名
    2. 验证交易号,防止重复提交订单
  2. 下单保存订单
    1. 微信支付(调用保存订单)
    2. 余额支付
      1. 远程调用账户服务扣减余额
      2. 远程调用用户服务新增购买记录
  3. 如果出现异常,则进行全局事务回滚

2.2 整合分布式事务Seata

这里采用Seata提供AT模式进行分布式事务管理。

  1. 分别在tingshu_accounttingshu_ordertingshu_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;
    
  2. 分别在service-orderservice-accountservice-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>
    
  3. 订单微服务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集群名称
       
    
  4. 账户微服务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集群名称
    
  5. 用户微服务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集群名称
    

2.3 服务提供方

2.3.1 检查并扣减账户余额

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.从ThreadLocal中获取用户ID
        Long userId = AuthContextHolder.getUserId();
        if (userId != null) {
            accountDeductVo.setUserId(userId);
        }
        //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) {
        throw new GuiguException(400, "账户余额不足!");
    }
    //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 用户ID
     * @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;
    }
}

2.3.2 新增购买记录

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/118

service-user模块中UserInfoApiController提供处理购买记录Restful接口

UserInfoApiController控制器

/**
 * 接口登录/未登录均可调用(微信支付成功后,需要在异步回调(没有Token),调用该方法处理购买记录)
 * 处理用户购买记录(虚拟物品发货)
 *
 * @param userPaidRecordVo
 * @return
 */
@GuiGuLogin(required = false)
@Operation(summary = "处理用户购买记录(虚拟物品发货)")
@PostMapping("/userInfo/savePaidRecord")
public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo) {
    //1.获取登录用户ID
    Long userId = AuthContextHolder.getUserId();
    if (userId != null) {
        userPaidRecordVo.setUserId(userId);
    }
    //2.调用业务逻辑处理用户购买记录
    userInfoService.savePaidRecord(userPaidRecordVo);
    return Result.ok();
}

UserInfoService业务接口

/**
 * 处理不同购买项:VIP会员,专辑、声音
 *
 * @param userPaidRecordVo
 */
void savePaidRecord(UserPaidRecordVo userPaidRecordVo);

业务实现类UserInfoServiceImpl

@Autowired
private AlbumFeignClient albumFeignClient;

@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;

@Autowired
private UserVipServiceMapper userVipServiceMapper;

/**
 * 处理用户购买记录(虚拟物品发货)
 *
 * @param userPaidRecordVo 购买记录VO
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
    //1.判断购买项目类型-处理专辑
    if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(userPaidRecordVo.getItemType())) {
        //1.1 根据订单编号查询专辑购买记录
        LambdaQueryWrapper<UserPaidAlbum> userPaidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>();
        userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo());
        Long count = userPaidAlbumMapper.selectCount(userPaidAlbumLambdaQueryWrapper);
        if (count > 0) {
            return;
        }
        //1.2 查询到专辑购买记录为空则新增购买记录
        UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
        userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
        userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
        userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
        userPaidAlbumMapper.insert(userPaidAlbum);
    } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(userPaidRecordVo.getItemType())) {
        //2.判断购买项目类型-处理声音
        //2.1 根据订单编号查询声音购买记录
        LambdaQueryWrapper<UserPaidTrack> userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>();
        userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
        Long count = userPaidTrackMapper.selectCount(userPaidTrackLambdaQueryWrapper);
        if (count > 0) {
            return;
        }
        //2.2 查询到声音购买记录为空则新增购买记录(循环批量新增)
        //2.2.1 远程调用专辑服务-根据声音ID查询声音对象-获取声音所属专辑ID
        TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
        Long albumId = trackInfo.getAlbumId();
        //2.2.2 遍历购买项目ID集合批量新增声音购买记录
        userPaidRecordVo.getItemIdList().forEach(trackId -> {
            UserPaidTrack userPaidTrack = new UserPaidTrack();
            userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
            userPaidTrack.setUserId(userPaidRecordVo.getUserId());
            userPaidTrack.setAlbumId(albumId);
            userPaidTrack.setTrackId(trackId);
            userPaidTrackMapper.insert(userPaidTrack);
        });
    } else if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(userPaidRecordVo.getItemType())) {
        //3.判断购买项目类型-处理VIP会员-允许多次购买
        //3.1 新增VIP购买记录
        UserVipService userVipService = new UserVipService();
        //3.1.1 根据VIP套餐ID查询套餐信息-得到VIP会员服务月数
        Long vipConfigId = userPaidRecordVo.getItemIdList().get(0);
        VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(vipConfigId);
        Integer serviceMonth = vipServiceConfig.getServiceMonth();
        //3.1.2 获取用户身份,如果是VIP会员,则续费
        UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
        Integer isVip = userInfo.getIsVip();
        if (isVip.intValue() == 1 && userInfo.getVipExpireTime().after(new Date())) {
            //如果是VIP会员,则续费
            userVipService.setStartTime(userInfo.getVipExpireTime());
            //续费会员过期时间=现有会员过期时间+套餐服务月数
            userVipService.setExpireTime(DateUtil.offsetMonth(userInfo.getVipExpireTime(), serviceMonth));
        } else {
            //3.1.3 获取用户身份,如果是普通用户,则新开
            userVipService.setStartTime(new Date());
            //续费会员过期时间=现有会员过期时间+套餐服务月数
            userVipService.setExpireTime(DateUtil.offsetMonth(new Date(), serviceMonth));
        }
        //3.1.4 构建VIP购买记录对象保存
        userVipService.setUserId(userPaidRecordVo.getUserId());
        userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
        userVipServiceMapper.insert(userVipService);

        //3.2 更新用户表中VIP状态及会员过期时间
        userInfo.setIsVip(1);
        userInfo.setVipExpireTime(userVipService.getExpireTime());
        userInfoMapper.updateById(userInfo);
    }
}

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

2.3.3 代码优化

​ 策略模式(Strategy Pattern)又叫政策模式(Policy Pattern),它是将定义的算法家族分别封装起来,让它们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户。属于行为型模式。

策略模式使用的就是面向对象的继承和多态机制,从而实现同一行为在不同场景下具备不同实现

优点:

  • 策略类之间可以自由切换:由于策略类都实现同一个接口,所以使它们之间可以自由切换。
  • 易于扩展:增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。

  • 环境(Context)类:用来操作策略的上下文环境,屏蔽高层模块(客户端)对策略、算法的直接访问,封装可能存在的变化。

策略接口

package com.atguigu.tingshu.user.strategy;

import com.atguigu.tingshu.vo.user.UserPaidRecordVo;

/**
 * 抽象:策略接口
 * 定义抽象方法:
 */
public interface ItemTypeStrategy {

    /**
     * 处理用户购买记录
     *
     * @param userPaidRecordVo
     */
    public void savePaidRecord(UserPaidRecordVo userPaidRecordVo);
}

VIP购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致

package com.atguigu.tingshu.user.strategy.imp;

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.ItemTypeStrategy;
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: 2023-12-27 11:43
 */
@Slf4j
@Component("1003")
public class VipStrategy implements ItemTypeStrategy {


    @Autowired
    private UserInfoMapper userInfoMapper;


    @Autowired
    private VipServiceConfigMapper vipServiceConfigMapper;


    @Autowired
    private UserVipServiceMapper userVipServiceMapper;

    /**
     * 处理购买项目类型为:VIP会员
     * @param userPaidRecordVo
     */
    @Override
    public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
        log.info("处理购买项目类型为:VIP会员");
        //3.判断购买项目类型-处理VIP会员-允许多次购买
        //3.1 新增VIP购买记录
        UserVipService userVipService = new UserVipService();
        //3.1.1 根据VIP套餐ID查询套餐信息-得到VIP会员服务月数
        Long vipConfigId = userPaidRecordVo.getItemIdList().get(0);
        VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(vipConfigId);
        Integer serviceMonth = vipServiceConfig.getServiceMonth();
        //3.1.2 获取用户身份,如果是VIP会员,则续费
        UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
        Integer isVip = userInfo.getIsVip();
        if (isVip.intValue() == 1 && userInfo.getVipExpireTime().after(new Date())) {
            //如果是VIP会员,则续费
            userVipService.setStartTime(userInfo.getVipExpireTime());
            //续费会员过期时间=现有会员过期时间+套餐服务月数
            userVipService.setExpireTime(DateUtil.offsetMonth(userInfo.getVipExpireTime(), serviceMonth));
        } else {
            //3.1.3 获取用户身份,如果是普通用户,则新开
            userVipService.setStartTime(new Date());
            //续费会员过期时间=现有会员过期时间+套餐服务月数
            userVipService.setExpireTime(DateUtil.offsetMonth(new Date(), serviceMonth));
        }
        //3.1.4 构建VIP购买记录对象保存
        userVipService.setUserId(userPaidRecordVo.getUserId());
        userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
        userVipServiceMapper.insert(userVipService);

        //3.2 更新用户表中VIP状态及会员过期时间
        userInfo.setIsVip(1);
        userInfo.setVipExpireTime(userVipService.getExpireTime());
        userInfoMapper.updateById(userInfo);
    }
}

专辑购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致

package com.atguigu.tingshu.user.strategy.imp;

import com.atguigu.tingshu.model.user.UserPaidAlbum;
import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper;
import com.atguigu.tingshu.user.strategy.ItemTypeStrategy;
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: 2023-12-27 11:43
 */
@Slf4j
@Component("1001") //将专辑类型购买策略实现类beanId跟前端提交购买项目类型一致
public class AlbumStrategy implements ItemTypeStrategy {

    @Autowired
    private UserPaidAlbumMapper userPaidAlbumMapper;

    /**
     * 处理购买项目类型为:专辑
     * @param userPaidRecordVo
     */
    @Override
    public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
        log.info("处理购买项目类型为:专辑");
        //1.1 根据订单编号查询专辑购买记录
        LambdaQueryWrapper<UserPaidAlbum> userPaidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>();
        userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo());
        Long count = userPaidAlbumMapper.selectCount(userPaidAlbumLambdaQueryWrapper);
        if (count > 0) {
            return;
        }
        //1.2 查询到专辑购买记录为空则新增购买记录
        UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
        userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
        userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
        userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
        userPaidAlbumMapper.insert(userPaidAlbum);
    }
}

声音购买项处理策略实现类:其中Bean对象ID要跟前端提交项目类型一致

package com.atguigu.tingshu.user.strategy.imp;

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.ItemTypeStrategy;
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: 2023-12-27 11:43
 */
@Slf4j
@Component("1002")  //beanID:trackStrategy
public class TrackStrategy implements ItemTypeStrategy {

    @Autowired
    private UserPaidTrackMapper userPaidTrackMapper;

    @Autowired
    private AlbumFeignClient albumFeignClient;


    /**
     * 处理购买项目类型为:声音
     * @param userPaidRecordVo
     */
    @Override
    public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
        log.info("处理购买项目类型为:声音");
        //2.判断购买项目类型-处理声音
        //2.1 根据订单编号查询声音购买记录
        LambdaQueryWrapper<UserPaidTrack> userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>();
        userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
        Long count = userPaidTrackMapper.selectCount(userPaidTrackLambdaQueryWrapper);
        if (count > 0) {
            return;
        }
        //2.2 查询到声音购买记录为空则新增购买记录(循环批量新增)
        //2.2.1 远程调用专辑服务-根据声音ID查询声音对象-获取声音所属专辑ID
        TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
        Long albumId = trackInfo.getAlbumId();
        //2.2.2 遍历购买项目ID集合批量新增声音购买记录
        userPaidRecordVo.getItemIdList().forEach(trackId -> {
            UserPaidTrack userPaidTrack = new UserPaidTrack();
            userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
            userPaidTrack.setUserId(userPaidRecordVo.getUserId());
            userPaidTrack.setAlbumId(albumId);
            userPaidTrack.setTrackId(trackId);
            userPaidTrackMapper.insert(userPaidTrack);
        });
    }
}

提供工厂类组装所有的策略实现类

package com.atguigu.tingshu.user.strategy;

import com.atguigu.tingshu.common.execption.GuiguException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 策略工厂:封装可能存在的变化(具体策略实现类)
 *
 * @author: atguigu
 * @create: 2023-12-27 11:46
 */
@Slf4j
@Component
public class StrategyFactory {


    /**
     * 将IOC容器中ItemTypeStrategy接口下所有实现类对象注入到Map中
     * Map中Key:Bean对象ID
     * Map中Value:实现类对象
     * {"1001":购买类型为专辑策略类对象}
     * {"1002":购买类型为声音策略类对象}
     * {"1003":购买类型为会员策略类对象}
     */
    @Autowired
    private Map<String, ItemTypeStrategy> strategyMap;


    /**
     * 根据购买项目类型获取具体策略实现类对象
     *
     * @param itemType 项目类型
     * @return
     */
    public ItemTypeStrategy getStrategy(String itemType) {
        if (strategyMap.containsKey(itemType)) {
            return strategyMap.get(itemType);
        }
        log.error("该策略实现类不存在");
        throw new GuiguException(500, "该策略" + itemType + "实现类不存在");
    }
}

UserInfoServiceImpl业务调用工厂获取不同策略实现类完成方法调用

@Autowired
private StrategyFactory strategyFactory;



/**
 * 处理用户购买记录(虚拟物品发货)
 *
 * @param userPaidRecordVo 购买记录VO
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
    ItemTypeStrategy strategy = strategyFactory.getStrategy(userPaidRecordVo.getItemType());
    strategy.savePaidRecord(userPaidRecordVo);
}

2.3 提交订单

我们将前端页面提交的数据统一封装到实体类OrderInfoVo中

package com.atguigu.tingshu.vo.order;

import com.atguigu.tingshu.common.util.Decimal2Serializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

@Data
@Schema(description = "订单对象")
public class OrderInfoVo {

    @NotEmpty(message = "交易号不能为空")
    @Schema(description = "交易号", required = true)
    private String tradeNo;

    @NotEmpty(message = "支付方式不能为空")
    @Schema(description = "支付方式:1101-微信 1102-支付宝 1103-账户余额", required = true)
    private String payWay;

    @NotEmpty(message = "付款项目类型不能为空")
    @Schema(description = "付款项目类型: 1001-专辑 1002-声音 1003-vip会员", required = true)
    private String itemType;

    /**
     * value:最小值
     * inclusive:是否可以等于最小值,默认true,>= 最小值
     * message:错误提示(默认有一个错误提示i18n支持中文)
     *
     * @DecimalMax 同上
     * @Digits integer: 整数位最多几位
     * fraction:小数位最多几位
     * message:同上,有默认提示
     */
    @DecimalMin(value = "0.00", inclusive = false, message = "订单原始金额必须大于0.00")
    @DecimalMax(value = "9999.99", inclusive = true, message = "订单原始金额必须大于9999.99")
    @Digits(integer = 4, fraction = 2)
    @Schema(description = "订单原始金额", required = true)
    @JsonSerialize(using = Decimal2Serializer.class)
    private BigDecimal originalAmount;

    @DecimalMin(value = "0.00", inclusive = true, message = "减免总金额必须大于0.00")
    @DecimalMax(value = "9999.99", inclusive = true, message = "减免总金额必须大于9999.99")
    @Digits(integer = 4, fraction = 2)
    @Schema(description = "减免总金额", required = true)
    @JsonSerialize(using = Decimal2Serializer.class)
    private BigDecimal derateAmount;

    @DecimalMin(value = "0.00", inclusive = false, message = "订单总金额必须大于0.00")
    @DecimalMax(value = "9999.99", inclusive = true, message = "订单总金额必须大于9999.99")
    @Digits(integer = 4, fraction = 2)
    @Schema(description = "订单总金额", required = true)
    @JsonSerialize(using = Decimal2Serializer.class)
    private BigDecimal orderAmount;

    @Valid
    @NotEmpty(message = "订单明细列表不能为空")
    @Schema(description = "订单明细列表", required = true)
    private List<OrderDetailVo> orderDetailVoList;

    @Schema(description = "订单减免明细列表")
    private List<OrderDerateVo> orderDerateVoList;

    @Schema(description = "时间戳", required = true)
    private Long timestamp;
    @Schema(description = "签名", required = true)
    private String sign;
}

service-order 微服务中添加提交订单控制器

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/99

2.4.1 控制器

OrderInfoApiController

/**
 * 提交订单,可能包含余额支付
 *
 * @param orderInfoVo
 * @return
 */
@GuiGuLogin
@Operation(summary = "提交订单,可能包含余额支付")
@PostMapping("/orderInfo/submitOrder")
public Result<Map<String, String>> submitOrder(@RequestBody OrderInfoVo orderInfoVo) {
    Long userId = AuthContextHolder.getUserId();
    Map<String, String> mapResult = orderInfoService.submitOrder(orderInfoVo, userId);
    return Result.ok(mapResult);
}

2.4.2 接口与实现

/**
 * 提交订单,处理余额付款情况
 * @param orderInfoVo 订单VO信息
 * @param userId 用户ID
 * @return
 */
Map<String, String> submitOrder(OrderInfoVo orderInfoVo, Long userId);

/**
 * 保存订单及订单商品明细优惠明细
 * @param orderInfoVo 订单信息VO对象
 * @param userId 用户ID
 * @return 保存后订单对象
 */
OrderInfo saveOrderInfo(OrderInfoVo orderInfoVo, Long userId);
@Autowired
private UserFeignClient userFeignClient;

@Autowired
private AccountFeignClient accountFeignClient;


/**
 * 提交订单,处理余额付款情况
 *
 * @param orderInfoVo 订单VO信息
 * @param userId      用户ID
 * @return
 */
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Map<String, String> submitOrder(OrderInfoVo orderInfoVo, Long userId) {
    //1.业务校验-验证流水号-解决订单重复提交问题
    String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
    //1.1 构建验证流水号lua脚本
    String scriptText = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";

    //2.2 执行脚本,如果脚本返回结果为false 抛出异常即可
    DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(scriptText, Boolean.class);
    boolean flag = (boolean) redisTemplate.execute(redisScript, Arrays.asList(tradeNoKey), orderInfoVo.getTradeNo());
    if (!flag) {
        throw new GuiguException(400, "流水号异常!");
    }
    //2.验证签名-解决用户篡改订单中数据
    //2.1 将提交订单VO参数转为Map 加签并未加入"payWay" ,手动将提交参数Map中payWay移除掉
    Map<String, Object> mapParams = BeanUtil.beanToMap(orderInfoVo);
    mapParams.remove("payWay");
    //2.2 调用签名工具类进行验签
    SignHelper.checkSign(mapParams);
    //3.保存订单及订单明细、优惠明细
    OrderInfo orderInfo = this.saveOrderInfo(orderInfoVo, userId);
    //4.处理余额付款 支付方式:1103 余额支付
    if (SystemConstant.ORDER_PAY_ACCOUNT.equals(orderInfoVo.getPayWay())) {
        // 4.1 TODO 余额支付-远程调用账户服务扣减账户余额
        AccountLockVo accountDeductVo = new AccountLockVo();
        accountDeductVo.setOrderNo(orderInfo.getOrderNo());
        accountDeductVo.setUserId(userId);
        accountDeductVo.setAmount(orderInfo.getOrderAmount());
        accountDeductVo.setContent(orderInfo.getOrderTitle());
        Result deductResult = accountFeignClient.checkAndDeduct(accountDeductVo);
        if (200 != deductResult.getCode()) {
            //扣减余额失败:全局事务都需要回滚
            throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
        }
        // 4.2 TODO 虚拟物品发货-远程调用用户服务新增购买记录
        UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
        userPaidRecordVo.setOrderNo(orderInfo.getOrderNo());
        userPaidRecordVo.setUserId(userId);
        userPaidRecordVo.setItemType(orderInfo.getItemType());
        List<Long> itemIdList = orderInfoVo.getOrderDetailVoList().stream().map(OrderDetailVo::getItemId).collect(Collectors.toList());
        userPaidRecordVo.setItemIdList(itemIdList);

        Result paidRecordResult = userFeignClient.savePaidRecord(userPaidRecordVo);
        if (200 != paidRecordResult.getCode()) {
            //新增购买记录失败:全局事务都需要回滚
            throw new GuiguException(211, "新增购买记录异常");
        }
        // 4.3 订单状态:已支付
        orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
        orderInfoMapper.updateById(orderInfo);
    }
    //5.响应提交成功订单编号
    Map<String, String> mapResult = new HashMap<>();
    mapResult.put("orderNo", orderInfo.getOrderNo());
    return mapResult;
}

2.4.3 保存订单方法

业务处理:

  • 1.将订单相关信息封装为订单对象,向订单表增加一条记录
  • 2.将提交订单明细封装为订单明细集合,批量向订单明细表新增若干条记录
  • 3.将提交优惠明细封装为优惠明细集合,批量向优惠明细表新增若干条记录

OrderInfoService接口

/**
 * 保存订单相关信息
 * @param userId 用户ID
 * @param orderInfoVo 订单VO信息
 * @return
 */
OrderInfo saveOrder(Long userId, OrderInfoVo orderInfoVo);

OrderInfoServiceImpl实现类

@Autowired
private OrderDetailMapper orderDetailMapper;

@Autowired
private OrderDerateMapper orderDerateMapper;

/**
 * 保存订单及订单商品明细优惠明细
 *
 * @param orderInfoVo 订单信息VO对象
 * @param userId      用户ID
 * @return 保存后订单对象
 */
@Override
@Transactional(rollbackFor = Exception.class)
public OrderInfo saveOrderInfo(OrderInfoVo orderInfoVo, Long userId) {
    //1.保存订单
    //1.1 通过拷贝将订单VO中信息拷贝到订单PO对象中
    OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class);
    //1.2 设置用户ID
    orderInfo.setUserId(userId);
    //1.3 为订单设置初始付款状态:未支付
    orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
    //1.4 生成全局唯一订单编号 形式:当日日期+雪花算法
    String orderNo = DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId();
    orderInfo.setOrderNo(orderNo);
    //1.5 保存订单
    orderInfoMapper.insert(orderInfo);
    Long orderId = orderInfo.getId();

    //2.保存订单商品明细
    List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
    if (CollectionUtil.isNotEmpty(orderDetailVoList)) {
        orderDetailVoList.forEach(orderDetailVo -> {
            OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class);
            //关联订单ID
            orderDetail.setOrderId(orderId);
            orderDetailMapper.insert(orderDetail);
        });
    }

    //3.保存订单优惠明细
    List<OrderDerateVo> orderDerateVoList = orderInfoVo.getOrderDerateVoList();
    if (CollectionUtil.isNotEmpty(orderDerateVoList)) {
        orderDerateVoList.forEach(orderDetailVo -> {
            OrderDerate orderDerate = BeanUtil.copyProperties(orderDetailVo, OrderDerate.class);
            //关联订单ID
            orderDerate.setOrderId(orderId);
            orderDerateMapper.insert(orderDerate);
        });
    }
    //4.返回订单对象
    return orderInfo;
}

3、我的订单

3.1 订单明细信息

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/100

当支付成功之后,点击查看订单

image-20231102120508592

service-order 微服务OrderInfoApiController中添加

/**
 * 查询当前用户指定订单信息
 *
 * @param orderNo
 * @return
 */
@GuiGuLogin
@GetMapping("/orderInfo/getOrderInfo/{orderNo}")
public Result<OrderInfo> getOrderInfo(@PathVariable("orderNo") String orderNo) {
    Long userId = AuthContextHolder.getUserId();
    OrderInfo orderInfo = orderInfoService.getOrderInfo(userId, orderNo);
    return Result.ok(orderInfo);
}

OrderInfoService接口

/**
 * 获取订单信息
 *
 * @param userId
 * @param orderNo
 * @return
 */
OrderInfo getOrderInfo(Long userId, String orderNo);

OrderInfoServiceImpl实现

/**
 * 根据订单编号查询订单详情(订单商品明细、订单优惠明细)
 *
 * @param orderNo
 * @return
 */
@Override
public OrderInfo getOrderInfo(String orderNo) {
    //1.根据订单编号查询订单信息
    LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(OrderInfo::getOrderNo, orderNo);
    OrderInfo orderInfo = orderInfoMapper.selectOne(queryWrapper);
    if(orderInfo!=null){
        //2.跟订单ID查询订单明细列表
        LambdaQueryWrapper<OrderDetail> orderDetailLambdaQueryWrapper = new LambdaQueryWrapper<>();
        orderDetailLambdaQueryWrapper.eq(OrderDetail::getOrderId, orderInfo.getId());
        List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailLambdaQueryWrapper);
        orderInfo.setOrderDetailList(orderDetailList);

        //3.跟订单ID查询订单优惠列表
        LambdaQueryWrapper<OrderDerate> orderDerateLambdaQueryWrapper = new LambdaQueryWrapper<>();
        orderDerateLambdaQueryWrapper.eq(OrderDerate::getOrderId, orderInfo.getId());
        List<OrderDerate> orderDerateList = orderDerateMapper.selectList(orderDerateLambdaQueryWrapper);
        orderInfo.setOrderDerateList(orderDerateList);

        //4.设置订单支付状态及支付方式:中文
        orderInfo.setOrderStatusName(this.getOrderStatusName(orderInfo.getOrderStatus()));
        orderInfo.setPayWayName(this.getPayWayName(orderInfo.getPayWay()));
        return orderInfo;
    }
    return null;
}

private String getOrderStatusName(String orderStatus) {
    if (SystemConstant.ORDER_STATUS_UNPAID.equals(orderStatus)) {
        return "未支付";
    } else if (SystemConstant.ORDER_STATUS_PAID.equals(orderStatus)) {
        return "已支付";
    } else if (SystemConstant.ORDER_STATUS_CANCEL.equals(orderStatus)) {
        return "取消";
    }
    return null;
}

/**
 * 根据支付方式编号得到支付名称
 *
 * @param payWay
 * @return
 */
private String getPayWayName(String payWay) {
    if (SystemConstant.ORDER_PAY_WAY_WEIXIN.equals(payWay)) {
        return "微信";
    } else if (SystemConstant.ORDER_PAY_ACCOUNT.equals(payWay)) {
        return "余额";
    } else if (SystemConstant.ORDER_PAY_WAY_ALIPAY.equals(payWay)) {
        return "支付宝";
    }
    return "";
}

3.2 我的订单列表

image-20231102121612056

YAPI接口地址:

service-orderOrderInfoApiController 控制器 添加

/**
 * 分页获取当前用户订单列表
 * @param page
 * @param limit
 * @return
 */
@GuiGuLogin
@Operation(summary = "分页获取当前用户订单列表")
@GetMapping("/orderInfo/findUserPage/{page}/{limit}")
public Result<Page<OrderInfo>> getUserOrderByPage(@PathVariable int page, @PathVariable int limit){
    Long userId = AuthContextHolder.getUserId();
    Page<OrderInfo> pageInfo = new Page<>(page, limit);
    pageInfo = orderInfoService.getUserOrderByPage(pageInfo, userId);
    return Result.ok(pageInfo);
}

OrderInfoService接口

/**
 * 分页获取当前用户订单列表
 * @param pageInfo
 * @param userId
 * @return
 */
Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId);

OrderInfoServiceImpl实现

/**
 * 分页获取当前用户订单列表
 *
 * @param pageInfo
 * @param userId
 * @return
 */
@Override
public Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId) {
    //1.调用持久层获取订单分页列表
    pageInfo = orderInfoMapper.getUserOrderByPage1(pageInfo, userId);
    //2.遍历处理订单状态、订单付费方式中文
    pageInfo.getRecords().forEach(orderInfo -> {
        orderInfo.setOrderStatusName(getOrderStatusName(orderInfo.getOrderStatus()));
        orderInfo.setPayWayName(getPayWayName(orderInfo.getPayWay()));
    });
    return pageInfo;
}

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
     * @return
     */
    Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, @Param("userId") Long userId);


    Page<OrderInfo> getUserOrderByPage1(Page<OrderInfo> pageInfo, @Param("userId") Long userId);
}

OrderInfoMapper.xml 映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >


<mapper namespace="com.atguigu.tingshu.order.mapper.OrderInfoMapper">


    <!--自定义 结果集-->
    <resultMap id="orderResultMap" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
        <id column="id" property="id"></id>
        <collection property="orderDetailList" ofType="com.atguigu.tingshu.model.order.OrderDetail" autoMapping="true">
            <id column="order_detail_id" property="id"></id>
        </collection>
    </resultMap>

    <select id="getUserOrderByPage" resultMap="orderResultMap">
        select
            oi.id,
            oi.order_title,
            oi.order_no,
            oi.order_status,
            oi.original_amount,
            oi.order_amount,
            oi.derate_amount,
            oi.item_type,
            oi.pay_way,
            oi.create_time,
            od.id order_detail_id,
            od.item_id,
            od.item_name,
            od.item_url,
            od.item_price
        from order_info oi left join order_detail od
                                     on od.order_id = oi.id
        where user_id = #{userId} and od.is_deleted = 0
        order by id desc
    </select>


    <!--自定义 结果集-->
    <resultMap id="orderResultMap1" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
        <id column="id" property="id"></id>
        <collection property="orderDetailList" column="id" ofType="com.atguigu.tingshu.model.order.OrderDetail" select="getOrderDetailList" autoMapping="true"></collection>
    </resultMap>

    <!--根据订单ID查询订单明细-->
    <select id="getOrderDetailList" resultType="com.atguigu.tingshu.model.order.OrderDetail">
        select * from order_detail where order_id = #{id}
    </select>

    <!--分页查询订单列表-->
    <select id="getUserOrderByPage1" resultMap="orderResultMap1">
        select
            oi.id,
            oi.order_title,
            oi.order_no,
            oi.order_status,
            oi.original_amount,
            oi.order_amount,
            oi.derate_amount,
            oi.item_type,
            oi.pay_way,
            oi.create_time
        from order_info oi
        where user_id = #{userId} and is_deleted = 0
        order by id desc
    </select>
</mapper>

4、订单延迟关单

4.1 什么是延迟任务

  • 定时任务:有固定周期的,有明确的触发时间
  • 延迟任务:没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件,任务可以立即执行,也可以延迟

image-20210513145942962

应用场景:

场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;

场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止

技术选择:

4.2 延迟关闭订单

service-util利用redissonClient 发送延迟消息,提供公共业务

package com.atguigu.tingshu.common.delay;

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 java.util.concurrent.TimeUnit;

/**
 * @author: atguigu
 * @create: 2023-12-02 14:20
 */
@Slf4j
@Component
public class DelayMsgService {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 基于Redisson(Redis)实现延迟消息
     *
     * @param data      数据
     * @param queueName 延迟队列名称
     * @param ttl       延迟时间:单位s
     */
    public void sendDelayMessage(String queueName, String data, int ttl) {
        try {
            //7.1 创建阻塞队列
            RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(queueName);
            //7.2 基于阻塞队列创建延迟队列
            RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
            //7.3 发送延迟消息 测试阶段:设置为30s
            delayedQueue.offer(data, ttl, TimeUnit.SECONDS);
            log.info("发送延迟消息成功:{}", data);
        } catch (Exception e) {
            log.error("[延迟消息]发送异常:{}", data);
            throw new RuntimeException(e);
        }
    }
}

service-order订单模块中提交订单方法中

@Autowired
private DelayMsgService delayMsgService;

/**
 * 提交订单
 *
 * @param userId
 * @param orderInfoVo
 * @return
 */
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public String submitOrder(Long userId, OrderInfoVo orderInfoVo) {
    //...省略
    //7.发送延迟消息-完成订单延迟关单
    delayMsgService.sendDelayMessage(KafkaConstant.QUEUE_ORDER_CANCEL, orderInfo.getId().toString(), 30);
    //6.响应新增订单后订单编号(全局唯一)
    return orderInfo.getOrderNo();
}

监听消息:

package com.atguigu.tingshu.order.handler;

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;

/**
 * @author: atguigu
 * @create: 2023-12-27 16:04
 */
@Slf4j
@Component
public class DelayMsgConsumer {


    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private OrderInfoService orderInfoService;

    /**
     * 项目启动后开启线程监听阻塞队列中消息
     */
    @PostConstruct
    public void orderCancal() {
        log.info("开启线程监听延迟消息:");
        //1.创建阻塞队列(当队列内元素超过上限,继续队列发送消息,进入阻塞状态/当队列中元素为空,继续拉取消息,进入阻塞状态)
        RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(KafkaConstant.QUEUE_ORDER_CANCEL);
        //2.开启线程监听阻塞队列中消息 只需要单一核心线程线程池对象即可
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(()->{
            while (true) {
                String take = null;
                try {
                    take = blockingQueue.take();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if (StringUtils.isNotBlank(take)) {
                    log.info("监听到延迟关单消息:{}", take);
                    //查询订单状态,关闭订单
                    orderInfoService.orderCanncal(Long.valueOf(take));
                }
            }
        });
    }
}

接口:

/**
 * 取消订单
 * @param orderId
 */
void orderCanncel(String orderId);

实现类:

/**
 * 查询订单支付状态,完成订单关闭
 *
 * @param valueOf
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void orderCanncel(Long valueOf) {
    //1.根据订单ID查询订单状态
    OrderInfo orderInfo = orderInfoMapper.selectById(valueOf);
    if (orderInfo != null && SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus())) {
        //2.如果订单为未支付,说明超时未付款-修改为关闭
        orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_CANCEL);
        orderInfoMapper.updateById(orderInfo);
    }

}