第7章 订单.md 83 KB

谷粒随享

第7章 订单

学习目标:

  • 订单结算(页面渲染)对价格计算
    • 购买项目-VIP(允许重复购买)
    • 购买项目-专辑(只能购买一次)
    • 购买项目-声音(只能购买一次)
    • 结算商品代码优化-设计模式(策略模式+工厂模式)
  • 提交订单(余额付款)-->确保余额安全性
    • 分布式事务解决方案-Seata
    • 虚拟物品发货代码优化-设计模式(策略模式+工厂模式)
  • 我的订单
  • 订单延迟关单
    • RabbitMQ提供延迟任务

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.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() {
        //1.获取当前登录用户ID
        Long userId = AuthContextHolder.getUserId();
        //2.获取可用余额
        BigDecimal availableAmount = userAccountService.getAvailableAmount(userId);
        return Result.ok(availableAmount);
    }
}

UserAccountService接口:

/**
 * 查询当前用户账户可用余额
 * @param userId
 * @return
 */
BigDecimal getAvailableAmount(Long userId);

UserAccountServiceImpl实现类:

/**
 * 查询当前用户账户可用余额
 *
 * @param userId
 * @return
 */
@Override
public BigDecimal getAvailableAmount(Long userId) {
    LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserAccount::getUserId, userId);
    queryWrapper.select(UserAccount::getAvailableAmount);
    UserAccount userAccount = userAccountMapper.selectOne(queryWrapper);
    if (userAccount != null) {
        return userAccount.getAvailableAmount();
    }
    return new BigDecimal("0.00");
}

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

1.2 VIP会员结算

订单-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套餐列表

订单-获取Vip套餐列表

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

service-user 微服务模块中的 VipServiceConfigApiController 控制器添加映射路径,返回 VipServiceConfig 实体类的集合数据。也就是查询vip_service_config表中的集合数据。

package com.atguigu.tingshu.user.api;

import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.service.VipServiceConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Tag(name = "VIP服务配置管理接口")
@RestController
@RequestMapping("api/user")
@SuppressWarnings({"all"})
public class VipServiceConfigApiController {

	@Autowired
	private VipServiceConfigService vipServiceConfigService;


