第7章 订单.md 74 KB

*谷粒随享**

第7章 订单

学习目标:

1、订单购买

购买包含分为:

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

1.1 获取账户余额

初始化账户

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

显示账户余额

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

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() {
        //	调用服务层方法
        BigDecimal availableAmount = userAccountService.getAvailableAmount(AuthContextHolder.getUserId());
        return Result.ok(availableAmount);
    }
}

UserAccountService接口:

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

 /**
  * 根据用户Id 获取到可用余额对象
  * @param userId
  * @return
  */
 UserAccount getUserAccountByUserId(Long userId);

UserAccountServiceImpl实现类:

@Override
public BigDecimal getAvailableAmount(Long userId) {
    //	根据用户Id 获取到用户余额对象
    UserAccount userAccount = this.getUserAccountByUserId(userId);
    return userAccount.getAvailableAmount();
}

@Override
public UserAccount getUserAccountByUserId(Long userId) {
    LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getUserId, userId);
    return this.getOne(queryWrapper);
}

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

1.2 获取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;


	@Operation(summary = "获取全部VIP 服务配置信息")
	@GetMapping("/vipServiceConfig/findAll")
	public Result<List<VipServiceConfig>> findAll(){
		//	调用服务层方法
		List<VipServiceConfig> list = this.vipServiceConfigService.list();
		return Result.ok(list);
	}
}

1.3 购买确定订单远程调用方法

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.1 判断用户是否购买过专辑

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) {
   // 获取到用户Id
   Long userId = AuthContextHolder.getUserId();
   // 调用服务层方法
   Boolean flag = userInfoService.isPaidAlbum(userId, albumId);
   return Result.ok(flag);
}

UserInfoService接口

/**
 * 根据用户Id 与专辑Id 查询结果
 * @param userId
 * @param albumId
 * @return
 */
Boolean isPaidAlbum(Long userId, Long albumId);

UserInfoServiceImpl实现类

/**
 * 查询用户是否购买专辑
 * @param userId
 * @param albumId
 * @return
 */
@Override
public Boolean isPaidAlbum(Long userId, Long albumId) {
    LambdaQueryWrapper<UserPaidAlbum> queryWrapper = new LambdaQueryWrapper<UserPaidAlbum>()
            .eq(UserPaidAlbum::getUserId, userId)
            .eq(UserPaidAlbum::getAlbumId, albumId);
    // 根据用户Id 与专辑Id 查询是否有记录
    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);

熔断类:

@Override
public Result<Boolean> isPaidAlbum(Long albumId) {
    return null;
}

1.3.2 根据Id 获取vip 配置信息

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

VipServiceConfigApiController控制器中添加

/**
 * 根据id获取VIP服务配置信息
 * @param id
 * @return
 */
@Operation(summary = "根据id获取VIP服务配置信息")
@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id) {
	return Result.ok(vipServiceConfigService.getById(id));
}

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) {
    return null;
}

1.4 会员与专辑结算

  • 购买VIP入口:

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

  • 购买声音入口:专辑表的price_type 价格类型 0201 单独购买声音

订单流程图如下:

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

访问订单控制器时,将提交的数据封装到当前实体对象中 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;
}

1.4.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.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({"unchecked", "rawtypes"})
public class OrderInfoApiController {

	@Autowired
	private OrderInfoService orderInfoService;

	/**
	 * 确认订单
	 * @param tradeVo
	 * @return
	 */
	@GuiGuLogin
	@Operation(summary = "确认订单")
	@PostMapping("/orderInfo/trade")
	public Result<OrderInfoVo> trade(@RequestBody @Validated TradeVo tradeVo) {
		// 获取到用户Id
		Long userId = AuthContextHolder.getUserId();
		//	调用服务层方法
		OrderInfoVo orderInfoVo = orderInfoService.trade(tradeVo, userId);
		//	返回数据
		return Result.ok(orderInfoVo);
	}
}

1.4.2 接口

public interface OrderInfoService extends IService<OrderInfo> {
    /**
     * 确定订单
     * @param tradeVo
     * @param userId
     * @return
     */
    OrderInfoVo trade(TradeVo tradeVo, Long userId);
}

1.4.3 实现类

