谷粒随享
学习目标:
购买包含分为:
在新用户第一次登录的时候,就进行了初始化账户余额信息操作!
当刷新主页的时候,会加载当前余额数据。
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.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
@GetMapping("/userAccount/getAvailableAmount")
public Result<BigDecimal> getAvailableAmount() {
Long userId = AuthContextHolder.getUserId();
BigDecimal availableAmount = userAccountService.getAvailableAmount(userId);
return Result.ok(availableAmount);
}
}
UserAccountService接口:
/**
* 查询账户可用余额
*
* @return
*/
BigDecimal getAvailableAmount(Long userId);
/**
* 查询账户信息
*
* @param userId
* @return
*/
UserAccount getUserAccount(Long userId);
UserAccountServiceImpl实现类:
/**
* 获取账户可用余额
*
* @param userId
* @return
*/
@Override
public BigDecimal getAvailableAmount(Long userId) {
//1.获取账户信息
UserAccount userAccount = this.getUserAccount(userId);
//2.获取可用余额
if (userAccount != null) {
return userAccount.getAvailableAmount();
}
return null;
}
/**
* 获取账户信息
*
* @param userId
* @return
*/
@Override
public UserAccount getUserAccount(Long userId) {
LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserAccount::getUserId, userId);
return userAccountMapper.selectOne(queryWrapper);
}
在首页点击专辑封面标识为: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;
/**
* 获取所有VIP套餐/配置列表
*
* @return
*/
@Operation(summary = "获取所有VIP套餐/配置列表")
@GetMapping("/vipServiceConfig/findAll")
public Result<List<VipServiceConfig>> getVipServiceConfig() {
List<VipServiceConfig> list = vipServiceConfigService.list();
return Result.ok(list);
}
}
在service-util
微服务中添加远程 feign 远程调用拦截器,来获取token 数据。
如上图:因为微服务之间并没有传递头文件,所以我们可以定义一个拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的用户信息再放入到header中,再调用其他微服务即可。
package com.atguigu.tingshu.common.feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
public class FeignInterceptor implements RequestInterceptor {
public void apply(RequestTemplate requestTemplate){
// 获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//异步编排 与 MQ消费者端 为 null 避免出现空指针
if(null != requestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String token = request.getHeader("token");
requestTemplate.header("token", token);
}
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/90
service-user
微服务的UserInfoApiController控制器
/**
* 判断用户是否购买过指定专辑
*
* @param albumId
* @return
*/
@Operation(summary = "判断用户是否购买过指定专辑")
@GuiGuLogin //获取当前登录用户ID
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId) {
Long userId = AuthContextHolder.getUserId();
Boolean isPaid = userInfoService.isPaidAlbum(userId, albumId);
return Result.ok(isPaid);
}
UserInfoService接口
/**
* 判断用户是否购买过指定专辑
* @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<>();
queryWrapper.eq(UserPaidAlbum::getUserId, userId);
queryWrapper.eq(UserPaidAlbum::getAlbumId, albumId);
Long count = userPaidAlbumMapper.selectCount(queryWrapper);
return count > 0;
}
service-user-client
远程调用 UserFeignClient 添加
/**
* 判断用户是否购买过指定专辑
*
* @param albumId
* @return
*/
@GetMapping("/userInfo/isPaidAlbum/{albumId}")
public Result<Boolean> isPaidAlbum(@PathVariable Long albumId);
UserDegradeFeignClient熔断类:
@Override
public Result<Boolean> isPaidAlbum(Long albumId) {
log.error("远程调用[用户服务]isPaidAlbum方法服务降级");
return null;
}
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) {
VipServiceConfig vipServiceConfig = vipServiceConfigService.getById(id);
return Result.ok(vipServiceConfig);
}
在service-user-client
模块UserFeignClient中添加
/**
* 根据套餐ID查询VIP套餐详情
*
* @param id
* @return
*/
@GetMapping("/vipServiceConfig/getVipServiceConfig/{id}")
public Result<VipServiceConfig> getVipServiceConfig(@PathVariable Long id);
UserDegradeFeignClient熔断类:
@Override
public Result<VipServiceConfig> getVipServiceConfig(Long id) {
log.error("远程调用[用户服务]getVipServiceConfig方法服务降级");
return null;
}
订单流程图如下:
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;
}
在service-order
微服务中编写确定订单控制器 ,
package com.atguigu.tingshu.order.api;
import com.atguigu.tingshu.common.login.GuiGuLogin;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.order.service.OrderInfoService;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "订单管理")
@RestController
@RequestMapping("api/order")
@SuppressWarnings({"all"})
public class OrderInfoApiController {
@Autowired
private OrderInfoService orderInfoService;
/**
* 订单结算页面渲染-数据汇总
*
* @return
*/
@Operation(summary = "订单结算页面渲染-数据汇总")
@GuiGuLogin
@PostMapping("/orderInfo/trade")
public Result<OrderInfoVo> trade(@RequestBody TradeVo tradeVo) {
Long userId = AuthContextHolder.getUserId();
OrderInfoVo orderInfoVo = orderInfoService.trade(userId, tradeVo);
return Result.ok(orderInfoVo);
}
}
package com.atguigu.tingshu.order.service;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.atguigu.tingshu.vo.order.TradeVo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface OrderInfoService extends IService<OrderInfo> {
/**
* 订单结算页面渲染-数据汇总
*
* @return
*/
OrderInfoVo trade(Long userId, TradeVo tradeVo);
}
思路:
专辑订单
vip 订单
生成一个流水号存储到缓存,防止用户重复提交订单
给OrderInfoVo 实体类赋值
最后返回OrderInfoVo 对象
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.album.AlbumFeignClient;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.model.album.AlbumInfo;
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.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@SuppressWarnings({"all"})
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Autowired
private OrderInfoMapper orderInfoMapper;
@Autowired
private UserFeignClient userFeignClient;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private AlbumFeignClient albumFeignClient;
/**
* 订单结算页面渲染-数据汇总
* 1.处理购买类型-VIP会员
* 2.处理购买类型-专辑
* 3.处理购买类型-声音
*
* @return
*/
@Override
public OrderInfoVo trade(Long userId, TradeVo tradeVo) {
//1.声明订单结算对象中价格变量,订单明细、优惠信息变量-赋予初始值
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<>();
//2.远程调用用户微服务获取用户信息
UserInfoVo userInfoVo = userFeignClient.getUserInfoVoByUserId(userId).getData();
Assert.notNull(userInfoVo, "用户信息为空,请联系管理员");
//3.处理购买类型-VIP会员
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
//3.1 远程调用“用户服务”获取 根据VIP套餐ID查询套餐信息
VipServiceConfig vipServiceConfig = userFeignClient.getVipServiceConfig(tradeVo.getItemId()).getData();
Assert.notNull(vipServiceConfig, "VIP套餐不存在,请联系管理员");
//3.2 动态计算VIP会员价格:原价、订单价、减免价
originalAmount = vipServiceConfig.getPrice();
orderAmount = vipServiceConfig.getDiscountPrice();
derateAmount = originalAmount.subtract(orderAmount);
//3.3 封装订单明细集合(VIP套餐信息)
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(tradeVo.getItemId());
orderDetailVo.setItemName(vipServiceConfig.getName());
orderDetailVo.setItemUrl(vipServiceConfig.getImageUrl());
//设置原价
orderDetailVo.setItemPrice(originalAmount);
orderDetailVoList.add(orderDetailVo);
//3.4 封装订单优惠集合(VIP优惠信息)
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_VIP_SERVICE_DISCOUNT);
orderDerateVo.setDerateAmount(derateAmount);
orderDerateVo.setRemarks("VIP折扣价:" + derateAmount);
orderDerateVoList.add(orderDerateVo);
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
//4.处理购买类型-专辑
//4.1 远程调用“用户服务”判断用户是否已购买该专辑
Boolean ifBuy = userFeignClient.isPaidAlbum(tradeVo.getItemId()).getData();
if (ifBuy) {
throw new GuiguException(ResultCodeEnum.REPEAT_BUY_ERROR);
}
//4.2 远程调用“专辑服务”获取专辑信息
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(tradeVo.getItemId()).getData();
Assert.notNull(albumInfo, "专辑不存在!");
//4.3 动态计算专辑价格:原价、订单价、减免价
originalAmount = albumInfo.getPrice();
orderAmount = originalAmount;
if (userInfoVo.getIsVip().intValue() == 1 && userInfoVo.getVipExpireTime().after(DateUtil.date())) {
if (albumInfo.getVipDiscount().intValue() != -1) {
//当前用户是VIP用户
orderAmount = originalAmount.multiply(albumInfo.getVipDiscount()).divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
//减免价格=原价-订单价格
derateAmount = originalAmount.subtract(orderAmount);
}
} else {
if (albumInfo.getDiscount().intValue() != -1) {
//普通用户折扣 订单价=原价*8/10
orderAmount = originalAmount.multiply(albumInfo.getDiscount()).divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
//减免价格=原价-订单价格
derateAmount = originalAmount.subtract(orderAmount);
}
}
//4.4 封装订单明细集合(专辑信息)
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setItemId(tradeVo.getItemId());
orderDetailVo.setItemName(albumInfo.getAlbumTitle());
orderDetailVo.setItemUrl(albumInfo.getCoverUrl());
orderDetailVo.setItemPrice(originalAmount);
orderDetailVoList.add(orderDetailVo);
//4.5 封装订单优惠集合(优惠信息)
OrderDerateVo orderDerateVo = new OrderDerateVo();
orderDerateVo.setDerateType(SystemConstant.ORDER_DERATE_ALBUM_DISCOUNT);
orderDerateVo.setDerateAmount(derateAmount);
orderDerateVo.setRemarks("专辑优惠:" + derateAmount);
orderDerateVoList.add(orderDerateVo);
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
//5.TODO 处理购买类型-声音
}
//6.构建渲染订单结算所需对象OrderInfoVo:包含价格、订单明细列表、优惠列表、其他属性
OrderInfoVo orderInfoVo = new OrderInfoVo();
//6.1 针对本次请求产生流水号,订单被重复条件(提交后回退到页面后继续提交) 将流水存入Redis,方便后续提交订单验证流水号
String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
String tradeNo = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES);
orderInfoVo.setTradeNo(tradeNo);
orderInfoVo.setItemType(tradeVo.getItemType());
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//6.2 针对本次请求所有参数进行签名 --->md5(参数)=签名值 防止数据被网络传输过程中被恶意篡改
orderInfoVo.setTimestamp(DateUtil.current());
//将Bean对象转为Map 将对象中空值排除掉:支付类型"payway"
Map<String, Object> map = BeanUtil.beanToMap(orderInfoVo, false, true);
String sign = SignHelper.getSign(map);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/94
service-user
模块
/**
* 根据专辑id+用户ID获取用户已购买声音id列表
*
* @param albumId
* @return
*/
@GuiGuLogin
@Operation(summary = "根据专辑id+用户ID获取用户已购买声音id列表")
@GetMapping("/userInfo/findUserPaidTrackList/{albumId}")
public Result<List<Long>> getUserPaidTrackList(@PathVariable Long albumId) {
Long userId = AuthContextHolder.getUserId();
List<Long> userPaidTrackIdList = userInfoService.getUserPaidTrackList(userId, albumId);
return Result.ok(userPaidTrackIdList);
}
/**
* 根据专辑id+用户ID获取用户已购买声音id列表
*
* @param albumId
* @return
*/
List<Long> getUserPaidTrackList(Long userId, Long albumId);
/**
* 根据专辑id+用户ID获取用户已购买声音id列表
*
* @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);
if (CollectionUtil.isNotEmpty(userPaidTrackList)) {
//获取已购声音ID列表
List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList());
return userPaidTrackIdList;
}
return null;
}
service-user-client
模块中UserFeignClient提供Feign接口
/**
* 根据专辑id+用户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) {
log.error("远程调用[用户服务]getUserPaidTrackList方法服务降级");
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",1); // 记录购买集数
list.add(map);
/**
* 查询当前用户分集购买声音列表
*
* @param trackId
* @return 分集对象包含(名称,价格,数量)
*/
@GuiGuLogin
@GetMapping("/trackInfo/findUserTrackPaidList/{trackId}")
@Operation(summary = "查询当前用户分集购买声音列表")
public Result<List<Map<String, Object>>> getUserTrackWaitPayList(@PathVariable Long trackId) {
Long userId = AuthContextHolder.getUserId();
List<Map<String, Object>> list = trackInfoService.getUserTrackWaitPayList(userId, trackId);
return Result.ok(list);
}
/**
* 查询当前用户分集购买声音列表
*
* @param userId
* @param trackId
* @return 分集对象包含(名称,价格,数量)
*/
List<Map<String, Object>> getUserTrackWaitPayList(Long userId, Long trackId);
思路:
/**
* 查询当前用户分集购买声音列表
*
* @param userId
* @param trackId
* @return 分集对象包含(名称,价格,数量) 用户可以购买声音列表(除了已购买声音外)
*/
@Override
public List<Map<String, Object>> getUserTrackWaitPayList(Long userId, Long trackId) {
//1.根据声音ID查询声音对象(用于获取专辑ID)
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
Assert.notNull(trackInfo, "声音不存在!");
//2.根据专辑ID+当前选中声音序号(序号大于等于当前声音序号声音列表),按照序号正序排-得到“待购”声音集合
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId());
queryWrapper.ge(TrackInfo::getOrderNum, trackInfo.getOrderNum());
queryWrapper.orderByAsc(TrackInfo::getOrderNum);
List<TrackInfo> waitBuyTrackInfoList = trackInfoMapper.selectList(queryWrapper);
//3.远程调用“用户服务”根据专辑ID获取用户已购声音ID列表
List<Long> userPaidTrckIdList = userFeignClient.getUserPaidTrackList(trackInfo.getAlbumId()).getData();
//4.将已购声音ID排除到分集购买以外
if (CollectionUtil.isNotEmpty(userPaidTrckIdList)) {
//用户已买过当前专辑下声音,过滤条件:待购声音对象ID没有出现在已购声音ID集合中得到未买声音
waitBuyTrackInfoList = waitBuyTrackInfoList.stream()
.filter(trackInfo1 -> !userPaidTrckIdList.contains(trackInfo1.getId())).collect(Collectors.toList());
}
//5.对预购的声音ID集合处理,动态展示“分集购买对象” 例如:本集、 后10集、后18集
List<Map<String, Object>> list = new ArrayList<>();
//获取待购声音数量
int count = waitBuyTrackInfoList.size();
// 获取声音单价
AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId());
BigDecimal price = albumInfo.getPrice();
//5.1 分集购买对象-本集 必然显示
Map<String, Object> currMap = new HashMap<>();
currMap.put("name", "本集");
currMap.put("price", price);
currMap.put("trackCount", 1);
list.add(currMap);
//5.2 待购声音数量<=10 则动态显示 后*集 采用循环优化
//if (count <= 10) {
// Map<String, Object> map = new HashMap<>();
// map.put("name", "后" + count + "集");
// map.put("price", price.multiply(BigDecimal.valueOf(count)));
// map.put("trackCount", count);
// list.add(map);
//}
////5.3 待购声音数量>10 则固定显示 后10集
//if (count > 10) {
// Map<String, Object> map = new HashMap<>();
// map.put("name", "后10集");
// map.put("price", price.multiply(BigDecimal.valueOf(10)));
// map.put("trackCount", 10);
// list.add(map);
//}
//
////5.3 待购声音数量>10 且 <=20 则动态显示 后*集
//if (count > 10 && count <= 20) {
// Map<String, Object> map = new HashMap<>();
// map.put("name", "后" + count + "集");
// map.put("price", price.multiply(BigDecimal.valueOf(count)));
// map.put("trackCount", count);
// list.add(map);
//}
////5.4 待购声音数量>20 则固定显示 后20集
//if (count > 20) {
// Map<String, Object> map = new HashMap<>();
// map.put("name", "后20集");
// map.put("price", price.multiply(BigDecimal.valueOf(20)));
// map.put("trackCount", 20);
// list.add(map);
//}
//
////5.5 待购声音数量>20 且 <=30 则动态显示 后*集
//if (count > 20 && count <= 30) {
// Map<String, Object> map = new HashMap<>();
// map.put("name", "后" + count + "集");
// map.put("price", price.multiply(BigDecimal.valueOf(count)));
// map.put("trackCount", count);
// list.add(map);
//}
//if (count > 30) {
// Map<String, Object> map = new HashMap<>();
// map.put("name", "后" + count + "集");
// map.put("price", price.multiply(BigDecimal.valueOf(count)));
// map.put("trackCount", count);
// list.add(map);
//}
for (int i = 10; i <= 50; i += 10) {
//判断 待购声音数量>10、20、30、40、50 则固定显示后 i 集
if (count > i) {
BigDecimal totalPrice = price.multiply(new BigDecimal(i));
Map<String, Object> map = new HashMap<>();
map.put("name", "后" + i + "集");
map.put("price", totalPrice);
map.put("trackCount", i);
list.add(map);
} else {
//待购声音数量在区间内 需要动态显示集(剩余所有的分集)
BigDecimal totalPrice = price.multiply(new BigDecimal(count));
Map<String, Object> map = new HashMap<>();
map.put("name", "后" + count + "集");
map.put("price", totalPrice);
map.put("trackCount", count);
list.add(map);
break;
}
}
return list;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/96
service-album
微服务中的TrackInfoApiController控制器添加代码
/**
* 查询当前用户待购声音列表-用于渲染声音结算页面
*
* @param trackId
* @param trackCount
* @return
*/
@Operation(summary = "查询当前用户待购声音列表-用于渲染声音结算页面")
@GuiGuLogin
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getWaitPayTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount) {
Long userId = AuthContextHolder.getUserId();
List<TrackInfo> list = trackInfoService.getWaitPayTrackInfoList(userId, trackId, trackCount);
return Result.ok(list);
}
/**
* 根据声音ID+声音数量 获取下单付费声音列表
* @param trackId
* @param trackCount
* @return
*/
List<TrackInfo> findPaidTrackInfoList(Long trackId, Integer trackCount);
思路:
1. 根据声音Id 获取到当前声音对象
2. 根据声音ID+当前声音序号查询待购声音列表
3. 远程调用用户服务获取已购买声音ID列表,将已购声音进行排除
4. 返回待购声音集合对象返回
/**
* 查询当前用户待购声音列表-用于渲染声音结算页面
*
* @param userId 用户ID
* @param trackId 选择声音ID
* @param trackCount 包含当前选择声音后续声音数量
* @return
*/
@Override
public List<TrackInfo> getWaitPayTrackInfoList(Long userId, Long trackId, Integer trackCount) {
//1.根据声音ID查询声音对象(用于获取专辑ID)
TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
Assert.notNull(trackInfo, "声音不存在!");
//2.根据专辑ID+当前选中声音序号(序号大于等于当前声音序号声音列表),按照序号正序排-得到“待购”声音集合 limit count 有可能包含已购声音
LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TrackInfo::getAlbumId, trackInfo.getAlbumId());
queryWrapper.ge(TrackInfo::getOrderNum, trackInfo.getOrderNum());
queryWrapper.orderByAsc(TrackInfo::getOrderNum);
queryWrapper.last("limit " + trackCount);
List<TrackInfo> waitBuyTrackInfoList = trackInfoMapper.selectList(queryWrapper);
//3.远程调用“用户服务”根据专辑ID获取用户已购声音ID列表
List<Long> userPaidTrckIdList = userFeignClient.getUserPaidTrackList(trackInfo.getAlbumId()).getData();
//4.将已购声音ID排除到分集购买以外
if (CollectionUtil.isNotEmpty(userPaidTrckIdList)) {
//用户已买过当前专辑下声音,过滤条件:待购声音对象ID没有出现在已购声音ID集合中得到未买声音
waitBuyTrackInfoList = waitBuyTrackInfoList.stream()
.filter(trackInfo1 -> !userPaidTrckIdList.contains(trackInfo1.getId())).collect(Collectors.toList());
}
return waitBuyTrackInfoList;
}
AlbumFeignClient
/**
* 查询当前用户待购声音列表-用于渲染声音结算页面
*
* @param trackId
* @param trackCount
* @return
*/
@GetMapping("/trackInfo/findPaidTrackInfoList/{trackId}/{trackCount}")
public Result<List<TrackInfo>> getWaitPayTrackInfoList(@PathVariable Long trackId, @PathVariable Integer trackCount);
熔断类:
@Override
public Result<List<TrackInfo>> getWaitPayTrackInfoList(Long trackId, Integer trackCount) {
log.error("[专辑模块Feign调用]getWaitPayTrackInfoList异常");
return null;
}
/**
* 订单结算页面渲染-数据汇总
* 1.处理购买类型-VIP会员
* 2.处理购买类型-专辑
* 3.处理购买类型-声音
*
* @return
*/
@Override
public OrderInfoVo trade(Long userId, TradeVo tradeVo) {
//1.声明订单结算对象中价格变量,订单明细、优惠信息变量-赋予初始值
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<>();
//2.远程调用用户微服务获取用户信息
UserInfoVo userInfoVo = userFeignClient.getUserInfoVoByUserId(userId).getData();
Assert.notNull(userInfoVo, "用户信息为空,请联系管理员");
//3.处理购买类型-VIP会员
if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(tradeVo.getItemType())) {
//...省略代码
} else if (SystemConstant.ORDER_ITEM_TYPE_ALBUM.equals(tradeVo.getItemType())) {
//4.处理购买类型-专辑 ...省略
} else if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(tradeVo.getItemType())) {
//TODO 5.处理购买类型-声音
//5.1 远程调用“专辑服务”-根据选择声音ID+数量得到待购声音列表(将已购声音排除掉)
List<TrackInfo> waitPayTrackInfoList = albumFeignClient.getWaitPayTrackInfoList(tradeVo.getItemId(), tradeVo.getTrackCount()).getData();
//5.2 远程调用"专辑服务"获取专辑信息-得到声音单价
AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(waitPayTrackInfoList.get(0).getAlbumId()).getData();
BigDecimal price = albumInfo.getPrice();
//5.3 计算价格:原价,订单价 声音不支持折扣
originalAmount = price.multiply(BigDecimal.valueOf(waitPayTrackInfoList.size()));
orderAmount = originalAmount;
//5.4 遍历待购声音列表构建订单明细集合
orderDetailVoList = waitPayTrackInfoList.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());
}
//6.构建渲染订单结算所需对象OrderInfoVo:包含价格、订单明细列表、优惠列表、其他属性
OrderInfoVo orderInfoVo = new OrderInfoVo();
//6.1 针对本次请求产生流水号,订单被重复条件(提交后回退到页面后继续提交) 将流水存入Redis,方便后续提交订单验证流水号
String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
String tradeNo = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES);
orderInfoVo.setTradeNo(tradeNo);
orderInfoVo.setItemType(tradeVo.getItemType());
orderInfoVo.setOriginalAmount(originalAmount);
orderInfoVo.setDerateAmount(derateAmount);
orderInfoVo.setOrderAmount(orderAmount);
orderInfoVo.setOrderDetailVoList(orderDetailVoList);
orderInfoVo.setOrderDerateVoList(orderDerateVoList);
//6.2 针对本次请求所有参数进行签名 --->md5(参数)=签名值 防止数据被网络传输过程中被恶意篡改
orderInfoVo.setTimestamp(DateUtil.current());
//将Bean对象转为Map 将对象中空值排除掉:支付类型"payway"
Map<String, Object> map = BeanUtil.beanToMap(orderInfoVo, false, true);
String sign = SignHelper.getSign(map);
orderInfoVo.setSign(sign);
return orderInfoVo;
}
声音购买-仅支持余额付款:
专辑购买:
VIP 购买:
在service-order
微服务中添加提交订单控制器
分集购买声音只支持余额支付
购买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;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/98
service-account
微服务控制器UserAccountApiController 控制器
/**
* 检查及锁定账户金额:验证账户余额是否充足、将部分金额进行锁定
*
* @param accountLockVo
* @return
*/
@Operation(summary = "检查及锁定账户金额")
@GuiGuLogin
@PostMapping("/userAccount/checkAndLock")
public Result<AccountLockResultVo> checkAndLock(@RequestBody AccountLockVo accountLockVo) {
Long userId = AuthContextHolder.getUserId();
AccountLockResultVo accountLockResultVo = userAccountService.checkAndLock(accountLockVo, userId);
return Result.ok(accountLockResultVo);
}
UserAccountService接口:
/**
* 检查及锁定账户余额,验证账户余额是否充足、将部分金额进行锁定
* @param accountLockVo
* @param userId
* @return
*/
AccountLockResultVo checkAndLock(AccountLockVo accountLockVo, Long userId);
/**
* 保存账户变动日志记录
* @param userId
* @param title
* @param tradeType
* @param amount
* @param orderNo
*/
void saveUserAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo);
UserAccountServiceImpl实现类:
返回锁定对象,并将对象放入缓存中
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserAccountDetailMapper userAccountDetailMapper;
/**
* 检查及锁定账户余额,验证账户余额是否充足、将部分金额进行锁定
*
* @param accountLockVo 订单编号、用户ID、锁定金额、购买内容
* @param userId
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AccountLockResultVo checkAndLock(AccountLockVo accountLockVo, Long userId) {
//1.幂等性处理:setnx避免同一笔订单多次对账户进行锁定,非首次处理查询Redis得到锁定结果,将锁定结果返回
String key = RedisConstant.ACCOUNT_MUTIPLE_CHECK + accountLockVo.getOrderNo();
String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + accountLockVo.getOrderNo();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, accountLockVo.getOrderNo(), 1, TimeUnit.HOURS);
if (!flag) {
//说明非首次调用 尝试从Redis中获取账户锁定结果,如果有结果则直接返回
AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
if (accountLockResultVo == null) {
//删除重复锁定key,抛出异常
redisTemplate.delete(key);
throw new GuiguException(ResultCodeEnum.ACCOUNT_LOCK_RESULT_NULL);
}
return accountLockResultVo;
}
//2.根据用户ID+锁定金额对账户表中记录进行查询(余额是否充足)利用数据库“锁”机制避免并发情况下对账户多次锁定-采用“悲观锁”-行锁
UserAccount userAccount = userAccountMapper.check(userId, accountLockVo.getAmount());
if (userAccount == null) {
//锁定失败
redisTemplate.delete(key);
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}
//3.如果查询余额充足,执行锁定操作
int cout = userAccountMapper.lock(userId, accountLockVo.getAmount());
if (cout == 0) {
//锁定失败
redisTemplate.delete(key);
throw new GuiguException(ResultCodeEnum.ACCOUNT_LESS);
}
//4.检查锁定都成功,将锁定结果存入Redis,后续账户扣减、账户恢复都可以从Redis获取
AccountLockResultVo accountLockResultVo = BeanUtil.copyProperties(accountLockVo, AccountLockResultVo.class);
redisTemplate.opsForValue().set(lockResultKey, accountLockResultVo, 1, TimeUnit.HOURS);
//5. 新增账户变动日志
this.saveUserAccountDetail(accountLockVo.getUserId(), "锁定:" + accountLockVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_LOCK, accountLockVo.getAmount(), accountLockVo.getOrderNo());
//5.将锁定结果返回
return accountLockResultVo;
}
/**
* 保存账户变动日志记录
*
* @param userId
* @param s
* @param accountTradeTypeLock
* @param amount
* @param orderNo
*/
@Override
public void saveUserAccountDetail(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);
}
UserAccountMapper.java
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 check(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
/**
* 账户可用余额锁定
* @param userId
* @param amount
* @return
*/
int lock(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}
UserAccountMapper.xml
<!--查询可用余额是否充足:采用悲观锁-->
<select id="check" resultType="com.atguigu.tingshu.model.account.UserAccount">
SELECT * from user_account where user_id = #{userId} and available_amount >= #{amount} for update
</select>
UserAccountMapper.xml
<!--账户锁定金额修改-->
<update id="lock">
UPDATE user_account set lock_amount = lock_amount + #{amount}, available_amount = available_amount - #{amount} where user_id = #{userId}
</update>
service-account-client
模块中添加 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.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 atguigu
*/
@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.impl;
import com.atguigu.tingshu.account.AccountFeignClient;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AccountDegradeFeignClient implements AccountFeignClient {
@Override
public Result<AccountLockResultVo> checkAndLock(AccountLockVo accountLockVo) {
log.error("[账户]服务调用checkAndLock执行服务降级");
return null;
}
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/99
OrderInfoApiController
/**
* 订单提交及可能存在余额支付
*
* @param orderInfoVo
* @return
*/
@Operation(summary = "订单提交(单独处理余额付款)")
@GuiGuLogin
@PostMapping("/orderInfo/submitOrder")
public Result<Map<String, String>> submitOrder(@RequestBody @Validated OrderInfoVo orderInfoVo) {
Long userId = AuthContextHolder.getUserId();
Map<String, String> map = orderInfoService.submitOrder(userId, orderInfoVo);
return Result.ok(map);
}
/**
* 订单提交及可能存在余额支付
*
* @param orderInfoVo
* @return
*/
Map<String, String> submitOrder(Long userId, OrderInfoVo orderInfoVo);
/**
* 订单提交及可能存在余额支付
*
* @param orderInfoVo
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, String> submitOrder(Long userId, OrderInfoVo orderInfoVo) {
//1.请求参数验签-避免提交参数被篡改
//1.1 当时渲染结算页签名生成将支付方式payWay排除,提交VO中包含支付方式,在验签前将VO转为Map将支付方式“payWay”移除
Map<String, Object> paramMap = BeanUtil.beanToMap(orderInfoVo);
paramMap.remove("payWay");
//1.2 调用工具方法验证签名
SignHelper.checkSign(paramMap);
//2.验证流水号-避免订单重复提交
//2.1 先通过流水号Key查询Redis中正确流水号
String tradeKey = RedisConstant.ORDER_TRADE_NO_PREFIX + userId;
//2.2 跟用户提交流水号比对 比对成功后,将流水号删除(保证原子性-lua脚本)
String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Boolean.class);
Boolean flag = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(tradeKey), orderInfoVo.getTradeNo());
if (!flag) {
throw new GuiguException(ResultCodeEnum.ORDER_SUBMIT_REPEAT);
}
//3.保存订单及订单明细、订单优惠明细
OrderInfo orderInfo = this.saveOrder(userId, orderInfoVo);
//4.TODO 处理余额付款(支付方式:1101-微信 1102-支付宝 1103-账户余额) VIP、专辑、声音都支持余额付款。声音仅自持余额付款
if (SystemConstant.ORDER_PAY_ACCOUNT.equals(orderInfoVo.getPayWay())) {
try {
//4.1 远程调用“账户微服务”-检查及锁定可用余额
AccountLockVo accountLockVo = new AccountLockVo();
accountLockVo.setOrderNo(orderInfo.getOrderNo());
accountLockVo.setUserId(userId);
accountLockVo.setAmount(orderInfoVo.getOrderAmount());
accountLockVo.setContent(orderInfoVo.getOrderDetailVoList().get(0).getItemName());
Result<AccountLockResultVo> lockResult = accountFeignClient.checkAndLock(accountLockVo);
if (200 != lockResult.getCode() || lockResult.getData() == null) {
log.error("[订单服务]远程调用账户服务进行锁定可用余额失败:业务状态码:{},信息:{}", lockResult.getCode(), lockResult.getMessage());
//锁定金额异常,订单也必须回滚
throw new GuiguException(lockResult.getCode(), lockResult.getMessage());
}
//4.2 TODO 锁定成功:默认扣减为成功 采用MQ完成账户扣减
kafkaService.sendMessage(KafkaConstant.QUEUE_ACCOUNT_MINUS, orderInfo.getOrderNo());
//4.3 TODO 修改订单状态:修改为已支付
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_PAID);
orderInfoMapper.updateById(orderInfo);
//4.4 TODO 采用MQ处理用户购买记录
UserPaidRecordVo userPaidRecordVo = new UserPaidRecordVo();
userPaidRecordVo.setOrderNo(orderInfo.getOrderNo());
userPaidRecordVo.setUserId(userId);
userPaidRecordVo.setItemType(orderInfoVo.getItemType());
List<Long> itemIdList = orderInfoVo.getOrderDetailVoList().stream().map(OrderDetailVo::getItemId).collect(Collectors.toList());
userPaidRecordVo.setItemIdList(itemIdList);
kafkaService.sendMessage(KafkaConstant.QUEUE_USER_PAY_RECORD, JSON.toJSONString(userPaidRecordVo));
} catch (Exception e) {
//4.5 TODO 如果以上操作异常,采用MQ完成账户回滚
//以上操作:锁定、扣减、购买记录等业务代码异常 则进行基于MQ消息进行回滚
//5.利用kafka消息完成解锁
kafkaService.sendMessage(KafkaConstant.QUEUE_ACCOUNT_UNLOCK, orderInfo.getOrderNo());
//TODO 利用Kafka消息完成购买记录回滚(删除)达到事务最终一致
throw new RuntimeException(e.getMessage());
}
}
//5.封装订单编号到Map
Map<String, String> map = new HashMap<>();
map.put("orderNo", orderInfo.getOrderNo());
//6.发送延迟消息:延迟关闭订单
this.sendDealyMessage(orderInfo.getId().toString(), 30, TimeUnit.SECONDS);
return map;
}
业务处理:
OrderInfoService接口
/**
* 保存订单及订单明细、订单优惠明细
* @param userId 用户ID
* @param orderInfoVo 订单相关信息
* @return
*/
OrderInfo saveOrder(Long userId, OrderInfoVo orderInfoVo);
OrderInfoServiceImpl实现类
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private OrderDerateMapper orderDerateMapper;
/**
* 保存订单及订单明细、订单优惠明细
* 1.将订单相关信息封装为订单对象,向订单表增加一条记录
* 2.将提交订单明细封装为订单明细集合,批量向订单明细表新增若干条记录
* 3.将提交优惠明细封装为优惠明细集合,批量向优惠明细表新增若干条记录
*
* @param userId 用户ID
* @param orderInfoVo 订单相关信息
* @return
*/
@Override
public OrderInfo saveOrder(Long userId, OrderInfoVo orderInfoVo) {
//1.将订单相关信息封装为订单对象,向订单表增加一条记录
//1.1 将入参订单VO拷贝到订单PO对象中
OrderInfo orderInfo = BeanUtil.copyProperties(orderInfoVo, OrderInfo.class);
//1.2 给属性赋值 用户ID、订单标题、订单编号、订单状态(未支付)
orderInfo.setUserId(userId);
orderInfo.setOrderStatus(SystemConstant.ORDER_STATUS_UNPAID);
orderInfo.setOrderTitle(orderInfoVo.getOrderDetailVoList().get(0).getItemName());
//订单编号形式:当天YYYYMMDD+分布式ID生成策略:雪花算法
String orderNo = DateUtil.today().replaceAll("-", "") + IdUtil.getSnowflakeNextId();
orderInfo.setOrderNo(orderNo);
orderInfoMapper.insert(orderInfo);
Long orderId = orderInfo.getId();
// 2.将提交订单明细封装为订单明细集合,批量向订单明细表新增若干条记录
List<OrderDetailVo> orderDetailVoList = orderInfoVo.getOrderDetailVoList();
if (CollectionUtil.isNotEmpty(orderDetailVoList)) {
orderDetailVoList.stream().forEach(orderDetailVo -> {
OrderDetail orderDetail = BeanUtil.copyProperties(orderDetailVo, OrderDetail.class);
orderDetail.setOrderId(orderId);
orderDetailMapper.insert(orderDetail);
});
}
//3.将提交优惠明细封装为优惠明细集合,批量向优惠明细表新增若干条记录
List<OrderDerateVo> orderDerateVoList = orderInfoVo.getOrderDerateVoList();
if (CollectionUtil.isNotEmpty(orderDerateVoList)) {
orderDerateVoList.stream().forEach(orderDerateVo -> {
OrderDerate orderDerate = BeanUtil.copyProperties(orderDerateVo, OrderDerate.class);
orderDerate.setOrderId(orderId);
orderDerateMapper.insert(orderDerate);
});
}
return orderInfo;
}
在service-account
模块AccountReceiver中监听完成扣减账户余额或账户余额回滚。
/**
* 扣减锁定金额
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_ACCOUNT_MINUS)
public void minus(ConsumerRecord<String, String> record) {
String orderNo = record.value();
if (StringUtils.isNotBlank(orderNo)) {
log.info("【账户服务】监听到扣减锁定金额消息:{}", orderNo);
//扣减锁定金额
userAccountService.minus(orderNo);
}
}
UserAccountServiceImpl实现类:
/**
* 扣减账户锁定金额
*
* @param orderNo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void minus(String orderNo) {
//1.业务去重避免多次扣减
String key = "account:minus" + orderNo;
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, orderNo, 1, TimeUnit.HOURS);
if (flag) {
//2.扣减锁定金额
//2.1 从Redis中获取锁定结果对象
String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + orderNo;
AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
if (accountLockResultVo != null) {
//2.2 扣减锁定金额
int count = userAccountMapper.minus(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
if (count == 0) {
//解除去重
redisTemplate.delete(key);
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH.ACCOUNT_MINUSLOCK_ERROR);
}
//2.3 记录账户变动日志
this.saveAccountDetail(accountLockResultVo.getUserId(), accountLockResultVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_MINUS, accountLockResultVo.getAmount(), orderNo);
//2.4 删除Redis中锁定结果
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>
在service-account
微服务中的AccountReceiver 添加
/**
* 还原锁定金额
*
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_ACCOUNT_UNLOCK)
public void unlock(ConsumerRecord<String, String> record) {
String orderNo = record.value();
if (StringUtils.isNotBlank(orderNo)) {
log.info("【账户服务】监听到还原锁定金额消息:{}", orderNo);
// 调用还原锁定金额
userAccountService.unlock(orderNo);
}
}
接口:
/**
* 解除锁定
* @param orderNo
*/
void unlock(String orderNo);
实现类:
/**
* 还原锁定金额
*
* @param orderNo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void unlock(String orderNo) {
//1.业务去重避免多次扣减
String key = "account:plus" + orderNo;
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, orderNo, 1, TimeUnit.HOURS);
if (!flag) {
return;
}
//2.从Redis中获取锁定结果用于恢复锁定金额
String lockResultKey = RedisConstant.ACCOUNT_CHECK_DATA + orderNo;
AccountLockResultVo accountLockResultVo = (AccountLockResultVo) redisTemplate.opsForValue().get(lockResultKey);
if (accountLockResultVo == null) {
return;
}
int count = userAccountMapper.unlock(accountLockResultVo.getUserId(), accountLockResultVo.getAmount());
if (count == 0) {
//更新失败 解除去重
redisTemplate.delete(key);
throw new GuiguException(ResultCodeEnum.ACCOUNT_UNLOCK_ERROR);
}
//3.记录账户变动日志
this.saveAccountDetail(accountLockResultVo.getUserId(), "解锁:" + accountLockResultVo.getContent(), SystemConstant.ACCOUNT_TRADE_TYPE_UNLOCK, accountLockResultVo.getAmount(), orderNo);
//4.恢复锁定金额后,删除锁定结果,以防重复恢复
redisTemplate.delete(lockResultKey);
}
UserAccountMapper.java
/**
* 调用解除锁定方法.
* @param userId
* @param amount
* @return
*/
int unLock(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
UserAccountMapper.xml
<update id="unLock">
update user_account
set lock_amount = lock_amount - #{amount}, available_amount = available_amount + #{amount}
where user_id = #{userId}
</update>
在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.commons.lang3.StringUtils;
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;
/**
* @author: atguigu
* @create: 2023-11-06 10:30
*/
@Slf4j
@Component
public class UserReceiver {
@Autowired
private UserInfoService userInfoService;
/**
* 处理用户购买记录;VIP会员、专辑、声音
* @param record
*/
@KafkaListener(topics = KafkaConstant.QUEUE_USER_PAY_RECORD)
public void saveUserPayRecord(ConsumerRecord<String, String> record) {
String userPaidRecordStr = record.value();
if (StringUtils.isNotBlank(userPaidRecordStr)) {
log.info("[用户服务]监听处理购买记录:{}", userPaidRecordStr);
UserPaidRecordVo userPaidRecordVo = JSON.parseObject(userPaidRecordStr, UserPaidRecordVo.class);
userInfoService.saveUserPayRecord(userPaidRecordVo);
}
}
}
UserInfoService 接口中添加
/**
* 处理用户购买记录
* @param userPaidRecordVo
*/
void saveUserPayRecord(UserPaidRecordVo userPaidRecordVo);
UserInfoServiceImpl实现类
@Autowired
private UserVipServiceMapper userVipServiceMapper;
@Autowired
private VipServiceConfigMapper vipServiceConfigMapper;
/**
* 处理用户购买记录
* 1.处理声音购买记录-根据订单编号避免重复增加购买记录
* 2.处理专辑购买记录-根据订单编号避免重复增加购买记录
* 3.处理会员购买记录
* -根据订单编号避免重复增加购买记录
* -修改用户表VIP状态及失效时间
*
* @param userPaidRecordVo
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void saveUserPayRecord(UserPaidRecordVo userPaidRecordVo) {
//1.处理声音购买记录
if (SystemConstant.ORDER_ITEM_TYPE_TRACK.equals(userPaidRecordVo.getItemType())) {
//1.1 根据订单编号避免重复增加购买记录
LambdaQueryWrapper<UserPaidTrack> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidTrack::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userPaidTrackMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//1.2 构建声音购买记录新增
//1.3 远程调用专辑服务获取声音信息
TrackInfo trackInfo = albumFeignClient.getTrackInfo(userPaidRecordVo.getItemIdList().get(0)).getData();
userPaidRecordVo.getItemIdList().forEach(trackId -> {
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_ALBUM.equals(userPaidRecordVo.getItemType())) {
//2.处理专辑购买记录
//2.1 根据订单编号避免重复增加购买记录
LambdaQueryWrapper<UserPaidAlbum> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPaidAlbum::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userPaidAlbumMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//2.2 新增专辑购买记录
userPaidRecordVo.getItemIdList().forEach(albumId -> {
UserPaidAlbum userPaidAlbum = new UserPaidAlbum();
userPaidAlbum.setOrderNo(userPaidRecordVo.getOrderNo());
userPaidAlbum.setUserId(userPaidRecordVo.getUserId());
userPaidAlbum.setAlbumId(albumId);
userPaidAlbumMapper.insert(userPaidAlbum);
});
} else if (SystemConstant.ORDER_ITEM_TYPE_VIP.equals(userPaidRecordVo.getItemType())) {
//3.处理会员购买记录
//3.1-根据订单编号避免重复增加购买记录
LambdaQueryWrapper<UserVipService> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserVipService::getOrderNo, userPaidRecordVo.getOrderNo());
Long count = userVipServiceMapper.selectCount(queryWrapper);
if (count > 0) {
return;
}
//3.2-修改用户表VIP状态及失效时间
Date startTime = new Date();
//2.1.1 获取用户
UserInfo userInfo = userInfoMapper.selectById(userPaidRecordVo.getUserId());
//2.1.2 根据套餐ID查询VIP套餐信息
VipServiceConfig vipServiceConfig = vipServiceConfigMapper.selectById(userPaidRecordVo.getItemIdList().get(0));
Date expireTime = new Date();
//刚购买VIP VIP开始时间-当前,过期时间:当前时间+购买月数
//已是VIP且在有效期,过期时间:现有失效时间+购买月数
if (userInfo.getIsVip() == 1 && userInfo.getVipExpireTime().after(new Date())) {
expireTime = DateUtil.offsetMonth(userInfo.getVipExpireTime(), vipServiceConfig.getServiceMonth());
} else {
//首次购买会员-过期时间
expireTime = DateUtil.offsetMonth(startTime, vipServiceConfig.getServiceMonth());
}
userInfo.setIsVip(1);
userInfo.setVipExpireTime(expireTime);
userInfoMapper.updateById(userInfo);
//3.3-新增用户VIP购买记录
for (Long vipConfigId : userPaidRecordVo.getItemIdList()) {
UserVipService userVipService = new UserVipService();
userVipService.setOrderNo(userPaidRecordVo.getOrderNo());
userVipService.setUserId(userPaidRecordVo.getUserId());
userVipService.setStartTime(startTime);
userVipService.setExpireTime(expireTime);
userVipService.setIsAutoRenew(0);
userVipService.setNextRenewTime(null);
userVipServiceMapper.insert(userVipService);
}
}
}
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;
}
YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/100
当支付成功之后,点击查看订单
在service-order
微服务OrderInfoApiController中添加
/**
* 查询当前用户指定订单信息
*
* @param orderNo
* @return
*/
@GuiGuLogin
@GetMapping("/orderInfo/getOrderInfo/{orderNo}")
public Result<OrderInfo> getOrderInfo(@PathVariable("orderNo") String orderNo) {
Long userId = AuthContextHolder.getUserId();
OrderInfo orderInfo = orderInfoService.getOrderInfo(userId, orderNo);
return Result.ok(orderInfo);
}
OrderInfoService接口
/**
* 获取订单信息
*
* @param userId
* @param orderNo
* @return
*/
OrderInfo getOrderInfo(Long userId, String orderNo);
OrderInfoServiceImpl实现
/**
* 获取订单信息
*
* @param userId
* @param orderNo
* @return
*/
@Override
public OrderInfo getOrderInfo(Long userId, String orderNo) {
//1.根据订单编号+用户ID查询订单信息
LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderInfo::getOrderNo, orderNo);
queryWrapper.eq(OrderInfo::getUserId, userId);
OrderInfo orderInfo = orderInfoMapper.selectOne(queryWrapper);
if (orderInfo != null) {
//2.根据订单ID查询订单明细
LambdaQueryWrapper<OrderDetail> orderDetailLambdaQueryWrapper = new LambdaQueryWrapper<>();
orderDetailLambdaQueryWrapper.eq(OrderDetail::getOrderId, orderInfo.getId());
List<OrderDetail> orderDetailList = orderDetailMapper.selectList(orderDetailLambdaQueryWrapper);
orderInfo.setOrderDetailList(orderDetailList);
orderInfo.setPayWayName(this.getPayWayName(orderInfo.getPayWay()));
orderInfo.setOrderStatusName(this.getOrderStatusName(orderInfo.getOrderStatus()));
//3.根据订单ID查询订单优惠明细
LambdaQueryWrapper<OrderDerate> orderDerateLambdaQueryWrapper = new LambdaQueryWrapper<>();
orderDerateLambdaQueryWrapper.eq(OrderDerate::getOrderId, orderInfo.getId());
List<OrderDerate> orderDerateList = orderDerateMapper.selectList(orderDerateLambdaQueryWrapper);
orderInfo.setOrderDerateList(orderDerateList);
return orderInfo;
}
return null;
}
private String getOrderStatusName(String orderStatus) {
if (SystemConstant.ORDER_STATUS_UNPAID.equals(orderStatus)) {
return "未支付";
} else if (SystemConstant.ORDER_STATUS_PAID.equals(orderStatus)) {
return "已支付";
} else if (SystemConstant.ORDER_STATUS_CANCEL.equals(orderStatus)) {
return "取消";
}
return null;
}
/**
* 根据支付方式编号得到支付名称
*
* @param payWay
* @return
*/
private String getPayWayName(String payWay) {
if (SystemConstant.ORDER_PAY_WAY_WEIXIN.equals(payWay)) {
return "微信";
} else if (SystemConstant.ORDER_PAY_ACCOUNT.equals(payWay)) {
return "余额";
} else if (SystemConstant.ORDER_PAY_WAY_ALIPAY.equals(payWay)) {
return "支付宝";
}
return "";
}
YAPI接口地址:
service-order
的OrderInfoApiController 控制器 添加
/**
* 分页获取用户订单列表
*
* @param page
* @param limit
* @return
*/
@GuiGuLogin
@Operation(summary = "分页获取用户订单列表")
@GetMapping("/orderInfo/findUserPage/{page}/{limit}")
public Result<Page<OrderInfo>> getUserOrderByPage(@PathVariable Long page, @PathVariable Long limit) {
Long userId = AuthContextHolder.getUserId();
Page<OrderInfo> pageInfo = new Page<>(page, limit);
pageInfo = orderInfoService.getUserOrderByPage(pageInfo, userId);
return Result.ok(pageInfo);
}
OrderInfoService接口
/**
* 分页获取用户订单列表
*
* @param pageInfo
* @param userId
* @return
*/
Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId);
OrderInfoServiceImpl实现
/**
* 分页获取用户订单列表
*
* @param pageInfo
* @param userId
* @return
*/
@Override
public Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, Long userId) {
pageInfo = orderInfoMapper.getUserOrderByPage(pageInfo, userId);
//设置订单状态跟支付方式:中文
pageInfo.getRecords().forEach(orderInfo -> {
orderInfo.setOrderStatusName(this.getOrderStatusName(orderInfo.getOrderStatus()));
orderInfo.setPayWayName(this.getPayWayName(orderInfo.getPayWay()));
});
return pageInfo;
}
OrderInfoMapper 接口
package com.atguigu.tingshu.order.mapper;
import com.atguigu.tingshu.model.order.OrderInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
Page<OrderInfo> getUserOrderByPage(Page<OrderInfo> pageInfo, @Param("userId") Long userId);
}
OrderInfoMapper.xml 映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.tingshu.order.mapper.OrderInfoMapper">
<!--查询订单及订单明细配置自定义结果集:封装一对多-->
<resultMap id="orderInfoMap" type="com.atguigu.tingshu.model.order.OrderInfo" autoMapping="true">
<id column="id" property="id"></id>
<collection property="orderDetailList" ofType="com.atguigu.tingshu.model.order.OrderDetail" autoMapping="true">
<id column="order_detial_id" property="id"></id>
</collection>
</resultMap>
<select id="getUserOrderByPage" resultMap="orderInfoMap">
SELECT
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 order_detial_id,
od.item_id,
od.item_name,
od.item_url,
od.item_price
FROM
order_info oi
INNER JOIN order_detail od ON od.order_id = oi.id
WHERE
oi.user_id = #{userId}
AND oi.is_deleted = 0
ORDER BY
oi.id DESC
</select>
</mapper>
应用场景:
场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;如果期间下单成功,任务取消
场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止
利用redissonClient 发送延迟消息。
/**
* 提交订单
*
* @param orderInfoVo
* @param userId
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, String> submitOrder(OrderInfoVo orderInfoVo, Long userId) {
//省略。。。。
//TODO 提交订单后,发送延迟关闭订单消息
this.sendDelayMessage(orderInfo.getId());
Map<String, String> map = new HashMap<>();
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
/**
* 发送延迟消息
*/
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 orderCancal(long orderId) {
OrderInfo orderInfo = orderInfoMapper.selectById(orderId);
if (SystemConstant.ORDER_STATUS_UNPAID.equals(orderInfo.getOrderStatus())) {
OrderInfo orderInfoUpt = new OrderInfo();
orderInfoUpt.setId(orderId);
orderInfoUpt.setOrderStatus(SystemConstant.ORDER_STATUS_CANCEL);
this.updateById(orderInfoUpt);
}
}