    /**
     * 获取平台套餐列表
     *
     * @return
     */
    @Operation(summary = "获取平台套餐列表")
    @GetMapping("/vipServiceConfig/findAll")
    public Result<List<VipServiceConfig>> findAll() {
        List<VipServiceConfig> list = vipServiceConfigService.list();
        return Result.ok(list);
    }
}

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查询套餐详情
 * @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.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;


    /**
     * 三种商品(VIP会员、专辑、声音)订单结算,渲染订单结算页面
     *
     * @param tradeVo (购买项目类型、购买项目ID、声音数量)
     * @return 订单VO信息
     */
    @GuiGuLogin
    @Operation(summary = "三种商品(VIP会员、专辑、声音)订单结算")
    @PostMapping("/orderInfo/trade")
    public Result<OrderInfoVo> trade(@RequestBody TradeVo tradeVo) {
        OrderInfoVo orderInfoVo = orderInfoService.trade(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 tradeVo (购买项目类型、购买项目ID、声音数量)
     * @return 订单VO信息
     */
    OrderInfoVo trade(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 AlbumFeignClient albumFeignClient;

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 三种商品(VIP会员、专辑、声音)订单结算,渲染订单结算页面
     *
     * @param tradeVo (购买项目类型、购买项目ID、声音数量)
     * @return 订单VO信息
     */
    @Override
    public OrderInfoVo trade(TradeVo tradeVo) {
        //0.创建订单VO对象
        OrderInfoVo orderInfoVo = new OrderInfoVo();
        Long userId = AuthContextHolder.getUserId();
        //1.初始化订单VO中价格、订单明细列表、订单减免明细列表
        //1.1 声明三个初始价格:原价、减免金额、订单金额
        BigDecimal originalAmount = new BigDecimal("0.00");
        BigDecimal orderAmount = new BigDecimal("0.00");
        BigDecimal derateAmount = new BigDecimal("0.00");
        //1.2 声明封装订单明细集合
        List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
        //1.3 声明封装订单减免明细集合
        List<OrderDerateVo> orderDerateVoList = new ArrayList<>();

        //获取购买项目类型 1001-专辑 1002-声音 1003-vip会员
        String itemType = tradeVo.getItemType();
        //2.处理购买项目类型:VIP套餐
        if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
            //2.1 远程调用用户服务,得到购买VIP会员套餐信息
            VipServiceConfig vipServiceConfig = userFeignClient.getVipServiceConfig(tradeVo.getItemId()).getData();
            Assert.notNull(vipServiceConfig, "会员套餐{}不存在", tradeVo.getItemId());
            //2.2 计算商品价格:订单价=原价-减免金额
            originalAmount = vipServiceConfig.getPrice();
            orderAmount = vipServiceConfig.getDiscountPrice();
            if (originalAmount.compareTo(orderAmount) == 1) {
                derateAmount = originalAmount.subtract(orderAmount);
            }
            //2.3 将得到VIP套餐作为订单明细,封装成订单明细VO对象,加入到订单明细列表
            OrderDetailVo orderDetailVo = new OrderDetailVo();
            orderDetailVo.setItemId(vipServiceConfig.getId());
            orderDetailVo.setItemName("会员套餐:" + vipServiceConfig.getName());
            orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl());
            orderDetailVo.setItemPrice(originalAmount);
            orderDetailVoList.add(orderDetailVo);

            //2.4 如有优惠金额,封装订单减免vo对象,加入到订单减免列表
            if (originalAmount.compareTo(orderAmount) == 1) {
                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信息共有的信息,价格赋值、商品明细列表赋值、订单减免明细列表赋值、流水号机制、签名机制
        //5.1 封装第一类信息:三个价格(原价、订单价、减免价)
        orderInfoVo.setOriginalAmount(originalAmount);
        orderInfoVo.setOrderAmount(orderAmount);
        orderInfoVo.setDerateAmount(derateAmount);

        //5.2 封装第二类信息:商品明细列表、优惠列表
        orderInfoVo.setOrderDetailVoList(orderDetailVoList);
        orderInfoVo.setOrderDerateVoList(orderDerateVoList);

        //5.3 封装第三类信息:购买项目类型、流水号机制(本次订单流水号)、签名机制(时间戳+签名值)
        //5.3.1 购买项目类型
        orderInfoVo.setItemType(itemType);
        //5.3.2 流水号机制第一步:生成本次订单流水号,解决回退、重复点击提交造成订单重复提交问题
        //5.3.2.1. 构建当前用户本次订单流水号Key
        String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
        //5.3.2.2. 构建当前用户本次订单流水号UUID值
        String tradeNo = IdUtil.randomUUID();
        //5.3.2.3. 写入Redis有效时间5分钟
        redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES);
        orderInfoVo.setTradeNo(tradeNo);

        //5.3.3 签名机制
        //5.3.3.1. 生成本次订单时间戳
        long timeMillis = System.currentTimeMillis();
        orderInfoVo.setTimestamp(timeMillis);
        //5.3.3.2 将订单VO转为Map,生成本次订单签名值 TODO 由于付款方式属性payWay为null不需要参与签名
        Map<String, Object> orderInfoMap = BeanUtil.beanToMap(orderInfoVo, false, true);
        String sign = SignHelper.getSign(orderInfoMap);
        //5.3.3.3. 封装Vo中签名值
        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) {
    Long userId = AuthContextHolder.getUserId();
    Boolean isPaidAlbum = userInfoService.isPaidAlbum(userId, albumId);
    return Result.ok(isPaidAlbum);
}

UserInfoService接口

/**
 * 查询当前用户是否购买指定专辑
 * @param albumId
 * @return
 */
Boolean isPaidAlbum(Long albumId);

UserInfoServiceImpl实现类

/**
 * 判断当前用户是否购买过指定专辑
 *
 * @param albumId
 * @return
 */
@Override
public Boolean isPaidAlbum(Long userId, Long albumId) {
    Long count = userPaidAlbumMapper.selectCount(
            new LambdaQueryWrapper<UserPaidAlbum>()
                    .eq(UserPaidAlbum::getUserId, userId)
                    .eq(UserPaidAlbum::getAlbumId, albumId)
    );
    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.3 OrderInfoServiceImpl实现类

/**
 * 订单确认数据汇总
 *
 * @param userId  用户ID
 * @param tradeVo 选择购买商品交易vo对象
 * @return
 */
@Override
public OrderInfoVo tradeData(Long userId, TradeVo tradeVo) {
    //0.创建订单VO对象
    OrderInfoVo orderInfoVo = new OrderInfoVo();
    //...省略已写代码

    //获取购买项目类型 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 flag = userFeignClient.isPaidAlbum(albumId).getData();
        if (flag) {
            throw new GuiguException(500, "用户已购买专辑");
        }

        //3.2 远程调用专辑服务,获取专辑价格以及相关折扣(普通、VIP折扣)
        AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData();
        Assert.notNull(albumInfo, "专辑{}不存在", albumId);
        BigDecimal price = albumInfo.getPrice();
        BigDecimal discount = albumInfo.getDiscount();
        BigDecimal vipDiscount = albumInfo.getVipDiscount();

        //3.3 远程调用用户服务,判断当前用户是否是会员
        Boolean isVIP = false;

        UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData();
        Assert.notNull(userInfoVo, "用户{}不存在", userId);
        if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(new Date())) {
            isVIP = true;
        }

        //3.4 基于用户角色专辑折扣计算专辑价格(原价、订单价、减免价格)
        originalAmount = price;
        orderAmount = originalAmount;
        if (!isVIP && discount.doubleValue() != -1) {
            //专辑价格 普通用户折扣  订单价格=原价*折扣
            orderAmount =
                    originalAmount.multiply(discount).divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
        }
        if (isVIP && vipDiscount.doubleValue() != -1) {
            //专辑价格 VIP会员折扣  订单价格=原价*折扣
            orderAmount =
                    originalAmount.multiply(vipDiscount).divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
        }
        //计算专辑价格减免金额 = 原价-订单价格
        derateAmount = originalAmount.subtract(orderAmount);
        //3.5 将专辑信息作为订单明细,封装成订单明细VO对象,加入到订单明细列表
        OrderDetailVo orderDetailVo = new OrderDetailVo();
        orderDetailVo.setItemId(albumId);
        orderDetailVo.setItemName("专辑:" + albumInfo.getAlbumTitle());
        orderDetailVo.setItemUrl(albumInfo.getCoverUrl());
        orderDetailVo.setItemPrice(originalAmount);
        orderDetailVoList.add(orderDetailVo);

        //3.6 如有优惠金额,封装订单减免vo对象,加入到订单减免列表
        if (originalAmount.compareTo(orderAmount) == 1) {
            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.省略已写代码
    return orderInfoVo;
}

1.4 声音结算

1.4.1 声音分集购买列表

订单-声音结算

1.4.1.1 获取已购声音Id列表Feign接口

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

service-user模块

UserInfoApiController控制器
/**
 * 查询当前用户某个专辑下已购声音ID列表
 * @param albumId
 * @return
 */
@GuiGuLogin
@Operation(summary = "查询当前用户某个专辑下已购声音ID列表")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> findUserPaidTrackList(@PathVariable Long albumId) {
    Long userId = AuthContextHolder.getUserId();
    List<Long> list = userInfoService.findUserPaidTrackList(userId, albumId);
    return Result.ok(list);
}
UserInfoService接口
/**
 * 查询当前用户某个专辑下已购声音ID列表
 * @param albumId
 * @return
 */
List<Long> findUserPaidTrackList(Long userId, Long albumId);
UserInfoServiceImpl实现类
/**
 * 查询当前用户某个专辑下已购声音ID列表
 *
 * @param albumId
 * @return
 */
@Override
public List<Long> findUserPaidTrackList(Long userId, Long albumId) {
    List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(
            new LambdaQueryWrapper<UserPaidTrack>()
                    .eq(UserPaidTrack::getUserId, userId)
                    .eq(UserPaidTrack::getAlbumId, albumId)
                    .select(UserPaidTrack::getTrackId)
    );
    if (CollUtil.isNotEmpty(userPaidTrackList)) {
        List<Long> userPaidTrackIdList = userPaidTrackList.stream()
                .map(UserPaidTrack::getTrackId)
                .collect(Collectors.toList());
        return userPaidTrackIdList;
    }
    return null;
}

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

/**
 * 查询当前用户某个专辑下已购声音ID列表
 * @param albumId
 * @return
 */
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> findUserPaidTrackList(@PathVariable Long albumId);

UserDegradeFeignClient熔断类

@Override
public Result<List<Long>> findUserPaidTrackList(Long albumId) {
    log.error("[用户服务]远程调用findUserPaidTrackList执行服务降级");
    return null;
}

1.4.1.2 声音分集购买列表

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-整专辑

Map<String, Object> map = new HashMap<>();
map.put("name","本集"); // 显示文本
map.put("price",albumInfo.getPrice()); // 专辑声音对应的价格
map.put("trackCount",1); // 记录购买集数
list.add(map);
/**
 * 基于当前选择购买起始声音得到分集购买列表
 *
 * @param trackId
 * @return [{name:"本集",price:1,trackCount:1},{name:"后10集",price:10,trackCount:10}...{name:"全集/45集",price:45,trackCount:45}]
 */
@GuiGuLogin
@Operation(summary = "基于当前选择购买起始声音得到分集购买列表")
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> getUserTrackPaidList(@PathVariable Long trackId) {
    //1.从ThreadLocal中获取当前登录用户ID
    Long userId = AuthContextHolder.getUserId();
    //2.调用业务逻辑获取分集购买列表
    List<Map<String, Object>> list = albumInfoService.getUserTrackPaidList(userId, trackId);
    return Result.ok(list);
}
AlbumInfoService接口
/**
 * 基于当前用户未购买声音数量动态渲染分集购买列表
 * @param userId 用户ID
 * @param trackId 选择购买声音ID,作为起始标准计算未购买声音数量
 * @return
 */
List<Map<String, Object>> findUserTrackPaidList(Long userId, Long trackId);
AlbumInfoServiceImpl实现类

思路:

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

image-20231103002225859

/**
 * 基于当前用户未购买声音数量动态渲染分集购买列表
 *
 * @param userId  用户ID
 * @param trackId 选择购买声音ID,作为起始标准计算未购买声音数量
 * @return [{name:"本集",price:0.1,trackCount:1},{name:"后10集",price:1,trackCount:10}..,{name:"全集",price:3.8,trackCount:38}]
 */
@Override
public List<Map<String, Object>> findUserTrackPaidList(Long userId, Long trackId) {
    //1.根据选择购买声音ID查询声音记录,得到声音序号,得到专辑ID
    TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
    Integer orderNum = trackInfo.getOrderNum();
    Long albumId = trackInfo.getAlbumId();
    //2.根据"起始声音"需要+专辑ID查询声音表得到“未购买声音列表”,这里可能包含已购买声音
    List<TrackInfo> waitBuyTrackList = trackInfoMapper.selectList(
            new LambdaQueryWrapper<TrackInfo>()
                    .eq(TrackInfo::getAlbumId, albumId)
                    .ge(TrackInfo::getOrderNum, orderNum)
                    .select(TrackInfo::getId)
    );

    //3.远程调用用户服务,得到用户已购声音ID列表,如果有值将已购买声音ID排除掉
    List<Long> userPaidTrackIdList = userFeignClient.findUserPaidTrackList(albumId).getData();
    if (CollUtil.isNotEmpty(userPaidTrackIdList)) {
        waitBuyTrackList =
                waitBuyTrackList.stream()
                        .filter(t -> !userPaidTrackIdList.contains(t.getId()))
                        .collect(Collectors.toList());
    }

    //4.基于未购买声音数量,动态封装分集购买列表
    int size = waitBuyTrackList.size();
    List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
    //4.0 查询专辑记录得到单集价格
    AlbumInfo albumInfo = albumInfoMapper.selectById(albumId);
    BigDecimal price = albumInfo.getPrice();
    ///4.1 创建"本集"分集购买对象
    Map<String, Object> currMap = new HashMap<>();
    currMap.put("name", "本集");
    currMap.put("price", price);
    currMap.put("trackCount", 1);
    list.add(currMap);
    //4.2 创建"后i*10集"分集购买对象
    for (int i = 10; i < 50; i += 10) {
        Map<String, Object> map = new HashMap<>();
        if (i < size) {
            map.put("name", "后" + i + "集");
            map.put("price", price.multiply(new BigDecimal(i)));
            map.put("trackCount", i);
            list.add(map);
        } else {
            map.put("name", "全集(后" + size + "集)");
            map.put("price", price.multiply(new BigDecimal(size)));
            map.put("trackCount", size);
            list.add(map);
            break;
        }
    }
    return list;
}

1.4.2 获取待购声音列表Feign接口

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>> findWaitBuyTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount) {
    Long userId = AuthContextHolder.getUserId();
    List<TrackInfo> list = trackInfoService.findWaitBuyTrackInfoList(userId, trackId, trackCount);
    return Result.ok(list);
}

TrackInfoService接口

/**
 *  查询用户未购买声音列表
 * @param trackId 选择购买声音ID,作为起始标准计算未购买声音数量
 * @param trackCount 限制查询未购买声音数量
 * @return
 */
List<TrackInfo> findWaitBuyTrackInfoList(Long userId, Long trackId, Integer trackCount);

实现类

思路:

1. 根据声音Id 获取到当前声音对象
2. 远程调用用户服务获取已购买声音ID列表
3. 根据声音ID+当前声音序号查询待购声音列表,将已购声音进行排除
4. 返回待购声音集合对象返回
/**
 * 查询用户未购买声音列表
 *
 * @param trackId    选择购买声音ID,作为起始标准计算未购买声音数量
 * @param trackCount 限制查询未购买声音数量
 * @return
 */
@Override
public List<TrackInfo> findWaitBuyTrackInfoList(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<Long> userPaidTrackList = userFeignClient.findUserPaidTrackList(albumId).getData();

    //3.构建查询条件查询未购买声音列表
    LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<TrackInfo>()
            .eq(TrackInfo::getAlbumId, albumId)
            .ge(TrackInfo::getOrderNum, orderNum)
            .orderByAsc(TrackInfo::getOrderNum)
            .select(TrackInfo::getId, TrackInfo::getTrackTitle, TrackInfo::getCoverUrl, TrackInfo::getAlbumId)
            .last("limit " + trackCount);
    if (CollUtil.isNotEmpty(userPaidTrackList)) {
        queryWrapper.notIn(TrackInfo::getId, userPaidTrackList);
    }
    List<TrackInfo> trackInfoList = trackInfoMapper.selectList(queryWrapper);
    return trackInfoList;
}

AlbumFeignClient

/**
 *  查询用户未购买声音列表
 * @param trackId 选择购买声音ID,作为起始标准计算未购买声音数量
 * @param trackCount 限制查询未购买声音数量
 * @return
 */
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> findWaitBuyTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount);

熔断类:

@Override
public Result<List<TrackInfo>> findWaitBuyTrackInfoList(Long trackId, Integer trackCount) {
    log.error("【专辑服务】提供findWaitBuyTrackInfoList远程调用失败");
    return null;
}

1.4.3 改造订单确认方法

/**
 * 三种商品(VIP会员、专辑、声音)订单结算,渲染订单结算页面
 *
 * @param tradeVo (购买项目类型、购买项目ID、声音数量)
 * @return 订单VO信息
 */
@Override
public OrderInfoVo trade(TradeVo tradeVo) {
    //0.创建订单VO对象
    //...省略已有代码
    //获取购买项目类型 1001-专辑 1002-声音 1003-vip会员
    String itemType = tradeVo.getItemType();
    //2.处理购买项目类型:VIP套餐
    if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
        //...省略已有代码
    } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
        //3. 处理购买项目类型:专辑
        //...省略已有代码
    } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) {
        //4.处理购买项目类型:声音

        //4.1 远程调用专辑服务获取待购买声音列表
        Long trackId = tradeVo.getItemId();
        List<TrackInfo> trackInfoList = albumFeignClient.findWaitBuyTrackInfoList(trackId, tradeVo.getTrackCount()).getData();
        Assert.notNull(trackInfoList, "没有需要结算声音");

        //4.2 远程调用专辑服务获取声音单价-声音不支持折扣
        AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(trackInfoList.get(0).getAlbumId()).getData();
        Assert.notNull(albumInfo, "专辑{}不存在", trackInfoList.get(0).getAlbumId());
        BigDecimal price = albumInfo.getPrice();

        //4.3 封装相关价格
        originalAmount = price.multiply(new BigDecimal(trackInfoList.size()));
        orderAmount = originalAmount;

        //4.4 将待购买声音作为订单明细,封装成订单明细VO对象,加入到订单明细列表
        orderDetailVoList = trackInfoList
                .stream()
                .map(trackInfo -> {
                    OrderDetailVo orderDetailVo = new OrderDetailVo();
                    orderDetailVo.setItemId(trackInfo.getId());
                    orderDetailVo.setItemName("声音:" + trackInfo.getTrackTitle());
                    orderDetailVo.setItemUrl(trackInfo.getCoverUrl());
                    orderDetailVo.setItemPrice(price);
                    return orderDetailVo;
                }).collect(Collectors.toList());

    }

    //5.封装订单VO信息共有的信息,价格赋值、商品明细列表赋值、订单减免明细列表赋值、流水号机制、签名机制
    //...省略已有代码
    //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
 */