思路:

  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 对象

    @Override
    public OrderInfoVo trade(TradeVo tradeVo, Long userId) {
    //获取用户信息
    Result<UserInfoVo> userInfoVoResult = userInfoFeignClient.getUserInfoVo(userId);
    Assert.notNull(userInfoVoResult);
    UserInfoVo userInfoVo = userInfoVoResult.getData();
    Assert.notNull(userInfoVo);
    //  订单原始金额
    BigDecimal originalAmount = new BigDecimal("0.00");
    //  减免总金额
    BigDecimal derateAmount = new BigDecimal("0.00");
    //  订单总价
    BigDecimal orderAmount = new BigDecimal("0.00");
    //  订单明细集合
    List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
    //  订单减免明细列表
    List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
    //  1001 专辑
    if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_ALBUM)){
    //  判断用户是否购买过专辑
    Result<Boolean> isPaidAlbumResult = this.userInfoFeignClient.isPaidAlbum(tradeVo.getItemId());
    Assert.notNull(isPaidAlbumResult);
    Boolean isPaidAlbum = isPaidAlbumResult.getData();
    Assert.notNull(isPaidAlbum);
    if (isPaidAlbum){
      throw new GuiguException(ResultCodeEnum.REPEAT_BUY_ERROR);
    }
    //  根据专辑Id 获取到专辑数据
    Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(tradeVo.getItemId());
    Assert.notNull(albumInfoResult,"返回专辑结果集不能为空");
    AlbumInfo albumInfo = albumInfoResult.getData();
    Assert.notNull(albumInfo,"专辑对象不能为空");
    //  判断当前用户是否是vip
    if (userInfoVo.getIsVip().intValue()==0){
      //  非VIP 用户
      originalAmount = albumInfo.getPrice();
      //  判断是否打折 , 不等于-1 就是打折
      if (albumInfo.getDiscount().intValue() != -1){
        //  打折 100 8  100*0.8
        derateAmount = originalAmount.multiply(new BigDecimal("10").subtract(albumInfo.getDiscount())).divide(new BigDecimal(10),2, RoundingMode.HALF_UP);
      }
      //  订单总价
      orderAmount = originalAmount.subtract(derateAmount);
    } else {
      // VIP会员
      originalAmount = albumInfo.getPrice();
      //discount=-1,不打折,折扣如:8折 9.5折
      if (albumInfo.getVipDiscount().intValue() != -1) {
        derateAmount = albumInfo.getPrice().multiply(new BigDecimal(10).subtract(albumInfo.getVipDiscount())).divide(new BigDecimal(10), 2, RoundingMode.HALF_UP);
      }
      //  订单总价
      orderAmount = originalAmount.subtract(derateAmount);
    }                              
    //  订单明细
    OrderDetailVo orderDetailVo = new OrderDetailVo();
    orderDetailVo.setItemId(tradeVo.getItemId());
    orderDetailVo.setItemName(albumInfo.getAlbumTitle());
    orderDetailVo.setItemUrl(albumInfo.getCoverUrl());
    orderDetailVo.setItemPrice(albumInfo.getPrice());
    orderDetailVoList.add(orderDetailVo);
    
    //  添加订单减免
    if (originalAmount.subtract(orderAmount).doubleValue() != 0) {
      OrderDerateVo orderDerateVo = new OrderDerateVo();
      orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_ALBUM_DISCOUNT);
      orderDerateVo.setDerateAmount(originalAmount.subtract(orderAmount));
      orderDerateVoList.add(orderDerateVo);
    }
    } else if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_VIP)){
    //  根据id 获取VIP 服务配置信息
    Result<VipServiceConfig> vipServiceConfigResult = vipServiceConfigFeignClient.getVipServiceConfig(tradeVo.getItemId());
    Assert.notNull(vipServiceConfigResult,"返回vip配置结果集不能为空");
    VipServiceConfig vipServiceConfig = vipServiceConfigResult.getData();
    Assert.notNull(vipServiceConfig,"返回vip配置对象不能为空");
    
    originalAmount = vipServiceConfig.getPrice();
    derateAmount = vipServiceConfig.getPrice().subtract(vipServiceConfig.getDiscountPrice());
    orderAmount = originalAmount.subtract(derateAmount);
    
    //订单明细
    OrderDetailVo orderDetailVo = new OrderDetailVo();
    orderDetailVo.setItemId(tradeVo.getItemId());
    orderDetailVo.setItemName("VIP会员"+vipServiceConfig.getName());
    orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl());
    orderDetailVo.setItemPrice(vipServiceConfig.getDiscountPrice());
    orderDetailVoList.add(orderDetailVo);
    
    //添加订单减免
    if (originalAmount.subtract(orderAmount).doubleValue() != 0) {
      OrderDerateVo orderDerateVo = new OrderDerateVo();
      orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
      orderDerateVo.setDerateAmount(originalAmount.subtract(orderAmount));
      orderDerateVoList.add(orderDerateVo);
    }
    }
    // 防重:生成一个唯一标识,保存到redis中一份
    String tradeNoKey = "user:trade:" + userId;
    // 定义一个流水号
    String tradeNo = UUID.randomUUID().toString().replace("-", "");
    redisTemplate.opsForValue().set(tradeNoKey, tradeNo);
    
    //构造结果
    OrderInfoVo orderInfoVo = new OrderInfoVo();
    orderInfoVo.setItemType(tradeVo.getItemType());
    orderInfoVo.setOriginalAmount(originalAmount);
    orderInfoVo.setDerateAmount(derateAmount);
    orderInfoVo.setOrderAmount(orderAmount);
    orderInfoVo.setTradeNo(tradeNo);
    orderInfoVo.setOrderDetailVoList(orderDetailVoList);
    orderInfoVo.setOrderDerateVoList(orderDerateVoList);
    orderInfoVo.setTimestamp(SignHelper.getTimestamp());
    //  支付方式默认值 目的是防止用户在前端篡改金额数据
    orderInfoVo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN);
    //  生成签名
    Map<String, Object> parameterMap = JSON.parseObject(JSON.toJSONString(orderInfoVo), Map.class);
    String sign = SignHelper.getSign(parameterMap);
    orderInfoVo.setSign(sign);
    //  返回对象
    return orderInfoVo;
    }
    

1.5 获取用户声音分集购买支付列表

根据专辑Id 获取到用户已支付声音Id列表

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

service-user模块

UserInfoApiController控制器

/**
 * 根据专辑Id 获取到用户已支付声音Id列表
 * @param albumId
 * @return
 */
@GuiGuLogin
@Operation(summary = "根据专辑id获取用户支付过的声音id列表")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackList(@PathVariable Long albumId){
   // 获取用户Id
   Long userId = AuthContextHolder.getUserId();
   // 根据用户Id 与 专辑Id 获取到已购买的声音Id 集合列表
   List<Long> trackIdList = this.userInfoService.getUserPaidTrackList(userId,albumId);
   // 返回声音Id 集合数据
   return Result.ok(trackIdList);
}

UserInfoService接口

/**
 * 根据用户Id 与 专辑Id 获取到已购买的声音Id 集合列表
 * @param userId
 * @param albumId
 * @return
 */
List<Long> getUserPaidTrackList(Long userId, Long albumId);

UserInfoServiceImpl实现类

/**
 * 根据用户Id 与专辑Id 查询已购买的声音ID列表
 *
 * @param userId
 * @param albumId
 * @return
 */
@Override
public List<Long> getUserPaidTrackList(Long userId, Long albumId) {
    LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserPaidTrack::getUserId, userId);
    queryWrapper.eq(UserPaidTrack::getAlbumId, albumId);
    //获取已购买专辑列表
    List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(queryWrapper);
    //获取已购买声音ID列表
    if (CollectionUtil.isNotEmpty(userPaidTrackList)) {
        List<Long> trackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
        return trackIdList;
    }
    return null;
}

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

/**
 * 根据专辑Id 获取到用户已支付声音Id列表
 * @param albumId
 * @return
 */
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackList(@PathVariable Long albumId);

UserDegradeFeignClient熔断类

@Override
public Result<List<Long>> getUserPaidTrackList(Long albumId) {
    return null;
}

声音结算列表

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",0); // 记录购买集数
list.add(map);
/**
 * 获取用户声音分集购买支付列表
 * @param trackId
 * @return
 */
@GuiGuLogin
@Operation(summary = "获取用户声音分集购买支付列表")
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
public Result<List<Map<String, Object>>> getUserTrackPaidList(@PathVariable Long trackId) {
   // 获取购买记录集合
   List<Map<String,Object>> map = trackInfoService.getUserTrackPaidList(trackId);
   return Result.ok(map);
}

TrackInfoService接口

/**
 * 根据声音Id 获取购买列表
 * @param trackId
 * @return
 */
List<Map<String, Object>> getUserTrackPaidList(Long trackId);

TrackInfoServiceImpl实现类

思路:

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

mybatis-plus 中的一些比较符号含义

lt:less than 小于 le:less than or equal to 小于等于 eq:equal to 等于 ne:not equal to 不等于 ge:greater than or equal to 大于等于 gt:greater than 大于

