本系列博客基于B站谷粒商城,只作为本人学习总结使用。这里我会比较注重业务逻辑的编写和相关配置的流程。有问题可以评论或者联系我互相交流。原视频地址谷粒商城雷丰阳版。本人git仓库地址Draknessssw的谷粒商城
pom依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--整合springsession,实现session共享-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
相关配置
spring.redis.host=192.168.75.129
spring.redis.port=6379
spring.session.store-type=redis
session名称配置
package com.xxxx.gulimall.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
@Autowired
private OrderService orderService;
/**
* 去结算确认页
* @param model
* @param request
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@GetMapping(value = "/toTrade")
public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("confirmOrderData",confirmVo);
//展示订单确认的数据
return "confirm";
}
webMVC配置一个拦截器,当session中有登录用户信息再放行请求
package com.xxxx.gulimall.order.config;
import com.xxxx.gulimall.order.interceptor.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
拦截器
在前置拦截器方法中,获取请求uri。匹配一下是否有订单状态和去支付的请求。然后获取请求sessoin中用户的信息后往ThreadLocal中设置用户信息,否则给客户端发送一个跳转登录页的弹出框。
package com.xxxx.gulimall.order.interceptor;
import com.xxxx.common.vo.MemberResponseVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import static com.xxxx.common.constant.AuthServerConstant.LOGIN_USER;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if (match || match1) {
return true;
}
//获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("");
// session.setAttribute("msg", "请先进行登录");
// response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
订单页Vo
package com.xxxx.gulimall.order.vo;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
public class OrderConfirmVo {
@Getter @Setter
/** 会员收获地址列表 **/
List<MemberAddressVo> memberAddressVos;
@Getter @Setter
/** 所有选中的购物项 **/
List<OrderItemVo> items;
/** 发票记录 **/
@Getter @Setter
/** 优惠券(会员积分) **/
private Integer integration;
/** 防止重复提交的令牌 **/
@Getter @Setter
private String orderToken;
@Getter @Setter
Map<Long,Boolean> stocks;
public Integer getCount() {
Integer count = 0;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
count += item.getCount();
}
}
return count;
}
/** 订单总额 **/
//BigDecimal total;
//计算订单总额
public BigDecimal getTotal() {
BigDecimal totalNum = BigDecimal.ZERO;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
//计算当前商品的总价格
BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
//再计算全部商品的总价格
totalNum = totalNum.add(itemPrice);
}
}
return totalNum;
}
/** 应付价格 **/
//BigDecimal payPrice;
public BigDecimal getPayPrice() {
return getTotal();
}
}
会员信息Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
@Data
public class MemberAddressVo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}
购物项Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class OrderItemVo {
private Long skuId;
private Boolean check;
private String title;
private String image;
/**
* 商品套餐属性
*/
private List<String> skuAttrValues;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
/** 商品重量 **/
private BigDecimal weight = new BigDecimal("0.085");
}
接着是实现类
获取ThreadLocal中当前用户登录信息,当前线程的请求头
//构建OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
这里订单服务调用远程接口存在请求头丢失的问题,原因是调用远程接口并不会自动装配cookie信息
配置拦截器对远程请求进行同步装配cookie信息
package com.xxxx.gulimall.order.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* feign拦截器
*/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
接着在每一个线程共享请求数据
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
第一个异步任务,远程查询当前用户的收货地址
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
远程接口
package com.xxxx.gulimall.order.feign;
import com.xxxx.gulimall.order.vo.MemberAddressVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 查询当前用户的全部收货地址
* @param memberId
* @return
*/
@GetMapping(value = "/member/memberreceiveaddress/{memberId}/address")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
List<MemberReceiveAddressEntity> addressList = this.baseMapper.selectList
(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
return addressList;
}
第二个异步任务,查询购物车中的购物项,根据这些购物项再出查询相应的库存
//开启第二个异步任务
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
//feign在远程调用之前要构造请求,调用很多的拦截器
}, threadPoolExecutor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
//获取全部商品的id
List<Long> skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//远程查询商品库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//将skuStockVos集合转换为map
Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(skuHasStockMap);
}
},threadPoolExecutor);
查询购物项的远程接口
package com.xxxx.gulimall.order.feign;
import com.xxxx.gulimall.order.vo.OrderItemVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@FeignClient("gulimall-cart")
public interface CartFeignService {
/**
* 查询当前用户购物车选中的商品项
* @return
*/
@GetMapping(value = "/currentUserCartItems")
List<OrderItemVo> getCurrentCartItems();
}
@Override
public List<CartItemVo> getUserCartItems() {
List<CartItemVo> cartItemVoList = new ArrayList<>();
//获取当前用户登录的信息
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
//如果用户未登录直接返回null
if (userInfoTo.getUserId() == null) {
return null;
} else {
//获取购物车项
String cartKey = CART_PREFIX + userInfoTo.getUserId();
//获取所有的
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems == null) {
throw new CartExceptionHandler();
}
//筛选出选中的
cartItemVoList = cartItems.stream()
.filter(items -> items.getCheck())
.map(item -> {
//更新为最新的价格(查询数据库)
BigDecimal price = productFeignService.getPrice(item.getSkuId());
item.setPrice(price);
return item;
})
.collect(Collectors.toList());
}
return cartItemVoList;
}
/**
* 获取购物车里面的数据
* @param cartKey
* @return
*/
private List<CartItemVo> getCartItems(String cartKey) {
//获取购物车里面的所有商品
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
String str = (String) obj;
CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemVoStream;
}
return null;
}
商品的价格可能会有较大可能随时更新,所以得获取最新的。远程调用商品服务来查询
远程接口
@FeignClient("gulimall-product")
public interface ProductFeignService {
/**
* 根据skuId查询当前商品的最新价格
* @param skuId
* @return
*/
@GetMapping(value = "/product/skuinfo/{skuId}/price")
BigDecimal getPrice(@PathVariable("skuId") Long skuId);
}
@Autowired
private SkuInfoService skuInfoService;
/**
* 根据skuId查询当前商品的价格
* @param skuId
* @return
*/
@GetMapping(value = "/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
//获取当前商品的信息
SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
//获取商品的价格
BigDecimal price = skuInfo.getPrice();
return price;
}
查询商品库存的远程接口
@FeignClient("gulimall-ware")
public interface WmsFeignService {
/**
* 查询sku是否有库存
* @return
*/
@PostMapping(value = "/ware/waresku/hasStock")
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
接着设置积分和防重复令牌
//3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
confirmVo.setIntegration(integration);
//4、价格数据自动计算
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
最终效果如下
/**
* 订单确认页返回需要用的数据
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//开启第一个异步任务
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//开启第二个异步任务
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
//feign在远程调用之前要构造请求,调用很多的拦截器
}, threadPoolExecutor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
//获取全部商品的id
List<Long> skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//远程查询商品库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//将skuStockVos集合转换为map
Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(skuHasStockMap);
}
},threadPoolExecutor);
//3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
confirmVo.setIntegration(integration);
//4、价格数据自动计算
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(addressFuture,cartInfoFuture).get();
return confirmVo;
}
幂等解决方案
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping(value = "/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes attributes) {
try {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单成功来到支付选择页
//下单失败回到订单确认页重新确定订单信息
if (responseVo.getCode() == 0) {
//成功
model.addAttribute("submitOrderResp",responseVo);
return "pay";
} else {
String msg = "下单失败";
switch (responseVo.getCode()) {
case 1: msg += "令牌订单信息过期,请刷新再次提交"; break;
case 2: msg += "订单商品价格发生变化,请确认后再次提交"; break;
case 3: msg += "库存锁定失败,商品库存不足"; break;
}
attributes.addFlashAttribute("msg",msg);
return "redirect:http://order.gulimall.com/toTrade";
}
} catch (Exception e) {
if (e instanceof NoStockException) {
String message = ((NoStockException)e).getMessage();
attributes.addFlashAttribute("msg",message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
订单提交的Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class OrderSubmitVo {
/** 收获地址的id **/
private Long addrId;
/** 支付方式 **/
private Integer payType;
//无需提交要购买的商品,去购物车再获取一遍
//优惠、发票
/** 防重令牌 **/
private String orderToken;
/** 应付价格 **/
private BigDecimal payPrice;
/** 订单备注 **/
private String remarks;
//用户相关的信息,直接去session中取出即可
}
提交订单的响应Vo
package com.xxxx.gulimall.order.vo;
import com.xxxx.gulimall.order.entity.OrderEntity;
import lombok.Data;
@Data
public class SubmitOrderResponseVo {
private OrderEntity order;
/** 错误状态码 **/
private Integer code;
}
实现类
首先暂且将请求提交的vo存到threadLocal里,从用户登录的拦截器设置的用户登录信息那里获取用户信息。
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//去创建、下订单、验令牌、验价格、锁定库存...
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
responseVo.setCode(0);
验证令牌防止重复提交
lua脚本获取Redis中token和vo中的token比对,一致就删除Redis中的token,其他情况返回0
而Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId())则是要验证的token中的key
//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
如果token验证失败,则设置表单返回vo的状态码为1即可(Controller中状态码为1才设置显示表单数据),订单信息不用设置,直接返回。否则就去设置返回Vo的订单详情再返回。
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功
//1、创建订单、订单项等信息
令牌验证成功,创建订单
//令牌验证成功
//1、创建订单、订单项等信息
OrderCreateTo order = createOrder();
创建订单方法有三个业务方法
private OrderCreateTo createOrder() {
OrderCreateTo createTo = new OrderCreateTo();
//1、生成订单号
String orderSn = IdWorker.getTimeId();
//创建订单号
OrderEntity orderEntity = builderOrder(orderSn);
//2、获取到所有的订单项
List<OrderItemEntity> orderItemEntities = builderOrderItems(orderSn);
//3、验价(计算价格、积分等信息)
computePrice(orderEntity,orderItemEntities);
createTo.setOrder(orderEntity);
createTo.setOrderItems(orderItemEntities);
return createTo;
}
/**
* 构建订单数据
* @param orderSn
* @return
*/
private OrderEntity builderOrder(String orderSn) {
//获取当前用户登录信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
OrderEntity orderEntity = new OrderEntity();
orderEntity.setMemberId(memberResponseVo.getId());
orderEntity.setOrderSn(orderSn);
orderEntity.setMemberUsername(memberResponseVo.getUsername());
OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
//远程获取收货地址和运费信息
R fareAddressVo = wmsFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareResp = fareAddressVo.getData("data", new TypeReference<FareVo>() {});
//获取到运费信息
BigDecimal fare = fareResp.getFare();
orderEntity.setFreightAmount(fare);
//获取到收货地址信息
MemberAddressVo address = fareResp.getAddress();
//设置收货人信息
orderEntity.setReceiverName(address.getName());
orderEntity.setReceiverPhone(address.getPhone());
orderEntity.setReceiverPostCode(address.getPostCode());
orderEntity.setReceiverProvince(address.getProvince());
orderEntity.setReceiverCity(address.getCity());
orderEntity.setReceiverRegion(address.getRegion());
orderEntity.setReceiverDetailAddress(address.getDetailAddress());
//设置订单相关的状态信息
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setAutoConfirmDay(7);
orderEntity.setConfirmStatus(0);
return orderEntity;
}
其中,运费信息和地址信息需要远程调用库存服务来获取
//远程获取收货地址和运费信息
R fareAddressVo = wmsFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareResp = fareAddressVo.getData("data", new TypeReference<FareVo>() {});
运费和收货地址信息Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class FareVo {
private MemberAddressVo address;
private BigDecimal fare;
}
远程接口
@FeignClient("gulimall-ware")
public interface WmsFeignService {
/**
* 查询运费和收货地址信息
* @param addrId
* @return
*/
@GetMapping(value = "/ware/wareinfo/fare")
R getFare(@RequestParam("addrId") Long addrId);
}
/**
* 获取运费信息
* @return
*/
@GetMapping(value = "/fare")
public R getFare(@RequestParam("addrId") Long addrId) {
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
实现类
/**
* 计算运费
* @param addrId
* @return
*/
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
//收获地址的详细信息
R addrInfo = memberFeignService.info(addrId);
MemberAddressVo memberAddressVo = addrInfo.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});
if (memberAddressVo != null) {
String phone = memberAddressVo.getPhone();
//截取用户手机号码最后一位作为我们的运费计算
//1558022051
String fare = phone.substring(phone.length() - 10, phone.length()-8);
BigDecimal bigDecimal = new BigDecimal(fare);
fareVo.setFare(bigDecimal);
fareVo.setAddress(memberAddressVo);
return fareVo;
}
return null;
}
这里又套娃去远程调用会员服务查询地址信息
远程接口
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 根据id获取用户地址信息
* @param id
* @return
*/
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R info(@PathVariable("id") Long id);
}
/**
* 信息
*/
@RequestMapping("/info/{id}")
public R info(@PathVariable("id") Long id){
MemberReceiveAddressEntity memberReceiveAddress = memberReceiveAddressService.getById(id);
return R.ok().put("memberReceiveAddress", memberReceiveAddress);
}
/**
* 构建所有订单项数据
* @return
*/
public List<OrderItemEntity> builderOrderItems(String orderSn) {
List<OrderItemEntity> orderItemEntityList = new ArrayList<>();
//最后确定每个购物项的价格
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
if (currentCartItems != null && currentCartItems.size() > 0) {
orderItemEntityList = currentCartItems.stream().map((items) -> {
//构建订单项数据
OrderItemEntity orderItemEntity = builderOrderItem(items);
orderItemEntity.setOrderSn(orderSn);
return orderItemEntity;
}).collect(Collectors.toList());
}
return orderItemEntityList;
}
商品项Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class OrderItemVo {
private Long skuId;
private Boolean check;
private String title;
private String image;
/**
* 商品套餐属性
*/
private List<String> skuAttrValues;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
/** 商品重量 **/
private BigDecimal weight = new BigDecimal("0.085");
}
获取当前购物车中的所有购物项远程接口
@FeignClient("gulimall-cart")
public interface CartFeignService {
/**
* 查询当前用户购物车选中的商品项
* @return
*/
@GetMapping(value = "/currentUserCartItems")
List<OrderItemVo> getCurrentCartItems();
}
@Autowired
private CartService cartService;
/**
* 获取当前用户的购物车商品项
* @return
*/
@GetMapping(value = "/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentCartItems() {
List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
return cartItemVoList;
}
/**
* 获取当前用户的购物车商品项
* @return
*/
@GetMapping(value = "/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentCartItems() {
List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
return cartItemVoList;
}
实现类
@Override
public List<CartItemVo> getUserCartItems() {
List<CartItemVo> cartItemVoList = new ArrayList<>();
//获取当前用户登录的信息
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
//如果用户未登录直接返回null
if (userInfoTo.getUserId() == null) {
return null;
} else {
//获取购物车项
String cartKey = CART_PREFIX + userInfoTo.getUserId();
//获取所有的
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems == null) {
throw new CartExceptionHandler();
}
//筛选出选中的
cartItemVoList = cartItems.stream()
.filter(items -> items.getCheck())
.map(item -> {
//更新为最新的价格(查询数据库)
BigDecimal price = productFeignService.getPrice(item.getSkuId());
item.setPrice(price);
return item;
})
.collect(Collectors.toList());
}
return cartItemVoList;
}
其中,获取购物车里的数据
/**
* 获取购物车里面的数据
* @param cartKey
* @return
*/
private List<CartItemVo> getCartItems(String cartKey) {
//获取购物车里面的所有商品
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
String str = (String) obj;
CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemVoStream;
}
return null;
}
查询每一个购物项的价格
远程接口
/**
* 根据skuId查询当前商品的最新价格
* @param skuId
* @return
*/
@GetMapping(value = "/product/skuinfo/{skuId}/price")
BigDecimal getPrice(@PathVariable("skuId") Long skuId);
/**
* 根据skuId查询当前商品的价格
* @param skuId
* @return
*/
@GetMapping(value = "/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
//获取当前商品的信息
SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
//获取商品的价格
BigDecimal price = skuInfo.getPrice();
return price;
}
接着回到构建所有订单项数据的方法,将获取的所有订单项封装重构
/**
* 构建某一个订单项的数据
* @param items
* @return
*/
private OrderItemEntity builderOrderItem(OrderItemVo items) {
OrderItemEntity orderItemEntity = new OrderItemEntity();
//1、商品的spu信息
Long skuId = items.getSkuId();
//获取spu的信息
R spuInfo = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {
});
orderItemEntity.setSpuId(spuInfoData.getId());
orderItemEntity.setSpuName(spuInfoData.getSpuName());
orderItemEntity.setSpuBrand(spuInfoData.getBrandName());
orderItemEntity.setCategoryId(spuInfoData.getCatalogId());
//2、商品的sku信息
orderItemEntity.setSkuId(skuId);
orderItemEntity.setSkuName(items.getTitle());
orderItemEntity.setSkuPic(items.getImage());
orderItemEntity.setSkuPrice(items.getPrice());
orderItemEntity.setSkuQuantity(items.getCount());
//使用StringUtils.collectionToDelimitedString将list集合转换为String
String skuAttrValues = StringUtils.collectionToDelimitedString(items.getSkuAttrValues(), ";");
orderItemEntity.setSkuAttrsVals(skuAttrValues);
//3、商品的优惠信息
//4、商品的积分信息
orderItemEntity.setGiftGrowth(items.getPrice().multiply(new BigDecimal(items.getCount())).intValue());
orderItemEntity.setGiftIntegration(items.getPrice().multiply(new BigDecimal(items.getCount())).intValue());
//5、订单项的价格信息
orderItemEntity.setPromotionAmount(BigDecimal.ZERO);
orderItemEntity.setCouponAmount(BigDecimal.ZERO);
orderItemEntity.setIntegrationAmount(BigDecimal.ZERO);
//当前订单项的实际金额.总额 - 各种优惠价格
//原来的价格
BigDecimal origin = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));
//原价减去优惠价得到最终的价格
BigDecimal subtract = origin.subtract(orderItemEntity.getCouponAmount())
.subtract(orderItemEntity.getPromotionAmount())
.subtract(orderItemEntity.getIntegrationAmount());
orderItemEntity.setRealAmount(subtract);
return orderItemEntity;
}
其中,获取商品集合的远程接口
@FeignClient("gulimall-product")
public interface ProductFeignService {
/**
* 根据skuId查询spu的信息
* @param skuId
* @return
*/
@GetMapping(value = "/product/spuinfo/skuId/{skuId}")
public R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId);
}
/**
* 根据skuId查询spu的信息
* @param skuId
* @return
*/
@GetMapping(value = "/skuId/{skuId}")
public R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId) {
SpuInfoEntity spuInfoEntity = spuInfoService.getSpuInfoBySkuId(skuId);
return R.ok().setData(spuInfoEntity);
}
实现类
/**
* 根据skuId查询spu的信息
* @param skuId
* @return
*/
@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
//先查询sku表里的数据
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
//获得spuId
Long spuId = skuInfoEntity.getSpuId();
//再通过spuId查询spuInfo信息表里的数据
SpuInfoEntity spuInfoEntity = this.baseMapper.selectById(spuId);
//查询品牌表的数据获取品牌名
BrandEntity brandEntity = brandService.getById(spuInfoEntity.getBrandId());
spuInfoEntity.setBrandName(brandEntity.getName());
return spuInfoEntity;
}
/**
* 计算价格的方法
* @param orderEntity
* @param orderItemEntities
*/
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {
//总价
BigDecimal total = new BigDecimal("0.0");
//优惠价
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal intergration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
//积分、成长值
Integer integrationTotal = 0;
Integer growthTotal = 0;
//订单总额,叠加每一个订单项的总额信息
for (OrderItemEntity orderItem : orderItemEntities) {
//优惠价格信息
coupon = coupon.add(orderItem.getCouponAmount());
promotion = promotion.add(orderItem.getPromotionAmount());
intergration = intergration.add(orderItem.getIntegrationAmount());
//总价
total = total.add(orderItem.getRealAmount());
//积分信息和成长值信息
integrationTotal += orderItem.getGiftIntegration();
growthTotal += orderItem.getGiftGrowth();
}
//1、订单价格相关的
orderEntity.setTotalAmount(total);
//设置应付总额(总额+运费)
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setCouponAmount(coupon);
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(intergration);
//设置积分成长值信息
orderEntity.setIntegration(integrationTotal);
orderEntity.setGrowth(growthTotal);
//设置删除状态(0-未删除,1-已删除)
orderEntity.setDeleteStatus(0);
}
接着回到提交订单的方法
验证提交订单的价格和最新价格一致再保存订单,否则设置订单响应错误状态码
//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
else {
responseVo.setCode(2);
return responseVo;
}
/**
* 保存订单所有数据
* @param orderCreateTo
*/
private void saveOrder(OrderCreateTo orderCreateTo) {
//获取订单信息
OrderEntity order = orderCreateTo.getOrder();
order.setModifyTime(new Date());
order.setCreateTime(new Date());
//保存订单
this.baseMapper.insert(order);
//获取订单项信息
List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();
//批量保存订单项数据
orderItemService.saveBatch(orderItems);
}
接着是锁定库存
锁定库存的Vo
package com.xxxx.gulimall.order.vo;
import lombok.Data;
import java.util.List;
@Data
public class WareSkuLockVo {
private String orderSn;
/** 需要锁住的所有库存信息 **/
private List<OrderItemVo> locks;
}
重新封装要锁定的商品信息,调用远程服务锁定库存
//4、库存锁定,只要有异常,回滚订单数据
//订单号、所有订单项信息(skuId,skuNum,skuName)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取出要锁定的商品数据信息
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
lockVo.setLocks(orderItemVos);
//TODO 调用远程锁定库存的方法
//出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
//为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
R r = wmsFeignService.orderLockStock(lockVo);
远程接口
@FeignClient("gulimall-ware")
public interface WmsFeignService {
/**
* 锁定库存
* @param vo
* @return
*/
@PostMapping(value = "/ware/waresku/lock/order")
R orderLockStock(@RequestBody WareSkuLockVo vo);
}
尝试去锁定库存,锁定失败就返回一个错误信息
/**
* 锁定库存
* @param vo
*
* 库存解锁的场景
* 1)、下订单成功,订单过期没有支付被系统自动取消或者被用户手动取消,都要解锁库存
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
* 3)、
*
* @return
*/
@PostMapping(value = "/lock/order")
public R orderLockStock(@RequestBody WareSkuLockVo vo) {
try {
boolean lockStock = wareSkuService.orderLockStock(vo);
return R.ok().setData(lockStock);
} catch (NoStockException e) {
return R.error(NO_STOCK_EXCEPTION.getCode(),NO_STOCK_EXCEPTION.getMessage());
}
}
实现类
首先保存库存单信息
/**
* 保存库存工作单详情信息
* 追溯
*/
WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
wareOrderTaskEntity.setCreateTime(new Date());
wareOrderTaskService.save(wareOrderTaskEntity);
查找商品库存信息,找到每个商品在哪个仓库都有库存
//1、按照下单的收货地址,找到一个就近仓库,锁定库存
//2、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map((item) -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪个仓库有库存
List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIdList);
return stock;
}).collect(Collectors.toList());
锁定库存时,没有任何仓库有这个商品的库存就抛出一个自定义没有库存的异常
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
package com.xxxx.common.exception;
import lombok.Getter;
import lombok.Setter;
public class NoStockException extends RuntimeException {
@Getter @Setter
private Long skuId;
public NoStockException(Long skuId) {
super("商品id:"+ skuId + "库存不足!");
}
public NoStockException(String msg) {
super(msg);
}
}
而锁定库存操作成功,也就是锁定库存的影响行数为1。则保存锁定库存信息和锁定库存详细信息到库存工作单详情当中
接着告诉MQ库存锁定成功
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
for (Long wareId : wareIds) {
//锁定成功就返回1,失败就返回0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if (count == 1) {
skuStocked = true;
WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
.skuId(skuId)
.skuName("")
.skuNum(hasStock.getNum())
.taskId(wareOrderTaskEntity.getId())
.wareId(wareId)
.lockStatus(1)
.build();
wareOrderTaskDetailService.save(taskDetailEntity);
//TODO 告诉MQ库存锁定成功
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(wareOrderTaskEntity.getId());
StockDetailTo detailTo = new StockDetailTo();
BeanUtils.copyProperties(taskDetailEntity,detailTo);
lockedTo.setDetailTo(detailTo);
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
break;
sql
<update id="lockSkuStock">
UPDATE wms_ware_sku
SET stock_locked = stock_locked + #{num}
WHERE
sku_id = #{skuId}
AND ware_id = #{wareId}
AND stock - stock_locked > 0
</update>
假使因为网络问题导致库存在提交订单时扣除成功,但是订单取消……
所以,这里使用延时队列来实现订单库存信息的锁定
但是结合一个服务一个交换机的设计原则,延时队列过期,应当返回给交换机,交换机再转给死信队列即可。
创建上述队列和路由组件
/* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */
/**
* 死信队列
*
* @return
*/
@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map arguments) 属性
*/
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
普通队列
/**
* 普通队列处理订单
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
创建路由
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
三段绑定关系
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
最后实现
package com.xxxx.gulimall.order.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class MyRabbitMQConfig {
/* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */
/**
* 死信队列
*
* @return
*/
@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map arguments) 属性
*/
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
/**
* 普通队列处理订单
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
}
最终效果
/**
* 为某个订单锁定库存
* @param vo
* @return
*/
@Transactional
@Override
public boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单详情信息
* 追溯
*/
WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
wareOrderTaskEntity.setCreateTime(new Date());
wareOrderTaskService.save(wareOrderTaskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存
//2、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map((item) -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪个仓库有库存
List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIdList);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
for (Long wareId : wareIds) {
//锁定成功就返回1,失败就返回0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if (count == 1) {
skuStocked = true;
WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
.skuId(skuId)
.skuName("")
.skuNum(hasStock.getNum())
.taskId(wareOrderTaskEntity.getId())
.wareId(wareId)
.lockStatus(1)
.build();
wareOrderTaskDetailService.save(taskDetailEntity);
//TODO 告诉MQ库存锁定成功
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(wareOrderTaskEntity.getId());
StockDetailTo detailTo = new StockDetailTo();
BeanUtils.copyProperties(taskDetailEntity,detailTo);
lockedTo.setDetailTo(detailTo);
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部都是锁定成功的
return true;
}
依赖
<!--amqp高级消息队列协议,rabbitmq实现-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
主启动类添加注解
spring:
rabbitmq:
host: 192.168.75.129
port: 5672
# 虚拟主机
virtual-host: /
package com.xxxx.gulimall.ware.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class MyRabbitMQConfig {
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
// @RabbitListener(queues = "stock.release.stock.queue")
// public void handle(Message message) {
//
// }
/**
* 库存服务默认的交换机
* @return
*/
@Bean
public Exchange stockEventExchange() {
//String name, boolean durable, boolean autoDelete, Map arguments
TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
return topicExchange;
}
/**
* 普通队列
* @return
*/
@Bean
public Queue stockReleaseStockQueue() {
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
Queue queue = new Queue("stock.release.stock.queue", true, false, false);
return queue;
}
/**
* 延迟队列
* @return
*/
@Bean
public Queue stockDelay() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
return queue;
}
/**
* 交换机与普通队列绑定
* @return
*/
@Bean
public Binding stockLocked() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map arguments
Binding binding = new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
return binding;
}
/**
* 交换机与延迟队列绑定
* @return
*/
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}
第一种是业务失败,订单提交了,但是后续业务处理失败,解锁库存。
第二种是订单取消了,解锁库存
package com.xxxx.gulimall.ware.listener;
import com.rabbitmq.client.Channel;
import com.xxxx.common.to.OrderTo;
import com.xxxx.common.to.mq.StockLockedTo;
import com.xxxx.gulimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
/**
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
*
* 2、订单失败
* 库存锁定失败
*
* 只要解锁库存的消息失败,一定要告诉服务解锁失败
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
log.info("******收到解锁库存的信息******");
try {
//当前消息是否被第二次及以后(重新)派发过来了
// Boolean redelivered = message.getMessageProperties().getRedelivered();
//解锁库存
wareSkuService.unlockStock(to);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
log.info("******收到订单关闭,准备解锁库存的信息******");
try {
wareSkuService.unlockStock(orderTo);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
需要远程查询订单状态再调用解锁方法
远程接口
package com.xxxx.gulimall.ware.feign;
import com.xxxx.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("gulimall-order")
public interface OrderFeignService {
@GetMapping(value = "/order/order/status/{orderSn}")
R getOrderStatus(@PathVariable("orderSn") String orderSn);
}
/**
* 根据订单编号查询订单状态
* @param orderSn
* @return
*/
@GetMapping(value = "/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn) {
OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
}
/**
* 根据订单编号查询订单状态
* @param orderSn
* @return
*/
@GetMapping(value = "/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn) {
OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
}
/**
* 按照订单号获取订单信息
* @param orderSn
* @return
*/
@Override
public OrderEntity getOrderByOrderSn(String orderSn) {
OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
return orderEntity;
}
最终效果
@Override
public void unlockStock(StockLockedTo to) {
//库存工作单的id
StockDetailTo detail = to.getDetailTo();
Long detailId = detail.getId();
/**
* 解锁
* 1、查询数据库关于这个订单锁定库存信息
* 有:证明库存锁定成功了
* 解锁:订单状况
* 1、没有这个订单,必须解锁库存
* 2、有这个订单,不一定解锁库存
* 订单状态:已取消:解锁库存
* 已支付:不能解锁库存
*/
WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
if (taskDetailInfo != null) {
//查出wms_ware_order_task工作单的信息
Long id = to.getId();
WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
//获取订单号查询订单状态
String orderSn = orderTaskInfo.getOrderSn();
//远程查询订单信息
R orderData = orderFeignService.getOrderStatus(orderSn);
if (orderData.getCode() == 0) {
//订单数据返回成功
OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});
//判断订单状态是否已取消或者支付或者订单不存在
if (orderInfo == null || orderInfo.getStatus() == 4) {
//订单已被取消,才能解锁库存
if (taskDetailInfo.getLockStatus() == 1) {
//当前库存工作单详情状态1,已锁定,但是未解锁才可以解锁
unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
}
}
} else {
//消息拒绝以后重新放在队列里面,让别人继续消费解锁
//远程调用服务失败
throw new RuntimeException("远程调用服务失败");
}
} else {
//无需解锁
}
}
实现类
/**
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
* 导致卡顿的订单,永远都不能解锁库存
* @param orderTo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
//查一下最新的库存解锁状态,防止重复解锁库存
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
//按照工作单的id找到所有 没有解锁的库存,进行解锁
Long id = orderTaskEntity.getId();
List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
.eq("task_id", id).eq("lock_status", 1));
for (WareOrderTaskDetailEntity taskDetailEntity : list) {
unLockStock(taskDetailEntity.getSkuId(),
taskDetailEntity.getWareId(),
taskDetailEntity.getSkuNum(),
taskDetailEntity.getId());
}
}
最后是这两个解锁库存方式都调用的解锁方法
/**
* 解锁库存的方法
* @param skuId
* @param wareId
* @param num
* @param taskDetailId
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
//变为已解锁
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
sql
<update id="unLockStock">
UPDATE wms_ware_sku
SET stock_locked = stock_locked - #{num}
WHERE
sku_id = ${skuId}
AND ware_id = #{wareId}
</update>
回到提交订单的业务
当提交订单时库存锁定成功,给死信队列发消息,开始执行关闭订单的业务,删除购物车数据
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
// int i = 10/0;
//TODO 订单创建成功,发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
//删除购物车里的数据
redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
return responseVo;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
// responseVo.setCode(3);
// return responseVo;
}
监听普通队列的消息。当普通队列中监听到消息,说明死信队列订单过期,返还给普通队列来处理关闭订单的消息。
package com.xxxx.gulimall.order.listener;
import com.rabbitmq.client.Channel;
import com.xxxx.gulimall.order.entity.OrderEntity;
import com.xxxx.gulimall.order.service.OrderService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* @Description: 定时关闭订单
*
**/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
关闭订单
订单关闭时发送消息给库存服务的普通队列,让库存服务解锁库存。
/**
* 关闭订单
* @param orderEntity
*/
@Override
public void closeOrder(OrderEntity orderEntity) {
//关闭订单之前先查询一下数据库,判断此订单状态是否已支付
OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().
eq("order_sn",orderEntity.getOrderSn()));
if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
//代付款状态进行关单
OrderEntity orderUpdate = new OrderEntity();
orderUpdate.setId(orderInfo.getId());
orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(orderUpdate);
// 发送消息给MQ
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderInfo, orderTo);
try {
//TODO 确保每个消息发送成功,给每个消息做好日志记录,(给数据库保存每一个详细信息)保存每个消息的详细信息
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
} catch (Exception e) {
//TODO 定期扫描数据库,重新发送失败的消息
}
}