@Operation(summary = "检查扣减账户余额")
@PostMapping("/userAccount/checkAndDeduct")
public Result checkAndDeduct(@RequestBody AccountDeductVo accountDeductVo) {
    userAccountService.deduct(accountDeductVo);
    return Result.ok();
}

业务接口UserAccountService

/**
 * 检查扣减账户余额
 * @param accountDeductVo
 */
void deduct(AccountDeductVo accountDeductVo);

业务实现类UserAccountService

/**
 * 检查扣减账户余额
 * 避免账户余额超扣,采用分布式锁;数据库锁(悲观锁或乐观锁)
 *
 * @param accountDeductVo
 */
@Override
//@Transactional(rollbackFor = Exception.class) //TODO 引入Seata一定要去掉,否则会导致分布式事务失效
public void deduct(AccountDeductVo accountDeductVo) {
    //1.采用数据库悲观锁,检查账户余额是否满足扣款需求
    BigDecimal amount = accountDeductVo.getAmount();
    Long userId = accountDeductVo.getUserId();
    UserAccount userAccount = userAccountMapper.checkDeduction(userId, amount);
    if (userAccount == null) {
        throw new GuiguException(500, "账户余额不足!");
    }
    //2.扣减账户余额
    userAccountMapper.update(
            null,
            new LambdaUpdateWrapper<UserAccount>()
                    .eq(UserAccount::getUserId, userId)
                    .setSql("total_amount = total_amount -" + amount)
                    .setSql("available_amount = available_amount -" + amount)
                    .setSql("total_pay_amount = total_pay_amount +" + amount)
            );
    //3.新增账户变动日志
    this.saveUserAccountDetail(accountDeductVo.getUserId(), accountDeductVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountDeductVo.getAmount(), accountDeductVo.getOrderNo());
}