@Override
public List<Map<String, Object>> getUserTrackPaidList(Long trackId) {
    //获取当前购买意向声音对象
    TrackInfo trackInfo = trackInfoMapper.selectById(trackId);

    //获取声音所属专辑对象
    AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId());

    //获取专辑已支付的声音Id集合列表
    Result<List<Long>> userPaidTrackListResult = userFeignClient.getUserPaidTrackList(albumInfo.getId());
    List<Long> userPaidTrackIdList = userPaidTrackListResult.getData();


    //获取当前声音且大于当前声音的全部声音ID
    LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId())
            .gt(TrackInfo::getOrderNum, trackInfo.getOrderNum()).select(TrackInfo::getId);
    List<TrackInfo> trackInfoList = trackInfoMapper.selectList(queryWrapper);
    if (!CollectionUtils.isEmpty(trackInfoList)) {
        List<Long> trackIdAllList = trackInfoList.stream().map(TrackInfo::getId).collect(Collectors.toList());

        //去掉用户已经支付的 剩余的是用户需要进行支付的声音ID
        if (!CollectionUtils.isEmpty(userPaidTrackIdList)) {
            trackIdAllList = trackIdAllList.stream().filter(trackId1 -> {
                return !userPaidTrackIdList.contains(trackId1);
            }).collect(Collectors.toList());
        }
        // 构造声音分集购买数据列表
        List<Map<String, Object>> list = new ArrayList<>();
        // 封装本集:需要付款的集数有
        if (!CollectionUtils.isEmpty(trackIdAllList)) {
            if (trackIdAllList.size() > 0) {
                Map<String, Object> map = new HashMap<>();
                map.put("name", "本专辑");
                map.put("price", albumInfo.getPrice());
                map.put("trackCount", 1);
                list.add(map);
            }
            if (trackIdAllList.size() > 0 && trackIdAllList.size() <= 10) {
                Map<String, Object> map = new HashMap<>();
                int count = trackIdAllList.size();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
                map.put("name", "后" + trackIdAllList.size() + "集");
                map.put("price", price);
                map.put("trackCount", count);
                list.add(map);
            }
            // 19
            if (trackIdAllList.size() > 10) {
                Map<String, Object> map = new HashMap<>();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(10));
                map.put("name", "后10集");
                map.put("price", price);
                map.put("trackCount", 10);
                list.add(map);
            }
            // 后20集
            if (trackIdAllList.size() > 10 && trackIdAllList.size() <= 20) {
                Map<String, Object> map = new HashMap<>();
                int count = trackIdAllList.size();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
                map.put("name", "后" + count + "集");
                map.put("price", price);
                map.put("trackCount", count);
                list.add(map);
            }
            if (trackIdAllList.size() > 20) {
                Map<String, Object> map = new HashMap<>();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(20));
                map.put("name", "后20集");
                map.put("price", price);
                map.put("trackCount", 20);
                list.add(map);
            }

            //后30集
            if (trackIdAllList.size() > 20 && trackIdAllList.size() <= 30) {
                Map<String, Object> map = new HashMap<>();
                int count = trackIdAllList.size();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
                map.put("name", "后" + count + "集");
                map.put("price", price);
                map.put("trackCount", count);
                list.add(map);
            }
            if (trackIdAllList.size() > 30) {
                Map<String, Object> map = new HashMap<>();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(30));
                map.put("name", "后30集");
                map.put("price", price);
                map.put("trackCount", 30);
                list.add(map);
            }

            //后50集
            if (trackIdAllList.size() > 30 && trackIdAllList.size() <= 50) {
                Map<String, Object> map = new HashMap<>();
                int count = trackIdAllList.size();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(count));
                map.put("name", "后" + count + "集");
                map.put("price", price);
                map.put("trackCount", count);
                list.add(map);
            }
            // 最多购买50集;
            if (trackIdAllList.size() > 50) {
                Map<String, Object> map = new HashMap<>();
                BigDecimal price = albumInfo.getPrice().multiply(new BigDecimal(50));
                map.put("name", "后50集");
                map.put("price", price);
                map.put("trackCount", 50);
                list.add(map);
            }
            return list;
        }
    }

    return null;
}

1.6 购买声音订单

获取下单声音列表

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

控制器

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

/**
 * 根据声音ID+声音数量 获取下单付费声音列表
 * @param trackId
 * @param trackCount
 * @return
 */
@Operation(summary = "批量获取下单付费声音列表")
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getPaidTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount) {
	//	调用服务层方法
	List<TrackInfo> trackInfoList = trackInfoService.getPaidTrackInfoList(trackId, trackCount);
	//	返回数据列表
	return Result.ok(trackInfoList);
}

TrackInfoService接口

/**
  * 根据声音ID+声音数量 获取下单付费声音列表
  * @param trackId
  * @param trackCount
  * @return
  */
List<TrackInfo> findPaidTrackInfoList(Long trackId, Integer trackCount);

实现类

思路:

1. 根据声音Id 获取到当前声音对象
2. 获取已支付的声音Id 列表
3. 判断购买声音的声音集数是否大于0
    1. 大于0
        1. 查询当前专辑、并且 大于当前声音顺序号的声音、并且要按照序号进行升序排序,如果有已支付的声音Id,一定要除去已购买的声音Id 列表 并添加到返回的集合列表中
    2. 等于0
        1. 将查询的对象直接添加到的集合列表中
@Override
public List<TrackInfo> getPaidTrackInfoList(Long trackId, Integer trackCount) {
    // 根据声音Id 获取到声音对象
    TrackInfo trackInfo = this.getById(trackId);
    Assert.notNull(trackCount, "声音为空");

    Result<List<Long>> userPaidTrackListResult = userFeignClient.getUserPaidTrackList(trackInfo.getAlbumId());
    Assert.notNull(userPaidTrackListResult);

    List<Long> userPaidTrackList = userPaidTrackListResult.getData();

    if (trackCount > 0) {
        LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId())
                .ge(TrackInfo::getOrderNum, trackInfo.getOrderNum())
                .orderByAsc(TrackInfo::getOrderNum);
        if (!CollectionUtils.isEmpty(userPaidTrackList)) {
            queryWrapper.notIn(TrackInfo::getId, userPaidTrackList);
        }
        queryWrapper.last("limit " + trackCount);
        List<TrackInfo> trackInfoList = this.list(queryWrapper);
        return trackInfoList;
    }
    // 获取已支付的声音id列表
    return Arrays.asList(trackInfo);
}
/**
 * 根据声音ID+声音数量 获取下单付费声音列表
 * @param trackId
 * @param trackCount
 * @return
 */
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getPaidTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount);

熔断类:

@Override
public Result<List<TrackInfo>> getPaidTrackInfoList(Long trackId, Integer trackCount) {
    return null;
}

改造订单确认方法

思路:

  1. 判断当前购买集数如果小于0则直接抛出异常
  2. 根据声音Id,购买集数获取到下单声音列表
  3. 判断用户是否购买过声音

    1. 购买过:抛出异常信息
    2. 未购买过:计算订单总价与订单明细

      @Override
      public OrderInfoVo trade(TradeVo tradeVo, Long userId) {
      //  获取当前用户对象:
      Result<UserInfoVo> userInfoVoResult = userInfoFeignClient.getUserInfoVo(userId);
      UserInfoVo userInfoVo = userInfoVoResult.getData();
      //  计算金额
      //  订单原始金额
      BigDecimal originalAmount = new BigDecimal("0.00");
      BigDecimal derateAmount = new BigDecimal("0.00");
      BigDecimal orderAmount = new BigDecimal("0.00");
      //  订单明细集合
      List<OrderDetailVo> orderDetailVoList = new ArrayList<>();
      //  订单减免明细列表
      List<OrderDerateVo> orderDerateVoList = new ArrayList<>();
      
      //  判断是否购买专辑 1001
      if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_ALBUM)) {
      // 此处代码省略....
            
      //  购买声音
      } else if (tradeVo.getItemType().equals(SystemConstant.ORDER_ITEM_TYPE_TRACK)) {
      //  判断
      if (tradeVo.getTrackCount().intValue() < 0) {
          throw new GuiguException(ResultCodeEnum.ARGUMENT_VALID_ERROR);
      }
      //  获取下单声音列表
      Result<List<TrackInfo>> trackInfoListResult = trackInfoFeignClient.findPaidTrackInfoList(tradeVo.getItemId(), tradeVo.getTrackCount());
      List<TrackInfo> trackInfoList = trackInfoListResult.getData();
      //  判断用户是否购买过专辑声音
      List<Long> trackIdList = trackInfoList.stream().map(TrackInfo::getId).collect(Collectors.toList());
      //  远程调用获取结果
      Result<Boolean> isPaidTrackResult = userInfoFeignClient.isPaidTrack(trackIdList);
      Assert.notNull(isPaidTrackResult);
      Boolean isPaidTrack = isPaidTrackResult.getData();
      Assert.notNull(isPaidTrack);
      if(isPaidTrack) {
          throw new GuiguException(ResultCodeEnum.REPEAT_BUY_ERROR);
      }
      //  购买声音不支持折扣
      Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(trackInfoList.get(0).getAlbumId());
      AlbumInfo albumInfo = albumInfoResult.getData();
      originalAmount = tradeVo.getTrackCount().intValue() > 0 ? albumInfo.getPrice().multiply(new BigDecimal(tradeVo.getTrackCount())) : albumInfo.getPrice();
      //  计算订单总价
      orderAmount = originalAmount;
      
      //  循环遍历声音集合对象赋值订单明细
      orderDetailVoList = trackInfoList.stream().map(trackInfo -> {
          OrderDetailVo orderDetailVo = new OrderDetailVo();
          orderDetailVo.setItemId(trackInfo.getId());
          orderDetailVo.setItemUrl(trackInfo.getCoverUrl());
          orderDetailVo.setItemPrice(albumInfo.getPrice());
          orderDetailVo.setItemName(trackInfo.getTrackTitle());
          return orderDetailVo;
      }).collect(Collectors.toList());
      } else {
      // 此处代码省略....
            
      }
      // 防重:生成一个唯一标识,保存到redis中一份
      String tradeNoKey = "user:trade:" + userId;
      // 定义一个流水号
      String tradeNo = UUID.randomUUID().toString().replace("-", "");
      redisTemplate.opsForValue().set(tradeNoKey, tradeNo);
      
      //构造结果
      OrderInfoVo orderInfoVo = new OrderInfoVo();
      orderInfoVo.setItemType(tradeVo.getItemType());
      orderInfoVo.setOriginalAmount(originalAmount);
      orderInfoVo.setDerateAmount(derateAmount);
      orderInfoVo.setOrderAmount(orderAmount);
      orderInfoVo.setTradeNo(tradeNo);
      orderInfoVo.setOrderDetailVoList(orderDetailVoList);
      orderInfoVo.setOrderDerateVoList(orderDerateVoList);
      orderInfoVo.setTimestamp(SignHelper.getTimestamp());
      // 默认选择微信支付
      //orderInfoVo.setPayWay(SystemConstant.ORDER_PAY_WAY_WEIXIN);
      Map map = JSON.parseObject(JSON.toJSONString(orderInfoVo), Map.class);
      String sign = SignHelper.getSign(map);
      orderInfoVo.setSign(sign);
      return orderInfoVo;
      }
      

2、提交订单

声音购买

专辑购买

VIP 购买

2.1 提交订单业务分析

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

  1. 校验签名
  2. 验证交易号,防止重复提交订单
  3. 下单
    1. 微信支付
      1. 调用保存订单
    2. 余额支付
      1. 锁定账户可用金额 将锁定数据封装到 AccountLockVo 对象中
        1. 检查与锁定账户金额
        2. 检查与锁定通过之后保存订单
        3. 发送消息减账户余额
        4. 返回订单号
      2. 如果出现异常,同时发送消息手动解锁账户余额
  • 分集购买声音只支持余额支付

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

我们将前端页面提交的数据统一封装到实体类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;
}

2.2 检查锁定账户可用金额

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

service-account 微服务控制器UserAccountApiController 控制器

/**
 * 检查锁定账户金额
 * @param accountLockVo
 * @return
 */
@GuiGuLogin
@Operation(summary = "检查与锁定账户金额")
@PostMapping("/userAccount/checkAndLock")
public Result<AccountLockResultVo> checkAndLock(@RequestBody AccountLockVo accountLockVo){
    //	调用服务层方法
    return this.userAccountService.checkAndLock(accountLockVo);
}

UserAccountService接口:

public interface UserAccountService extends IService<UserAccount> {

    /**
     * 检查与锁定账户金额
     * @param accountLockVo
     * @return
     */
    Result<AccountLockResultVo> checkAndLock(AccountLockVo accountLockVo);
}