UserAccountMapper

package com.atguigu.tingshu.account.mapper;

import com.atguigu.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

@Mapper
public interface UserAccountMapper extends BaseMapper<UserAccount> {

    /**
     * 采用悲观锁,检查账户余额是否满足扣款需求
     * @param userId
     * @param amount
     * @return
     */
    UserAccount checkDeduction(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}

UserAccountMapper.xml

<!--采用悲观锁,检查账户余额是否满足扣款需求-->
<select id="checkDeduction" resultType="com.atguigu.tingshu.model.account.UserAccount">
	SELECT
		*
	FROM
		user_account
	WHERE
		user_id = #{userId}
	  AND available_amount >= #{amount} FOR UPDATE
</select>

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控制器

/**
 * 用户支付成功后,虚拟物品发货
 * @param userPaidRecordVo
 * @return
 */
@Operation(summary = "用户支付成功后,虚拟物品发货")
@PostMapping("/userInfo/savePaidRecord")
public Result savePaidRecord(@RequestBody UserPaidRecordVo userPaidRecordVo) {
    userInfoService.savePaidRecord(userPaidRecordVo);
    return Result.ok();
}

UserInfoService业务接口

/**
 * 保存用户不同购买项目类型的购买记录
 * @param userPaidRecordVo
 */
void savePaidRecord(UserPaidRecordVo userPaidRecordVo);

业务实现类UserInfoServiceImpl

@Autowired
private AlbumFeignClient albumFeignClient;

@Autowired
private UserVipServiceMapper userVipServiceMapper;

@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;

/**
 * 用户支付成功后,虚拟物品发货
 *
 * @param userPaidRecordVo
 * @return
 */
@Override
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
    //TODO 策略模式+工厂模式优化
    //付款项目类型: 1001-专辑 1002-声音 1003-vip会员
    String itemType = userPaidRecordVo.getItemType();
    //1.处理虚拟物品"专辑"发货逻辑
    if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
        //1.1 根据订单编号查询已购专辑记录,如果存在,则返回
        Long count = userPaidAlbumMapper.selectCount(
                new LambdaQueryWrapper<UserPaidAlbum>()
                        .eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo())
        );
        if (count > 0) {
            log.info("已存在该订单:{}的已购专辑记录", userPaidRecordVo.getOrderNo());
            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(itemType)) {
        //2. 处理虚拟物品"声音"发货逻辑
        //2.1 根据订单编号查询已购声音记录,如果存在,则返回
        Long count = userPaidTrackMapper.selectCount(
                new LambdaQueryWrapper<UserPaidTrack>()
                        .eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo())
        );
        if (count > 0) {
            log.info("已存在该订单:{}的已购声音记录", userPaidRecordVo.getOrderNo());
            return;
        }
        //2.2 如果不存在,则构建声音已购记录完成保存
        TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
        List<Long> trackIdList = userPaidRecordVo.getItemIdList();
        for (Long trackId : trackIdList) {
            UserPaidTrack userPaidTrack = new UserPaidTrack();
            userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
            userPaidTrack.setUserId(userPaidRecordVo.getUserId());
            userPaidTrack.setAlbumId(trackInfo.getAlbumId());
            userPaidTrack.setTrackId(trackId);
            userPaidTrackMapper.insert(userPaidTrack);
        }
    } else if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
        //3. 处理虚拟物品"会员"发货逻辑
        //3.1 根据订单编号查询已购会员记录,如果存在,则返回
        Long count = userVipServiceMapper.selectCount(
                new LambdaQueryWrapper<UserVipService>()
                        .eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo())
        );
        if (count > 0) {
            return;
        }
        //3.2 判断当前用户是否为会员
        Boolean isVIP = false;
        UserInfoVo userInfoVo   = this.getUserInfo(userPaidRecordVo.getUserId());
        Date now = new Date();
        if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(now)) {
            isVIP = true;
        }
        //3.3 新增会员购买记录
        UserVipService userVipService = new UserVipService();
        userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
        userVipService.setUserId(userPaidRecordVo.getUserId());
        //3.3.1 获取会员套餐信息得到服务月数
        VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0));
        Integer serviceMonth = vipServiceConfig.getServiceMonth();
        //3.3.2 计算本次会员购买记录生效时间
        //3.3.3 计算本次会员购买记录失效时间
        if (!isVIP) {
            //普通用户,生效时间为当前时间 失效时间=当前时间+购买会员套餐月数
            userVipService.setStartTime(now);
            DateTime expireTime = DateUtil.offsetMonth(now, serviceMonth);
            userVipService.setExpireTime(expireTime);
        } else {
            //会员用户,生效时间为=会员的到期时间+1 失效时间=会员的到期时间+购买会员套餐月数
            Date vipExpireTime = userInfoVo.getVipExpireTime();  //6.14
            userVipService.setStartTime(DateUtil.offsetDay(vipExpireTime, 1)); //6.15
            userVipService.setExpireTime(DateUtil.offsetMonth(userVipService.getStartTime(), serviceMonth)); //7.15
        }
        userVipServiceMapper.insert(userVipService);
        //3.4 更新用户会员标识以及更新会员过期时间
        userInfoVo.setIsVip(1);
        userInfoVo.setVipExpireTime(userVipService.getExpireTime());
        UserInfo userInfo = BeanUtil.copyProperties(userInfoVo, UserInfo.class);
        userInfoMapper.updateById(userInfo);
    }
}

service-user模块中Feign接口提供远程调用方法:UserFeignClient

/**
 * 用户付款成功后虚拟物品(VIP,专辑,声音)发货
 * @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.pattern;

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

/**
 * 抽象”发货“策略接口
 */
public interface DeliveryStrategy {

    /**
     * 虚拟物品"发货"抽象方法
     * @param userPaidRecordVo
     */
    void delivery(UserPaidRecordVo userPaidRecordVo);

}

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

package com.atguigu.tingshu.user.pattern.impl;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.model.user.UserVipService;
import com.atguigu.tingshu.model.user.VipServiceConfig;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.mapper.UserVipServiceMapper;
import com.atguigu.tingshu.user.mapper.VipServiceConfigMapper;
import com.atguigu.tingshu.user.pattern.DeliveryStrategy;
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: 2025-06-16 09:36
 */
@Slf4j
@Component(SystemConstant.ORDER_ITEM_TYPE_VIP)
public class VIPDelivery implements DeliveryStrategy {

    @Autowired
    private UserVipServiceMapper userVipServiceMapper;

    @Autowired
    private VipServiceConfigMapper vipServiceConfigMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    /**
     * 虚拟物品-普通会员-发货行为实现
     *
     * @param userPaidRecordVo
     */
    @Override
    public void delivery(UserPaidRecordVo userPaidRecordVo) {
        //3. 处理虚拟物品"会员"发货逻辑
        //3.1 根据订单编号查询已购会员记录,如果存在,则返回
        Long count = userVipServiceMapper.selectCount(
                new LambdaQueryWrapper<UserVipService>()
                        .eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo())
        );
        if (count > 0) {
            return;
        }
        //3.2 判断当前用户是否为会员
        Boolean isVIP = false;
        UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
        Date now = new Date();
        if (userInfo.getIsVip().intValue() == 1 && userInfo.getVipExpireTime().after(now)) {
            isVIP = true;
        }
        //3.3 新增会员购买记录
        UserVipService userVipService = new UserVipService();
        userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
        userVipService.setUserId(userPaidRecordVo.getUserId());
        //3.3.1 获取会员套餐信息得到服务月数
        VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0));
        Integer serviceMonth = vipServiceConfig.getServiceMonth();
        //3.3.2 计算本次会员购买记录生效时间
        //3.3.3 计算本次会员购买记录失效时间
        if (!isVIP) {
            //普通用户,生效时间为当前时间 失效时间=当前时间+购买会员套餐月数
            userVipService.setStartTime(now);
            DateTime expireTime = DateUtil.offsetMonth(now, serviceMonth);
            userVipService.setExpireTime(expireTime);
        } else {
            //会员用户,生效时间为=会员的到期时间+1 失效时间=会员的到期时间+购买会员套餐月数
            Date vipExpireTime = userInfo.getVipExpireTime();  //6.14
            userVipService.setStartTime(DateUtil.offsetDay(vipExpireTime, 1)); //6.15
            userVipService.setExpireTime(DateUtil.offsetMonth(userVipService.getStartTime(), serviceMonth)); //7.15
        }
        userVipServiceMapper.insert(userVipService);
        //3.4 更新用户会员标识以及更新会员过期时间
        userInfo.setIsVip(1);
        userInfo.setVipExpireTime(userVipService.getExpireTime());
        userInfoMapper.updateById(userInfo);
    }
}

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