UserAccountServiceImpl实现类:

  1. 使用redis-setnx防止重复请求
  2. 核对账户可用金额并锁定账户数据(悲观锁);查询返回的是满足要求的账户
  3. 用户没有可用账户或没有足够的金额,则删除锁定key,并返回信息提示
  4. 添加账户明细,写入user_account_detail 表中
  5. 返回锁定对象,并将对象放入缓存中

    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
    * 检查锁定余额;新增账户变动明细;将锁定结果写入Redis
    *
    * @param accountLockVo
    * @return
    */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<AccountLockResultVo> checkAndLock(AccountLockVo accountLockVo) {
    String reCheckLockKey = RedisConstant.ACCOUNT_MUTIPLE_CHECK + accountLockVo.getOrderNo();
    Boolean reCheckLock = redisTemplate.opsForValue().setIfAbsent(reCheckLockKey, accountLockVo.getOrderNo(), 1, TimeUnit.HOURS);
    if (!reCheckLock) {
        //如果 isExist=false; 说明不是第一次执行,则直接将锁定结果对象从Redis查询返回
        String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + accountLockVo.getOrderNo();
        AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
        if (accountLockResultVo != null) {
            return Result.ok(accountLockResultVo);
        } else {
            //如果未产生锁定结果,需要再次提交
            return Result.build(null, ResultCodeEnum.ACCOUNT_LOCK_RESULT_NULL);
        }
    }
    //2.第一次执行账户锁定
    //2.1 核对账户可用金额并锁定账户数据(悲观锁);查询返回的是满足要求的账户
    UserAccount userAccount = userAccountMapper.check(accountLockVo.getUserId(), accountLockVo.getAmount());
    if (userAccount == null) {
        //说明锁定余额不足,删除重复锁定的Key
        redisTemplate.delete(reCheckLockKey);
        return Result.build(null, ResultCodeEnum.ACCOUNT_LESS);
    }
    //2.2 锁定账户金额
    int lock = userAccountMapper.lock(accountLockVo.getUserId(), accountLockVo.getAmount());
    if (lock == 0) {
        //锁定失败
        // 解除去重
        this.redisTemplate.delete(reCheckLockKey);
        return Result.build(null, ResultCodeEnum.ACCOUNT_LOCK_ERROR);
    }
    //2.3 账户检查且锁定成功,新增账号变动明细
    this.saveAccountDetail(accountLockVo.getUserId(), "锁定:" + accountLockVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_LOCK, accountLockVo.getAmount(), "lock:" + accountLockVo.getOrderNo());
    
    //2.4 封装锁定结果对象;将锁定结果对象存入Redis
    AccountLockResultVo accountLockResultVo = new AccountLockResultVo();
    accountLockResultVo.setUserId(accountLockVo.getUserId());
    accountLockResultVo.setAmount(accountLockVo.getAmount());
    accountLockResultVo.setContent(accountLockVo.getContent());
    // 如果账户锁定成功的情况下,需要缓存锁定信息到redis。以方便将来解锁账户金额 或者 减账户金额
    String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + accountLockVo.getOrderNo();
    redisTemplate.opsForValue().set(lockResultKey, accountLockResultVo, 1, TimeUnit.HOURS);
    return Result.ok(accountLockResultVo);
    }
    

UserAccountMapper.java

@Mapper
public interface UserAccountMapper extends BaseMapper<UserAccount> {
    /**
     * 获取可用账户
     * @param userId
     * @param amount
     * @return
     */
    UserAccount check(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
 }

UserAccountMapper.xml

<!--查询可用用户-->
<select id="check" resultMap="userAccountMap">
  select * from user_account where user_id = #{userId} and available_amount >= #{amount} for update
</select>

UserAccountMapper.java

@Mapper
public interface UserAccountMapper extends BaseMapper<UserAccount> {
	/**
     * 锁定账户金额
     * @param userId
     * @param amount
     * @return
     */
    Integer lock(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}

UserAccountMapper.xml

<!--锁定金额-->
  <update id="lock">
		update user_account
		set lock_amount = lock_amount + #{amount}, available_amount = available_amount - #{amount}
where user_id = #{userId}
</update>

账户变动日志

@Autowired
private UserAccountDetailMapper userAccountDetailMapper;

/**
 * 保存账号变动日志记录
 * @param userId 用户ID
 * @param title 变动详情
 * @param tradeType 交易类型
 * @param amount 变动金额
 * @param orderNo 订单编号
 */
@Override
public void saveAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo) {
    //添加账户明细
    UserAccountDetail userAccountDetail = new UserAccountDetail();
    userAccountDetail.setUserId(userId);
    userAccountDetail.setTitle(title);
    userAccountDetail.setTradeType(tradeType);
    userAccountDetail.setAmount(amount);
    userAccountDetail.setOrderNo(orderNo);
    //	添加数据
    userAccountDetailMapper.insert(userAccountDetail);
}

service-account-client 模块中添加 AccountFeignClient远程调用

package com.atguigu.tingshu.account.client;

import com.atguigu.tingshu.account.client.impl.AccountDegradeFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * <p>
 * 产品列表API接口
 * </p>
 *
 * @author qy
 */
@FeignClient(value = "service-account", path = "api/account", fallback = AccountDegradeFeignClient.class)
public interface AccountFeignClient {


    /**
     * 检查锁定账户金额
     * @param accountLockVo
     * @return
     */
    @PostMapping("/userAccount/checkAndLock")
    public Result<AccountLockResultVo> checkAndLock(@RequestBody AccountLockVo accountLockVo);
}

AccountDegradeFeignClient熔断类:

package com.atguigu.tingshu.account.client.impl;


import com.atguigu.tingshu.account.client.AccountFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import org.springframework.stereotype.Component;

@Component
public class AccountDegradeFeignClient implements AccountFeignClient {

    @Override
    public Result<AccountLockResultVo> checkAndLock(AccountLockVo accountLockVo) {
        return null;
    }
}

2.3 提交订单

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

控制器

OrderInfoApiController

/**
 * 提交订单
 * @param orderInfoVo
 * @return
 */
@GuiGuLogin
@Operation(summary = "提交订单")
@PostMapping("/orderInfo/submitOrder")
public Result<Map<String, Object>> submitOrder(@RequestBody @Validated OrderInfoVo orderInfoVo) {
	//  获取到用户Id
	Long userId = AuthContextHolder.getUserId();
	//  调用服务层方法
	Map<String, Object> map = orderInfoService.submitOrder(orderInfoVo, userId);
	//	返回数据
	return Result.ok(map);
}

接口与实现

/**
 * 提交保存订单
 * @param orderInfoVo
 * @param userId
 * @return 
 */
Map<String, Object> submitOrder(OrderInfoVo orderInfoVo, Long userId);
@Autowired
private AccountFeignClient accountFeignClient;

@Autowired
private KafkaService kafkaService;

/**
 * 提交保存订单
 *
 * @param orderInfoVo
 * @param userId
 * @return
 */
@Override
public Map<String, Object> submitOrder(OrderInfoVo orderInfoVo, Long userId) {
    //获取支付方式存入变量,从入参中移除支付方式,避免验签失败
    String payWay = orderInfoVo.getPayWay();
    //1.验证签名
    Map<String, Object> paramsMap = BeanUtil.beanToMap(orderInfoVo);
    paramsMap.remove("payWay");
    SignHelper.checkSign(paramsMap);

    //2.验证流水号,避免订单重复提交
    String tradeNoKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
    String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
    Boolean flag = (Boolean) redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(tradeNoKey), orderInfoVo.getTradeNo());
    if (!flag) {
        throw new GuiguException(ResultCodeEnum.ORDER_SUBMIT_REPEAT);
    }