package com.atguigu.tingshu.user.pattern.impl;

import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.user.UserPaidAlbum;
import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper;
import com.atguigu.tingshu.user.pattern.DeliveryStrategy;
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: 2025-06-16 09:31
 */
@Slf4j
@Component(SystemConstant.ORDER_ITEM_TYPE_ALBUM)//默认Bean的ID 为 albumDelivery
public class AlbumDelivery implements DeliveryStrategy {

    @Autowired
    private UserPaidAlbumMapper userPaidAlbumMapper;

    /**
     * 虚拟物品-专辑-发货行为实现
     * @param userPaidRecordVo
     */
    @Override
    public void delivery(UserPaidRecordVo userPaidRecordVo) {
        //1.1 根据订单编号查询已购专辑记录,如果存在,则返回
        Long count = userPaidAlbumMapper.selectCount(
                new LambdaQueryWrapper<UserPaidAlbum>()
                        .eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo())
        );
        if (count > 0) {
            log.info("已存在该订单:{}的已购专辑记录", userPaidRecordVo.getOrderNo());
            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.pattern.impl;

import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.user.UserPaidTrack;
import com.atguigu.tingshu.user.mapper.UserPaidTrackMapper;
import com.atguigu.tingshu.user.pattern.DeliveryStrategy;
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: 2025-06-16 09:31
 */
@Slf4j
@Component(SystemConstant.ORDER_ITEM_TYPE_TRACK)//默认Bean的ID 为 albumDelivery
public class TrackDelivery implements DeliveryStrategy {

    @Autowired
    private UserPaidTrackMapper userPaidTrackMapper;

    @Autowired
    private AlbumFeignClient albumFeignClient;

    /**
     * 虚拟物品-声音-发货行为实现
     *
     * @param userPaidRecordVo
     */
    @Override
    public void delivery(UserPaidRecordVo userPaidRecordVo) {
        //2. 处理虚拟物品"声音"发货逻辑
        //2.1 根据订单编号查询已购声音记录,如果存在,则返回
        Long count = userPaidTrackMapper.selectCount(
                new LambdaQueryWrapper<UserPaidTrack>()
                        .eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo())
        );
        if (count > 0) {
            log.info("已存在该订单:{}的已购声音记录", userPaidRecordVo.getOrderNo());
            return;
        }
        //2.2 如果不存在,则构建声音已购记录完成保存
        TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
        List<Long> trackIdList = userPaidRecordVo.getItemIdList();
        for (Long trackId : trackIdList) {
            UserPaidTrack userPaidTrack = new UserPaidTrack();
            userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
            userPaidTrack.setUserId(userPaidRecordVo.getUserId());
            userPaidTrack.setAlbumId(trackInfo.getAlbumId());
            userPaidTrack.setTrackId(trackId);
            userPaidTrackMapper.insert(userPaidTrack);
        }
    }
}

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

package com.atguigu.tingshu.user.pattern.impl;

import com.atguigu.tingshu.album.AlbumFeignClient;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.album.TrackInfo;
import com.atguigu.tingshu.model.user.UserPaidTrack;
import com.atguigu.tingshu.user.mapper.UserPaidTrackMapper;
import com.atguigu.tingshu.user.pattern.DeliveryStrategy;
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: 2025-06-16 09:31
 */
@Slf4j
@Component(SystemConstant.ORDER_ITEM_TYPE_TRACK)//默认Bean的ID 为 albumDelivery
public class TrackDelivery implements DeliveryStrategy {

    @Autowired
    private UserPaidTrackMapper userPaidTrackMapper;

    @Autowired
    private AlbumFeignClient albumFeignClient;

    /**
     * 虚拟物品-声音-发货行为实现
     *
     * @param userPaidRecordVo
     */
    @Override
    public void delivery(UserPaidRecordVo userPaidRecordVo) {
        //2. 处理虚拟物品"声音"发货逻辑
        //2.1 根据订单编号查询已购声音记录,如果存在,则返回
        Long count = userPaidTrackMapper.selectCount(
                new LambdaQueryWrapper<UserPaidTrack>()
                        .eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo())
        );
        if (count > 0) {
            log.info("已存在该订单:{}的已购声音记录", userPaidRecordVo.getOrderNo());
            return;
        }
        //2.2 如果不存在,则构建声音已购记录完成保存
        TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
        List<Long> trackIdList = userPaidRecordVo.getItemIdList();
        for (Long trackId : trackIdList) {
            UserPaidTrack userPaidTrack = new UserPaidTrack();
            userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
            userPaidTrack.setUserId(userPaidRecordVo.getUserId());
            userPaidTrack.setAlbumId(trackInfo.getAlbumId());
            userPaidTrack.setTrackId(trackId);
            userPaidTrackMapper.insert(userPaidTrack);
        }
    }
}

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

@Autowired
private DeliveryStrategyFactory deliveryStrategyFactory;

/**
 * 用户支付成功后,虚拟物品发货
 *
 * @param userPaidRecordVo
 * @return
 */
@Override
public void savePaidRecord(UserPaidRecordVo userPaidRecordVo) {
    //TODO 策略模式+工厂模式优化
    //1. 根据商品类型从工厂中返回具体策略实现类对象
    DeliveryStrategy strategy = deliveryStrategyFactory.getStrategy(userPaidRecordVo.getItemType());
    //2. 调用策略实现类对象进行虚拟物品发货逻辑
    strategy.delivery(userPaidRecordVo);
}

2.3 提交订单

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 @Validated OrderInfoVo orderInfoVo) {
    //1.获取当前登录用户ID
    Long userId = AuthContextHolder.getUserId();
    //2.调用业务逻辑提交订单
    Map<String, String> map = orderInfoService.submitOrder(userId, orderInfoVo);
    return Result.ok(map);
}