    //3.保存订单-无论支付方式是哪种
    String orderNo = this.saveOrderInfo(orderInfoVo, userId, payWay);
    //3.判断支付类型-余额              1101-微信 1102-支付宝 1103-账户余额
    if (SystemConstant.ORDER_PAY_ACCOUNT.equals(payWay)) {
        //3.1 保存订单及订单明细
        try {
            //3.2 账户余额检查及余额锁定
            AccountLockVo accountLockVo = new AccountLockVo();
            accountLockVo.setUserId(userId);
            accountLockVo.setOrderNo(orderNo);
            accountLockVo.setAmount(orderInfoVo.getOrderAmount());
            accountLockVo.setContent(orderInfoVo.getOrderDetailVoList().get(0).getItemName());
            Result<AccountLockResultVo> accountLockResultVoResult = accountFeignClient.checkAndLock(accountLockVo);
            if (200 != accountLockResultVoResult.getCode()) {
                //检查锁定余额失败
                throw new GuiguException(accountLockResultVoResult.getCode(), accountLockResultVoResult.getMessage());
            }
            //3.3 支付成功扣减账户金额
            kafkaService.sendMessage(KafkaConstant.QUEUE_ACCOUNT_MINUS, orderNo);

            //3.4 锁定成功-则认为余额扣减成功 修改订单状态为已支付
            this.orderPaySuccess(orderNo);
        } catch (Exception e) {
            //  异常手动解锁账户
            kafkaService.sendMessage(KafkaConstant.QUEUE_ACCOUNT_UNLOCK, orderNo);
            //抛出异常
            throw new GuiguException(ResultCodeEnum.DATA_ERROR);
        }
    } else {
        //TODO 4.判断支付类型-微信
    }
    Map<String, Object> mapResult = new HashMap<>();
    mapResult.put("orderNo", orderNo);
    return mapResult;
}

保存订单方法

业务逻辑

OrderInfoService接口

/**
 * 保存订单
 * @param orderInfoVo
 * @param userId
 * @param orderNo
 * @param payWay
 */
String saveOrderInfo(OrderInfoVo orderInfoVo, Long userId, String payWay);

OrderInfoServiceImpl实现类

@Autowired
private OrderDetailMapper orderDetailMapper;

@Autowired
private OrderDerateMapper orderDerateMapper;

/**
 * 保存订单
 *
 * @param orderInfoVo
 * @param userId
 * @param payWay
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public String saveOrderInfo(OrderInfoVo orderInfoVo, Long userId, String payWay) {
    //1.拷贝订单VO到订单PO对象
    OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class);
    //2.设置订单相关属性
    //2.1 生成订单唯一编号
    String orderNo = DateUtil.today().replace("-", "") + IdUtil.getSnowflakeNextIdStr();
    orderInfo.setOrderNo(orderNo);
    //2.2 设置订单名称
    List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
    orderInfo.setOrderTitle(orderDetailVoList.get(0).getItemName());
    //2.3 设置用户ID
    orderInfo.setUserId(userId);
    //2.4 设置订单状态 未支付
    orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
    //2.5 设置支付方式
    orderInfo.setPayWay(payWay);
    //2.5 保存订单
    orderInfoMapper.insert(orderInfo);

    //2.保存订单明细
    if (CollectionUtil.isNotEmpty(orderDetailVoList)) {
        orderDetailVoList.forEach(orderDetailVo -> {
            OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class);
            orderDetail.setOrderId(orderInfo.getId());
            orderDetailMapper.insert(orderDetail);
        });
    }

    //3.保存订单减免明细
    List<OrderDerateVo> orderDerateVoList = orderInfoVo.getOrderDerateVoList();
    if (CollectionUtil.isNotEmpty(orderDerateVoList)) {
        orderDerateVoList.forEach(orderDerateVo -> {
            OrderDerate orderDerate = BeanUtil.copyProperties(orderDerateVo, OrderDerate.class);
            orderDerate.setOrderId(orderInfo.getId());
            orderDerateMapper.insert(orderDerate);
        });
    }
    return orderInfo.getOrderNo();
}

@Override
public void orderPaySuccess(String orderNo) {
    //  根据orderNo 修改订单状态数据
    OrderInfo orderInfoUpt = new OrderInfo();
    orderInfoUpt.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
    this.update(orderInfoUpt, new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));

    //  更新用户支付记录
    OrderInfo orderInfo = this.getOrderInfoByOrderNo(orderNo);
    List<Long> itemIdList = orderInfo.getOrderDetailList().stream().map(OrderDetail::getItemId).collect(Collectors.toList());
    UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
    userPaidRecordVo.setOrderNo(orderNo);
    userPaidRecordVo.setUserId(orderInfo.getUserId());
    userPaidRecordVo.setItemType(orderInfo.getItemType());
    userPaidRecordVo.setItemIdList(itemIdList);
    //  发送用户支付成功消息,监听并更新用户支付记录
    kafkaService.sendMessage(KafkaConstant.QUEUE_USER_PAY_RECORD, JSON.toJSONString(userPaidRecordVo));
}

/**
 * 根据订单orderNo 获取订单对象
 * @param orderNo
 * @return
 */
public OrderInfo getOrderInfoByOrderNo(String orderNo) {
    //  获取订单对象
    OrderInfo orderInfo = this.getOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));
    List<OrderDetail> orderDetailList = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderInfo.getId()));

    List<OrderDerate> orderDerateList = orderDerateMapper.selectList(new LambdaQueryWrapper<OrderDerate>().eq(OrderDerate::getOrderId, orderInfo.getId()));
    // 赋值订单明细
    orderInfo.setOrderDetailList(orderDetailList);
    // 赋值减免金额
    orderInfo.setOrderDerateList(orderDerateList);
    orderInfo.setOrderStatusName(getOrderStatusName(orderInfo.getOrderStatus()));
    orderInfo.setPayWayName(getPayWayName(orderInfo.getPayWay()));
    return orderInfo;
}

/**
 * 根据订单状态获取订单状态名称
 * @param orderStatus
 * @return
 */
private String getOrderStatusName(String orderStatus) {
    String orderStatusName = "";
    if(orderStatus.equals(SystemConstant.ORDER_STATUS_UNPAID)) {
        orderStatusName = "待支付";
    } else if (orderStatus.equals(SystemConstant.ORDER_STATUS_PAID)) {
        orderStatusName = "已支付";
    } else {
        orderStatusName = "已取消";
    }
    return orderStatusName;
}

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

2.4 监听新增购买记录

service-user 模块中进行监听

package com.atguigu.tingshu.user.receiver;

import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.user.UserPaidRecordVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class UserReceiver {

    @Autowired
    private UserInfoService userInfoService;

    /**
     * 更新用户支付记录
     *
     * @param record
     */
    @KafkaListener(topics = KafkaConstant.QUEUE_USER_PAY_RECORD)
    public void updateUserPaidRecord(ConsumerRecord<String,String> record) {
        UserPaidRecordVo userPaidRecordVo = JSON.parseObject(record.value(), UserPaidRecordVo.class);
        log.info("更新用户支付记录: {}", JSON.toJSONString(userPaidRecordVo));
        //通知更新用户账号
        userInfoService.updateUserPaidRecord(userPaidRecordVo);
    }
}