2.4.2 接口与实现

/**
 * 提交订单
 * @param userId 用户ID
 * @param orderInfoVo 订单vo信息
 * @return {orderNo:"订单编号"}
 */
Map<String, String> submitOrder(Long userId, OrderInfoVo orderInfoVo);

/**
 * 保存订单相关信息
 * @param userId
 * @param orderInfoVo
 * @return
 */
OrderInfo saveOrderInfo(Long userId, OrderInfoVo orderInfoVo);
/**
 * 提交订单,支持付款方式(微信、余额)
 *
 * @param orderInfoVo 订单vo
 * @return {orderNo:"订单唯一编号"}
 */
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Map<String, String> submitOrder(OrderInfoVo orderInfoVo) {
    Long userId = AuthContextHolder.getUserId();
    //1.业务校验1,验证流水号,防止订单重复提交 采用Lua脚本判断删除原子性
    String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
    //KEYS[1]=流水号Key ARGV[1]=用户提交流水号值
    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<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
    Boolean flag = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(tradeKey), orderInfoVo.getTradeNo());
    if (!flag) {
        throw new GuiguException(500, "验证流水号失败");
    }

    //2.业务校验2,验证签名,防止订单确认页中订单数据(商品、金额、数量)被篡改
    //2.1 将参与签名订单VO转为Map,TODO 加签时付款方式属性payWay未参与签名
    Map<String, Object> orderInfoMap = BeanUtil.beanToMap(orderInfoVo);
    //2.2 手动删除掉签名属性payWay
    orderInfoMap.remove("payWay");
    //2.3 验签
    SignHelper.checkSign(orderInfoMap);

    //TODO 后续核心业务处理
    //3. 保存订单,返回订单对象 此时订单支付状态:未支付
    OrderInfo orderInfo = this.saveOrderInfo(orderInfoVo, userId);

    //支付方式:1101-微信 1102-支付宝 1103-账户余额
    String payWay = orderInfoVo.getPayWay();

    //4.TODO 处理付款方式为余额
    if (SystemConstant.ORDER_PAY_ACCOUNT.equals(payWay)) {
        //4.1 远程调用"账户服务",检查扣减余额
        //4.1.1 构建扣减账户余额VO对象AccountDeductVo
        AccountDeductVo accountDeductVo = new AccountDeductVo();
        accountDeductVo.setOrderNo(orderInfo.getOrderNo());
        accountDeductVo.setUserId(orderInfo.getUserId());
        accountDeductVo.setAmount(orderInfo.getOrderAmount());
        accountDeductVo.setContent(orderInfo.getOrderTitle());

        //4.1.2 远程调用
        Result result = accountFeignClient.checkAndDeduct(accountDeductVo);
        //4.1.3 判断远程调用响应业务状态码,如果不是200抛出异常,触发全局事务回滚
        if (result.getCode().intValue() != 200) {
            //4.2 扣减余额失败,业务终止,回滚全局事务
            throw new GuiguException(result.getCode(), result.getMessage());
        }
        //4.3 扣减余额成功,订单状态改为已支付
        orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
        orderInfoMapper.updateById(orderInfo);

        //4.4 远程调用"用户服务",虚拟物品发货(增加购买记录)
        //4.4.1 构建虚拟物品发货VO对象UserPaidRecordVo
        UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
        userPaidRecordVo.setOrderNo(orderInfo.getOrderNo());
        userPaidRecordVo.setUserId(orderInfo.getUserId());
        userPaidRecordVo.setItemType(orderInfo.getItemType());
        List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
        if (CollUtil.isNotEmpty(orderDetailVoList)) {
            List<Long> itemIdList = orderDetailVoList
                    .stream()
                    .map(OrderDetailVo::getItemId)
                    .collect(Collectors.toList());
            userPaidRecordVo.setItemIdList(itemIdList);
            //4.4.2 远程调用
            result = userFeignClient.savePaidRecord(userPaidRecordVo);
            if (result.getCode().intValue() != 200) {
                //4.2 虚拟物品发货失败,业务终止,回滚全局事务
                throw new GuiguException(result.getCode(), result.getMessage());
            }
        }
    }
    //5.处理付款方式为微信
    if (SystemConstant.ORDER_PAY_WAY_WEIXIN.equals(payWay)) {
        //5.1 发送延迟关单消息,自动关闭超时未支付订单
        int delayTime = 60 * 15;
        rabbitService.sendDelayMessage(MqConst.EXCHANGE_CANCEL_ORDER, MqConst.ROUTING_CANCEL_ORDER, orderInfo.getId(), delayTime);
    }

    //x.封装响应结果 如果是微信支付,根据响应订单编号对接微信支付;如果是余额支付成功,返回订单编号,查询订单详情
    Map<String, String> map = new HashMap<>();
    map.put("orderNo", orderInfo.getOrderNo());
    return map;
}

2.4.3 保存订单方法

业务处理:

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

OrderInfoService接口

/**
 * 保存订单相关信息
 * @param userId
 * @param orderInfoVo
 * @return
 */
OrderInfo saveOrderInfo(Long userId, OrderInfoVo orderInfoVo);

OrderInfoServiceImpl实现类

@Autowired
private OrderDetailMapper orderDetailMapper;

@Autowired
private OrderDerateMapper orderDerateMapper;

/**
 * 保存订单相关信息(订单、订单明细、优惠明细)
 *
 * @param orderInfoVo
 * @param userId
 * @return
 */
@Override
public OrderInfo saveOrderInfo(OrderInfoVo orderInfoVo, Long userId) {
    //1.封装订单对象,保存订单
    OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class);
    //1.1 设置用户ID
    orderInfo.setUserId(userId);
    //1.2 设置订单标题
    List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
    if (CollUtil.isNotEmpty(orderDetailVoList)) {
        OrderDetailVo orderDetailVo = orderDetailVoList.get(0);
        orderInfo.setOrderTitle(orderDetailVo.getItemName());
    }
    //1.3 设置订单编号 确保全局唯一且趋势递增 生成策略=当日+雪花算法
    String orderNo = DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId();
    orderInfo.setOrderNo(orderNo);
    //1.4 设置订单状态 订单状态:0901-未支付 0902-已支付 0903-已取消
    orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
    //1.5 保存订单
    orderInfoMapper.insert(orderInfo);
    Long orderId = orderInfo.getId();

    //2.封装订单明细对象,保存订单明细
    if (CollUtil.isNotEmpty(orderDetailVoList)) {
        List<OrderDetail> orderDetailList = orderDetailVoList
                .stream()
                .map(orderDetailVo -> {
                    OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class);
                    //将订单明细关联订单ID
                    orderDetail.setOrderId(orderId);
                    return orderDetail;
                }).collect(Collectors.toList());
        orderDetailService.saveBatch(orderDetailList);
    }
    //3.封装订单优惠对象,保存订单优惠明细
    List<OrderDerateVo> orderDerateVoList = orderInfoVo.getOrderDerateVoList();
    if (CollUtil.isNotEmpty(orderDerateVoList)) {
        orderDerateVoList.stream().forEach(orderDerateVo -> {
            OrderDerate orderDerate = BeanUtil.copyProperties(orderDerateVo, OrderDerate.class);
            orderDerate.setOrderId(orderId);
            orderDerateMapper.insert(orderDerate);
        });
    }
    return orderInfo;
}

3、我的订单

3.1 订单明细信息

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

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

订单-我的订单

service-order 微服务OrderInfoApiController中添加

/**
 * 根据订单编号查询订单信息(包含明细) 考虑到在支付成功后收到微信异步回调需要获取订单信息固该接口不需要登录访问
 *
 * @param orderNo
 * @return
 */ 
@Operation(summary = "根据订单编号查询订单信息")
@GetMapping("/orderInfo/getOrderInfo/{orderNo}")
public Result<OrderInfo> getOrderInfo(@PathVariable String orderNo) {
    OrderInfo orderInfo = orderInfoService.getOrderInfo(orderNo);
    return Result.ok(orderInfo);
}

OrderInfoService接口

/**
 * 根据订单编号查询订单信息
 * @param orderNo
 * @return
 */
OrderInfo getOrderInfo(String orderNo);

OrderInfoServiceImpl实现

/**
 * 根据订单编号查询订单信息
 *
 * @param orderNo
 * @return
 */
@Override
public OrderInfo getOrderInfo(String orderNo) {
    //1.根据订单编号查询订单信息
    LambdaUpdateWrapper<OrderInfo> orderInfoLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
    orderInfoLambdaUpdateWrapper.eq(OrderInfo::getOrderNo, orderNo);
    OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoLambdaUpdateWrapper);
    if (orderInfo != null) {

        //2.根据订单ID查询订单明细列表
        LambdaUpdateWrapper<OrderDetail> orderDetailLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        orderDetailLambdaUpdateWrapper.eq(OrderDetail::getOrderId, orderInfo.getId());
        List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailLambdaUpdateWrapper);
        if (CollectionUtil.isNotEmpty(orderDetailList)) {
            orderInfo.setOrderDetailList(orderDetailList);
        }
        //3.根据订单ID查询订单优惠列表
        LambdaUpdateWrapper<OrderDerate> orderDerateLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        orderDerateLambdaUpdateWrapper.eq(OrderDerate::getOrderId, orderInfo.getId());
        List<OrderDerate> orderDerateList = orderDerateMapper.selectList(orderDerateLambdaUpdateWrapper);
        if (CollectionUtil.isNotEmpty(orderDerateList)) {
            orderInfo.setOrderDerateList(orderDerateList);
        }
    }
    return orderInfo;
}

需要展示订单支付状态、支付方式,固在OrderInfo中增加对应属性get方法即可保证响应JSON中有对应属性

public String getOrderStatusName() {
   if("0901".equals(orderStatus)){
      return "未支付";
   } else if ("0902".equals(orderStatus)) {
      return "已支付";
   } else if ("0903".equals(orderStatus)) {
      return "已取消";
   }
   return null;
}

public String getPayWayName() {
	if ("1101".equals(payWay)) {
		return "微信";
	} else if ("1102".equals(payWay)) {
		return "支付宝";
	} else if ("1103".equals(payWay)) {
		return "余额";
	}
	return "";
}

3.2 我的订单列表

image-20231102121612056

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

service-orderOrderInfoApiController 控制器 添加

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

OrderInfoService接口


/**
 * 查询当前用户订单列表
 *
 * @param userId
 * @param pageInfo
 * @return
 */
Page<OrderInfo> getOrderByPage(Long userId, Page<OrderInfo> pageInfo);

OrderInfoServiceImpl实现

/**
 * 查询当前用户订单列表
 *
 * @param userId
 * @param pageInfo
 * @return
 */
@Override
public Page<OrderInfo> getOrderByPage(Long userId, Page<OrderInfo> pageInfo) {
    //1.调用持久层动态SQL查询订单及订单明细
    pageInfo = orderInfoMapper.getOrderByPage(pageInfo, userId);
    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> getOrderByPage(Page<OrderInfo> pageInfo, @Param("userId") Long userId);
}

OrderInfoMapper.xml 映射文件

<resultMap id="orderInfoResultMap" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
    <id property="id" column="id"></id>
    <!--订单明细列表 select指定查询ID(查询订单明细)-->
    <collection property="orderDetailList" column="id" select="getOrderDetailById"></collection>
</resultMap>

<!--根据订单ID查询订单明细列表-->
<select id="getOrderDetailById" resultType="com.atguigu.tingshu.model.order.OrderDetail">
    select id,item_id,item_name,item_price,item_url from order_detail where order_id = #{id}
</select>

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

4、订单延迟关单

4.1 什么是延迟任务

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

image-20210513145942962

应用场景:

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

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

技术选择:

4.2 延迟关闭订单

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

//获取Nacos中配置的订单超时时间 
//注意:热部署更新配置需要在类上使用@RefreshScope注解
@Value("${order.cancel}")
private int ttl;


/**
 * 提交订单
 *
 * @param userId      用户ID
 * @param orderInfoVo 订单vo信息
 * @return {orderNo:"订单编号"}
 */
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public Map<String, String> 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<String, String> map = new HashMap<>();
    map.put("orderNo", orderInfo.getOrderNo());
    return map;
}

配置延迟消息:

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<String, Object> args = new HashMap<String, Object>();
        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();
    }

}

监听延迟消息:

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接口:

/**
 * 查询并且根据状态取消订单
 * @param orderId
 */
void cancelOrder(Long orderId);

OrderInfoServiceImpl实现类:

/**
 * 查询并且根据状态取消订单
 *
 * @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);
    }
}