UserInfoService 接口中添加

 /**
  * 更新用户支付记录
  * @param userPaidRecordVo
  */
 void updateUserPaidRecord(UserPaidRecordVo userPaidRecordVo);

UserInfoServiceImpl实现类

@Autowired
private UserVipServiceMapper userVipServiceMapper;

@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;

@Autowired
private AlbumFeignClient albumFeignClient;


/**
 * 监听购买成功消息,新增用户购买记录
 *
 * @param userPaidRecordVo
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void processPaidRecord(UserPaidRecordVo userPaidRecordVo) {
    //项目类型 1001-专辑 1002-声音 1003-vip会员
    String itemType = userPaidRecordVo.getItemType();
    //判断购买类型-VIP会员
    if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(itemType)) {
        //1.根据订单编号查询VIP购买记录
        LambdaQueryWrapper<UserVipService> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo());
        Long count = userVipServiceMapper.selectCount(queryWrapper);
        if (count > 0) {
            return;
        }
        //2.获取用户购买VIP的类别信息
        Long vipServiceId = userPaidRecordVo.getItemIdList().get(0);
        VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(vipServiceId);

        UserVipService userVipService = new UserVipService();
        userVipService.setUserId(userPaidRecordVo.getUserId());
        userVipService.setOrderNo(userPaidRecordVo.getOrderNo());

        Date startTime = new Date();
        //获取用户信息
        UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
        if (userInfo.getIsVip().intValue() == 1 && userInfo.getVipExpireTime().after(new Date())) {
            startTime = userInfo.getVipExpireTime();
        }
        DateTime expireTime = DateUtil.offsetMonth(startTime, vipServiceConfig.getServiceMonth());
        userVipService.setExpireTime(expireTime);
        userVipServiceMapper.insert(userVipService);

        //更新用户vip信息
        userInfo.setIsVip(1);
        userInfo.setVipExpireTime(expireTime);
        this.updateById(userInfo);
    } else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(itemType)) {
        //判断购买类型-专辑
        // 防止重复消费,如果有记录则直接停止
        long count = userPaidAlbumMapper.selectCount(new LambdaQueryWrapper<UserPaidAlbum>().eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo()));
        if (count > 0) return;
        // 创建用户支付记录对象
        UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
        userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
        userPaidAlbum.setAlbumId(userPaidRecordVo.getItemIdList().get(0));
        userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
        userPaidAlbumMapper.insert(userPaidAlbum);

    } else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(itemType)) {
        //判断购买类型-声音
        // 防止重复消费
        long count = userPaidTrackMapper.selectCount(new LambdaQueryWrapper<UserPaidTrack>().eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo()));
        if (count > 0) return;
        //远程调用专辑服务获取声音详情
        TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
        //遍历购买项ID集合生成构建记录对象
        userPaidRecordVo.getItemIdList().stream().forEach(itemId->{
            UserPaidTrack userPaidTrack = new UserPaidTrack();
            userPaidTrack.setUserId(userPaidRecordVo.getUserId());
            userPaidTrack.setAlbumId(trackInfo.getAlbumId());
            userPaidTrack.setTrackId(itemId);
            userPaidTrack.setOrderNo(userPaidRecordVo.getOrderNo());
            userPaidTrackMapper.insert(userPaidTrack);
        });
    }
}

service-album-client 模块中AlbumFeignClient添加远程调用

/**
 * 根据Id 获取数据声音信息
 *
 * @param id
 * @return
 */
@GetMapping("/trackInfo/getTrackInfo/{id}")
public Result<TrackInfo> getTrackInfo(@PathVariable Long id);

AlbumDegradeFeignClient熔断类:

@Override
public Result<TrackInfo> getTrackInfo(Long id) {
    return null;
}

2.5 监听账户变更消息

service-account模块中监听完成扣减账户余额或账户余额回滚。

2.5.1 监听扣减金额

/**
 * 扣减锁定金额
 *
 * @param record
 */
@KafkaListener(topics = KafkaConstant.QUEUE_ACCOUNT_MINUS)
public void minus(ConsumerRecord<String, String> record) {
    String orderNo = record.value();
    if (StringUtils.isEmpty(orderNo)) {
        return;
    }

    //扣减锁定金额
    userAccountService.minus(orderNo);
}

实现类:

/**
 * 扣减锁定金额
 *
 * @param orderNo
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void minus(String orderNo) {
    //1.业务去重避免多次扣减
    String reMinusKey = RedisConstant.BUSINESS_PREFIX + "minus:" + orderNo;
    Boolean flag = redisTemplate.opsForValue().setIfAbsent(reMinusKey, orderNo, 1, TimeUnit.HOURS);
    if (flag) {
        //2.构建锁定金额对象的Key 从Redis中获取锁定结果对象
        String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + orderNo;
        AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
        if (accountLockResultVo != null) {
            int minus = userAccountMapper.minus(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
            if(minus == 0) {
                //解除去重
                this.redisTemplate.delete(reMinusKey);
                throw new GuiguException(ResultCodeEnum.ACCOUNT_MINUSLOCK_ERROR);
            }
            //记录日志
            this.saveAccountDetail(accountLockResultVo.getUserId(), accountLockResultVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountLockResultVo.getAmount(), orderNo);
            // 解锁账户金额之后,删除锁定缓存。以防止重复解锁
            this.redisTemplate.delete(lockResultKey);
        }
    }
}

UserAccountMapper

/**
 * 解锁账户
 * @param userId
 * @param amount
 * @return
 */
Integer minus(@Param("userId") Long userId, @Param("amount") BigDecimal amount);

UserAccountMapper.xml

<update id="minus">
   update user_account
   set lock_amount = lock_amount - #{amount}, total_amount = total_amount - #{amount}, total_pay_amount = total_pay_amount + #{amount}
   where user_id = #{userId}
</update>

2.5.2 监听解锁锁定金额

service-account 微服务中的AccountReceiver 添加

/**
 * 解锁锁定金额
 *
 * @param record
 */
@KafkaListener(topics = KafkaConstant.QUEUE_ACCOUNT_UNLOCK)
public void unlock(ConsumerRecord<String, String> record) {
    String orderNo = record.value();
    if (StringUtils.isEmpty(orderNo)) {
        return;
    }
    //  调用解除锁定
    userAccountService.unlock(orderNo);
}

接口:

/**
 * 解除锁定
 * @param orderNo
 */
void unlock(String orderNo);

实现类:

@Override
@Transactional(rollbackFor = Exception.class)
public void unlock(String orderNo) {
    //1.业务去重避免多次扣减
    String reMinusKey = RedisConstant.BUSINESS_PREFIX + "unlock:" + orderNo;
    Boolean flag = redisTemplate.opsForValue().setIfAbsent(reMinusKey, orderNo, 1, TimeUnit.HOURS);
    if (!flag) {
        return;
    }
    String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + orderNo;
    AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
    if (accountLockResultVo == null) {
        return;
    }
    //恢复账户锁定
    int unLock = userAccountMapper.unLock(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
    if(unLock == 0) {
        //解除去重
        this.redisTemplate.delete(reMinusKey);
        throw new GuiguException(ResultCodeEnum.ACCOUNT_UNLOCK_ERROR);
    }

    //记录账户变更明细
    this.saveAccountDetail(accountLockResultVo.getUserId(), "解锁:"+accountLockResultVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_UNLOCK, accountLockResultVo.getAmount(), "unlock:"+orderNo);
    // 解锁账户金额之后,删除锁定缓存。以防止重复解锁
    this.redisTemplate.delete(lockResultKey);
}

Mapper.java

/**
 * 调用解除锁定方法.
 * @param userId
 * @param amount
 * @return
 */
int unLock(@Param("userId") Long userId, @Param("amount") BigDecimal amount);

Mapper.xml

<update id="unLock">
   update user_account
   set lock_amount = lock_amount - #{amount}, available_amount = available_amount + #{amount}
   where user_id = #{userId}
</update>

3、我的订单

订单明细查询

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

service-order 微服务中添加

/**
 * 查看我的订单
 * @param orderNo
 * @return
 */
@GuiGuLogin
@Operation(summary = "根据订单号获取订单信息")
@GetMapping("/orderInfo/getOrderInfo/{orderNo}")
public Result<OrderInfo> getOrderInfo(@PathVariable String orderNo) {
   OrderInfo orderInfo = orderInfoService.getOrderInfoByOrderNo(orderNo);
   return Result.ok(orderInfo);
}

接口与实现

/**
 * 查看我的订单
 * @param orderNo
 * @return
 */
OrderInfo getOrderInfoByOrderNo(String orderNo);
@Override
public OrderInfo getOrderInfoByOrderNo(String orderNo) {
    OrderInfo orderInfo = this.getOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));
    List<OrderDetail> orderDetailList = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderInfo.getId()));
    List<OrderDerate> orderDerateList = orderDerateMapper.selectList(new LambdaQueryWrapper<OrderDerate>().eq(OrderDerate::getOrderId, orderInfo.getId()));

    orderInfo.setOrderDetailList(orderDetailList);
    orderInfo.setOrderDerateList(orderDerateList);
    orderInfo.setOrderStatusName(getOrderStatusName(orderInfo.getOrderStatus()));
    orderInfo.setPayWayName(getPayWayName(orderInfo.getPayWay()));

    return orderInfo;

}

我的订单列表

service-orderOrderInfoApiController 控制器 添加

/**
 * 查看我的订单
 *
 * @param page
 * @param limit
 * @return
 */
@GuiGuLogin
@Operation(summary = "获取用户订单")
@GetMapping("/orderInfo/findUserPage/{page}/{limit}")
public Result getUserOrderByPage(@PathVariable Long page, @PathVariable Long limit) {
    // 获取到用户Id
    Long userId = AuthContextHolder.getUserId();
    Page<OrderInfo> pageInfo = new Page<>(page, limit);
    // 调用服务层方法
    orderInfoService.getUserOrderByPage(pageInfo, userId);
    return Result.ok(pageInfo);
}

接口与实现

/**
 * 查看我的订单
 * @param pageParam
 * @param userId
 * @return
 */
void getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId);
@Override
public IPage<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageParam, Long userId) {
    //  调用mapper 层方法
    IPage<OrderInfo> infoIPage = orderInfoMapper.getUserOrderByPage(pageParam,userId);
    infoIPage.getRecords().forEach(item->{
        //  设置状态名
        item.setOrderStatusName(getOrderStatusName(item.getOrderStatus()));
        item.setPayWayName(getPayWayName(item.getPayWay()));
    });
    return infoIPage;
}

mapper.java 接口

@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {

    /**
     * 查看分页订单列表
     * @param pageParam
     * @param userId
     * @return
     */
    IPage<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageParam, Long userId);
}

mapper.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="orderInfoMap" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
        <id property="id" column="id"></id>
        <collection property="orderDetailList" ofType="com.atguigu.tingshu.model.order.OrderDetail" autoMapping="true">
            <id property="id" column="detail_id"></id>
        </collection>
    </resultMap>

    <!-- 用于select查询公用抽取的列 -->
    <sql id="columns">
            oi.id ,
            oi.user_id,
            oi.order_title,
            oi.order_no,
            oi.order_status,
            oi.original_amount,
            oi.derate_amount,
            oi.order_amount,
            oi.item_type,
            oi.pay_way,
            od.id detail_id,
            od.item_id,
            od.item_name,
            od.item_url,
            od.item_price
    </sql>

    <select id="getUserOrderByPage" resultMap="orderInfoMap">
        select
        <include refid="columns"></include>
        from order_info oi inner join order_detail od on oi.id = od.order_id
        where oi.user_id = #{userId}
        order by oi.id desc
    </select>
</mapper>

4、订单延迟关单

利用redissonClient 发送延迟消息。

/**
 * 发送延迟消息
 */
private void sendDelayMessage(Long orderId) {
    try {
        //  创建一个队列
        RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(KafkaConstant.QUEUE_ORDER_CANCEL);
        //  将队列放入延迟队列中
        RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
        //  发送的内容
        delayedQueue.offer(orderId.toString(), KafkaConstant.DELAY_TIME, TimeUnit.SECONDS);
        log.info("添加延时队列成功 ,延迟时间:{},订单id:{}", KafkaConstant.DELAY_TIME, orderId);
    } catch (Exception e) {
        log.error("添加延时队列失败 ,延迟时间:{},订单id:{}", KafkaConstant.DELAY_TIME, orderId);
        e.printStackTrace();
    }
}

监听消息:

package com.atguigu.tingshu.order.handle;

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.redisson.api.RBlockingDeque;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;


@Slf4j
@Component
public class RedisDelayHandle {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private OrderInfoService orderInfoService;

    @PostConstruct
    public void listener() {
        new Thread(()->{
            while (true){
                RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque(KafkaConstant.QUEUE_ORDER_CANCEL);
                try {
                    String orderId = blockingDeque.take();
                    if(!StringUtils.isEmpty(orderId)) {
                        log.info("接收延时队列成功,订单id:{}", orderId);
                        orderInfoService.orderCancel(Long.parseLong(orderId));
                    }
                } catch (InterruptedException e) {
                    log.error("接收延时队列失败");
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

接口:

public interface OrderInfoService extends IService<OrderInfo> {    
	/**
     * 根据订单Id 取消订单
     * @param orderId
     */
    void orderCancel(Long orderId);
}

实现类:

@Override
public void orderCancel(Long orderId) {
  OrderInfo orderInfoUpt = new OrderInfo();
  orderInfoUpt.setId(orderId);
  orderInfoUpt.setOrderStatus(SystemConstant.ORDER_STATUS_CANCEL);
  this.updateById(orderInfoUpt);
}