订单服务的实现流程(确认订单->提交订单->支付)
1、整合SpringSession
使用SpringSession的目的是来解决分布式session不同步不共享的问题,其实就是为了让登录信息在订单微服务里共享
注意:由于这里使用springsession的用的类型是redis,所以这springsession和redis都要一起加入依赖和配置
(1)导入依赖
<!-- 整合springsession 来解决分布式session不同步不共享的问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- 整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)在application.properties配置文件里配置springsession
#配置springsession
spring.session.store-type=redis
server.servlet.session.timeout=30m
#配置redis的ip地址
spring.redis.host=192.168.241.128
(3)在config配置中加入springSession配置类
package com.saodai.saodaimall.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;
/**
* springSession配置类(所有要使用session的服务的session配置要一致)
*/
@Configuration
public class GulimallSessionConfig {
/**
* 配置session(主要是为了放大session作用域)
* @return
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("saodaimall.com");
cookieSerializer.setCookieName("SAODAISESSION");
return cookieSerializer;
}
/**
* 配置Session放到redis存储的格式为json(其实就是json序列化)
* @return
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
(4)在启动类上添加@EnableRedisHttpSession注解
package com.saodai.saodaimall.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* 订单服务启动类
*/
@EnableFeignClients
@EnableRedisHttpSession
@EnableDiscoveryClient
@SpringBootApplication
public class SaodaimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SaodaimallOrderApplication.class, args);
}
}
2、增加登录拦截器
(1)点击去结算后会去订单详情确认页面,这个时候需要用户登录才可以去结算
package com.saodai.saodaimall.order.interceptor;
import com.saodai.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 static com.saodai.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 {
//未登录,返回登录页面
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.saodaimall.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 {
}
}
(2)一定要记得在SpringMVC的配置文件里注册拦截器,不然拦截器不会生效
package com.saodai.saodaimall.order.config;
import com.saodai.saodaimall.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;
/**
* springmvc配置类
**/
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
3、根据订单业务需求抽取模型
(1)订单确认页类OrderConfirmVo(也就是订单确认页需要用的数据)
package com.saodai.saodaimall.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();
}
}
(2)订单项类OrderItemVo
package com.saodai.saodaimall.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 Boolean hasStock;
/** 商品重量 **/
private BigDecimal weight = new BigDecimal("0.085");
}
(3)用户收货信息类MemberAddressVo
package com.saodai.saodaimall.order.vo;
import lombok.Data;
/**
* 用户订单的地址
**/
@Data
public class MemberAddressVo {
/**
* 地址id
*/
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;
}
4、点击去结算按钮(确认订单)
(1)订单服务的web的OrderWebController控制器来处理/toTrade请求
@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";
}
(2)处理/toTrade请求的confirmOrder方法具体实现
流程:
1、远程调用会员服务来查询所有的收获地址列表
2、远程调用购物车服务来查询购物车所有选中的购物项
3、远程调用库存服务来批量查询所有商品的库存是否有货
4、查询用户积分
5、生成一个防重令牌并放到redis中(防止表单重复提交),这格令牌是会过期的,过期时间为30分钟(注意是结算的时候服务器生成一个防重令牌,然后把这个令牌隐藏在订单页面,点击去结算只是生成这个令牌并存到redis中和订单页面上,下面的点击提交订单按钮才会做校验令牌,保证原子性,防刷这些操作)
格式为key:order:token:+用户id,value:防重令牌
/**
* 去结算确认页时封装订单确认页返回需要用的数据
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//获取当前用户登录的信息(直接获取)
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//获取当前线程请求头信息(用于解决Feign异步调用丢失上下文的问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
/**开启第一个异步任务来远程查询所有的收获地址列表**/
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题)
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收获地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
/**开启第二个异步任务来远程查询购物车所有选中的购物项**/
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题)
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);
//SkuStockVo就是下面的SkuHasStockVo类
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、价格数据由OrderConfirmVo的getTotal方法自动计算
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
//防重令牌一个放到redis里 USER_ORDER_TOKEN_PREFIX = "order:token"
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
//防重令牌一个放到前台页面(不过隐藏了)
confirmVo.setOrderToken(token);
//阻塞异步线程,只有两个异步都完成了才可以进行下一步
CompletableFuture.allOf(addressFuture,cartInfoFuture).get();
return confirmVo;
}
package com.saodai.common.vo;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
*会员信息
**/
@ToString
@Data
public class MemberResponseVo implements Serializable {
private static final long serialVersionUID = 5573669251256409786L;
private Long id;
/**
* 会员等级id
*/
private Long levelId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 手机号码
*/
private String mobile;
/**
* 邮箱
*/
private String email;
/**
* 头像
*/
private String header;
/**
* 性别
*/
private Integer gender;
/**
* 生日
*/
private Date birth;
/**
* 所在城市
*/
private String city;
/**
* 职业
*/
private String job;
/**
* 个性签名
*/
private String sign;
/**
* 用户来源
*/
private Integer sourceType;
/**
* 积分
*/
private Integer integration;
/**
* 成长值
*/
private Integer growth;
/**
* 启用状态
*/
private Integer status;
/**
* 注册时间
*/
private Date createTime;
/**
* 社交登录用户的ID
*/
private String socialId;
/**
* 社交登录用户的名称
*/
private String socialName;
/**
* 社交登录用户的自我介绍
*/
private String socialBio;
}
1>远程调用会员服务来查询所有的收获地址列表
@Autowired
private MemberReceiveAddressService memberReceiveAddressService;
/**
* 根据会员id查询会员的所有地址(用于获取订单时远程的查询地址)
* @param memberId
* @return
*/
@GetMapping(value = "/{memberId}/address")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
List<MemberReceiveAddressEntity> addressList = memberReceiveAddressService.getAddress(memberId);
return addressList;
}
//根据会员id查询会员的所有地址(用于获取订单时远程的查询地址)
@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
//MemberReceiveAddressEntity对象就是上面的用户收货信息类MemberAddressVo
List<MemberReceiveAddressEntity> addressList = this.baseMapper.selectList
(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
return addressList;
}
2>远程调用购物车服务来查询购物车所有选中的购物项
package com.saodai.saodaimall.order.feign;
import com.saodai.saodaimall.order.vo.OrderItemVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
/**
* 远程调用购物车服务
**/
@FeignClient("saodaimall-cart")
public interface CartFeignService {
/**
* 查询当前用户购物车选中的商品项
* @return
*/
@GetMapping(value = "/currentUserCartItems")
List<OrderItemVo> getCurrentCartItems();
}
@Resource
private CartService cartService;
/**
* 获取当前用户的购物车商品项(订单生成需要查询用户购物车中选择的购物项)
* @return
*/
@GetMapping(value = "/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentCartItems() {
List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
return cartItemVoList;
}
查询购物车所有选中的购物项分流程:
1、获取当前用户登录的信息来判断登录没
2、组装Redis中的HashMap结构的Hash值,用来绑定Hash操作(这里的购物车和购物项的数据都是存到Reids缓存里的,格式为Hash值:saodaimall:cart:10290038,key:39,value:CartItemVo对象的String类型,其中10290038是用户id,39是skuId)
3、调用getCartItems方法来把所有的value值取出来并都封装成CartItemVo对象(注意每一个Hash值对应一个用户的购物车,所以其中的key和value合在一起表示每个购物项)
4、远程调用商品服务来查询每个商品的最新价格(由于redis缓存中的数据可能不是最新的数据)
/**
* 获取当前用户的购物车商品项(订单生成需要查询用户购物车中选择的购物项)
* @return
*/
@Override
public List<CartItemVo> getUserCartItems() {
List<CartItemVo> cartItemVoList = new ArrayList<>();
//获取当前用户登录的信息
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
//如果用户未登录直接返回null
// userInfoTo.getUserId()== null是因为拦截器判断用户登录了就设置其userId
// 没有登录就设置userKey,所以不能像以前那样直接就用userInfoTo==null来判断
if (userInfoTo.getUserId() == null) {
return null;
} else {
//获取购物车项 String CART_PREFIX = "saodaimall:cart:"是Hash值
String cartKey = CART_PREFIX + userInfoTo.getUserId();
//把Hash值传给getCartItems来获取购物车的所有购物项
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems == null) {
throw new CartExceptionHandler();
}
//由于redis缓存中的数据可能不是最新的数据,所以要远程在查一次价格
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 redis中的外围map的key值
* @return
*/
private List<CartItemVo> getCartItems(String cartKey) {
//获取购物车里面的所有商品
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
//注意这里从Reids中取出来的是Object类型
List<Object> values = operations.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
//这里要转为String类型是因为reids中取出来的是Obhect类型
String str = (String) obj;
//在把String转为CartItemVo对象
CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemVoStream;
}
return null;
}
@Autowired
private SkuInfoService skuInfoService;
/**
* 根据skuId查询当前商品的价格(订单生成需要查询用户购物车中选择的购物项时的最新价格)
* @param skuId
* @return
*/
@GetMapping(value = "/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
//获取当前商品的信息(skuInfoService.getById()这个方法是代码构造器自动生成的代码里有的)
SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
//获取商品的价格
BigDecimal price = skuInfo.getPrice();
return price;
}
3>远程调用库存服务来批量查询所有商品的库存是否有货
/**
* 查询sku是否有库存(商品服务的saveSpuInfoImpl中up方法要用到的,去结算确认页时批量查询有货没也要用到)
* @return
*/
@PostMapping("/hasstock")
public R getSkusHasStock(@RequestBody List<Long> skuIds){
List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
return R.ok().setData(vos);
}
//查询sku是否有库存(saveSpuInfo中up方法要用到的)
@Override
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
List<SkuHasStockVo> skuHasStockVos = skuIds.stream().map(skuId -> {
SkuHasStockVo vo = new SkuHasStockVo();
//查询当前sku的总库存量
Long count = baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count==null?false:true);
return vo;
}).collect(Collectors.toList());
return skuHasStockVos;
}
<!-- 查询当前sku的总库存量-->
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id = #{skuId}
</select>
package com.saodai.saodaimall.ware.vo;
import lombok.Data;
/**
*查询sku是否有库存的封装类
**/
@Data
public class SkuHasStockVo {
private Long skuId;
//有没有库存
private Boolean hasStock;
}
5、订单确认页面的渲染
(1)收货人信息渲染
<p class="p1">填写并核对订单信息 <span style="color: red" th:if="${msg}!=null" > <b>提交订单失败!失败原因是:[[${msg}]]</b></span> </p>
<div class="section">
<!--收货人信息-->
<div class="top-2">
<span>收货人信息</span>
<span>新增收货地址</span>
</div>
<!--地址-->
<div class="top-3 addr-item" th:each="addr:${confirmOrderData.memberAddressVos}">
<p th:attr="def=${addr.defaultStatus},addrId=${addr.id}">[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.city}]] [[${addr.region}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
</div>
<p class="p2">更多地址︾</p>
<div class="hh1"/>
</div>
(2)购物项信息的渲染
<!--图片-->
<div class="yun1" th:each="item:${confirmOrderData.items}">
<img th:src="${item.image}" class="yun"/>
<div class="mi">
<p>[[${item.title}]]<span style="color: red;" th:text="'¥' + ${#numbers.formatDecimal(item.price,3,2)}"> ¥ 499.00</span>
<span> [[${item.count}]] </span> <span> [[${confirmOrderData.stocks[item.skuId]?"有货":"无货"}]]</span>
</p>
<p><span>[[${item.weight}]]kg</span></p>
<p class="tui-1"><img src="/static/order/confirm/img/i_07.png"/>支持7天无理由退货</p>
</div>
</div>
(3)结算区域的渲染
<div class="xia">
<div class="qian">
<p class="qian_y">
<span>[[${confirmOrderData.count}]]</span>
<span>件商品,总商品金额:</span>
<span class="rmb">¥[[${#numbers.formatDecimal(confirmOrderData.total, 1, 2)}]]</span>
</p>
<p class="qian_y">
<span>返现:</span>
<span class="rmb"> -¥0.00</span>
</p>
<p class="qian_y">
<span>运费: </span>
<span class="rmb">   ¥<b id="fare"></b></span>
</p>
<p class="qian_y">
<span>服务费: </span>
<span class="rmb">   ¥0.00</span>
</p>
<p class="qian_y">
<span>退换无忧: </span>
<span class="rmb">   ¥0.00</span>
</p>
</div>
<div class="yfze">
<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥<b id="payPrice">[[${#numbers.formatDecimal(confirmOrderData.payPrice, 1, 2)}]]</b></span>
</p>
<p class="yfze_b">寄送至: <span id="receiveAddress"></span> 收货人:<span id="receiver"></span></p>
</div>
<form action="http://order.saodaimall.com/submitOrder" method="post">
<input id="addrInput" type="hidden" name="addrId" />
<input id="payPriceInput" type="hidden" name="payPrice">
<input name="orderToken" th:value="${confirmOrderData.orderToken}" type="hidden"/>
<button class="tijiao" type="submit" >提交订单</button>
</form>
</div>
6、运费的模拟
(1)直接从confirm.html页面的js代码发的请求去查运费
//查运费
function getFare(addrId) {
//给表单回填选择的地址
$("#addrInput").val(addrId);
$.get("http://saodaimall.com/api/ware/wareinfo/fare?addrId=" + addrId, function (resp) {
console.log(resp);
$("#fare").text(resp.data.fare);
var total = [[${confirmOrderData.total}]];
var payPrice = total * 1 + resp.data.fare * 1;
//设置运费
$("#payPrice").text(payPrice);
$("#payPriceInput").val(payPrice);
//设置收获地址人信息
$("#receiveAddress").text(resp.data.address.province + " " + resp.data.address.city + " " + " " + resp.data.address.region + resp.data.address.detailAddress);
$("#receiver").text(resp.data.address.name);
})
}
(2)这个请求由库存服务的WareInfoController控制器来处理
/**
* 获取运费信息(订单结算界面时)
* @return
*/
@GetMapping(value = "/fare")
public R getFare(@RequestParam("addrId") Long addrId) {
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
分流程:
1、远程调用会员服务的MemberReceiveAddressController控制器来查找用户收货信息MemberAddressVo对象
2、截取用户手机号码最后一位作运费计算
/**
* 获取运费信息(订单结算界面时)
* @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() - 1, phone.length());
BigDecimal bigDecimal = new BigDecimal(fare);
fareVo.setFare(bigDecimal);
fareVo.setAddress(memberAddressVo);
return fareVo;
}
return null;
}
package com.saodai.saodaimall.ware.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
*运费vo类
**/
@Data
public class FareVo {
//用户信息
private MemberAddressVo address;
//运费
private BigDecimal fare;
}
package com.saodai.saodaimall.ware.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;
}
/**
* 根据地址id找到地址信息
*/
@RequestMapping("/info/{id}")
//@RequiresPermissions("member:memberreceiveaddress:info")
public R info(@PathVariable("id") Long id){
MemberReceiveAddressEntity memberReceiveAddress = memberReceiveAddressService.getById(id);
return R.ok().put("memberReceiveAddress", memberReceiveAddress);
}
7、提交订单(点击提交订单按钮,下单)
(1)订单服务的web的OrderWebController控制器来处理/submitOrder请求
/**
* 下单功能(提交订单)
* @param vo
* OrderSubmitVo对象是确认订单页面的部分数据,自动封装好的,因为后面提交订单需要这些数据
* @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.saodaimall.com/toTrade";
}
} catch (Exception e) {
if (e instanceof NoStockException) {
String message = ((NoStockException)e).getMessage();
attributes.addFlashAttribute("msg",message);
}
return "redirect:http://order.saodaimall.com/toTrade";
}
}
OrderSubmitVo对象是确认订单页面的部分数据,自动封装好的,因为后面提交订单需要这些数据
package com.saodai.saodaimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 封装订单提交数据的vo
**/
@Data
public class OrderSubmitVo {
/** 收获地址的id **/
private Long addrId;
/** 支付方式 **/
private Integer payType;
//无需提交要购买的商品,去购物车再获取一遍
//优惠、发票
/** 防重令牌 **/
private String orderToken;
/** 应付价格 **/
private BigDecimal payPrice;
/** 订单备注 **/
private String remarks;
//用户相关的信息,直接去session中取出即可
}
(2)OrderServiceImpl的submitOrder方法的具体实现
注意:这个submitOrder方法方法非常复杂,可以从对象的封装来一步步理解
/**
* 获取运费信息(订单结算界面时)
* @return
*/
@GetMapping(value = "/fare")
public R getFare(@RequestParam("addrId") Long addrId) {
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
@Autowired
private MemberFeignService memberFeignService;
@Autowired
private CartFeignService cartFeignService;
@Autowired
private WmsFeignService wmsFeignService;
@Autowired
private ProductFeignService productFeignService;
@Autowired
private OrderItemService orderItemService;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 创建订单(下单)
* // @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别
* // @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播级别
*
*/
@Transactional(rollbackFor = Exception.class)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//把OrderSubmitVo对象放到本地线程,方便后面共享
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//去创建、下订单、验令牌、验价格、锁定库存...
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//所在默认状态码为0表示没有异常
responseVo.setCode(0);
/**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类型的0或1,0表示令牌验证失败,1表示成功)
//Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId())表示存到redis里的值
// orderToken是页面传过来的令牌(表示当前需要验证的值)
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功
/**2、创建订单、订单项等信息(抽取成createOrder方法)**/
OrderCreateTo order = createOrder();
//后端计算出的价格
BigDecimal payAmount = order.getOrder().getPayAmount();
//前端界面传来的价格
BigDecimal payPrice = vo.getPayPrice();
//只要这两个的差值小于0.01都是可以接受的
/**3、验证前端界面传来的价格和计算后的价格是否相同(验价)**/
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
/**4、保存订单信息**/
saveOrder(order);
//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);
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
// int i = 10/0;
//这里使用消息队列是用于处理超过指定时间还未支付的订单(也就是把这些订单给取消)
/**
* 订单创建成功,发送消息给MQ交换机order-event-exchange,交换机根据路由键order.create.order把消息放到延时队列order.delay.queue
* 当消息在延时队列里等待了指定的时间后还没有被消费就会再次由交换机路由到队列order.release.order
* 这个设置是在MyRabbitMQConfig中指定的 arguments.put("x-dead-letter-routing-key", "order.release.order");
*/
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);
/**
* Code 1: "令牌订单信息过期,请刷新再次提交";
* Code 2: "订单商品价格发生变化,请确认后再次提交";
* Code 3: "商品库存不足";
*/
responseVo.setCode(3);
return responseVo;
}
} else {
responseVo.setCode(2);
return responseVo;
}
}
}
/**
* 创建订单
*/
private OrderCreateTo createOrder() {
OrderCreateTo createTo = new OrderCreateTo();
//1、生成订单号(IdWorker.getTimeId()是jdk提供的生成订单订单号的方法)
String orderSn = IdWorker.getTimeId();
//订单构造(将订单构造抽取为builderOrder方法)
OrderEntity orderEntity = builderOrder(orderSn);
//2、获取到所有的订单项(将所有订单项构造抽取为builderOrderItems方法)
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());
/**通过线程共享数据来获取地址id信息**/
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());
//设置订单相关的状态信息 CREATE_NEW(0,"待付款")
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
//设置自动确认时间(天)
orderEntity.setAutoConfirmDay(7);
//设置确认收货状态[0->未确认;1->已确认]
orderEntity.setConfirmStatus(0);
return orderEntity;
}
/**
* 构建所有订单项数据
* @param orderSn 订单号
* @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;
}
/**
* 构建某一个订单项的数据
* @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());
//设置商品sku图片
orderItemEntity.setSkuPic(items.getImage());
//设置商品sku价格
orderItemEntity.setSkuPrice(items.getPrice());
//商品购买的数量
orderItemEntity.setSkuQuantity(items.getCount());
//使用StringUtils.collectionToDelimitedString将list集合转换为String
String skuAttrValues = StringUtils.collectionToDelimitedString(items.getSkuAttrValues(), ";");
//设置商品销售属性组合(JSON)
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);
//当前订单项的实际金额.总额 - 各种优惠价格
//原来的价格 orderItemEntity.getSkuQuantity()是获取商品购买的数量
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;
}
/**
* 保存订单所有数据
* @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);
}
/**
* 计算价格的方法
*/
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);
}
分流程(submitOrder方法是封装SubmitOrderResponseVo对象):
package com.saodai.saodaimall.order.vo;
import com.saodai.saodaimall.order.entity.OrderEntity;
import lombok.Data;
/**
* 下单后返回的数据封装类
**/
@Data
public class SubmitOrderResponseVo {
//下单成功的订单信息
private OrderEntity order;
/** 错误状态码 **/
private Integer code;
}
1、把OrderSubmitVo对象(前台传过来已经封装好的)放到本地线程,方便后面共享
package com.saodai.saodaimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 封装订单提交数据的vo
**/
@Data
public class OrderSubmitVo {
/** 收获地址的id **/
private Long addrId;
/** 支付方式 **/
private Integer payType;
//无需提交要购买的商品,去购物车再获取一遍
//优惠、发票
/** 防重令牌 **/
private String orderToken;
/** 应付价格 **/
private BigDecimal payPrice;
/** 订单备注 **/
private String remarks;
//用户相关的信息,直接去session中取出即可
}
2、验证令牌是否合法【令牌的对比和删除必须保证原子性】
3、创建订单(订单和订单项都有对应的数据库表)–>(封装OrderCreateTo对象)
package com.saodai.saodaimall.order.to;
import com.saodai.saodaimall.order.entity.OrderEntity;
import com.saodai.saodaimall.order.entity.OrderItemEntity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*生成的订单类(组合类型的订单)
**/
@Data
public class OrderCreateTo {
//订单实体类
private OrderEntity order;
//订单项
private List<OrderItemEntity> orderItems;
/** 订单计算的应付价格 **/
private BigDecimal payPrice;
/** 运费 **/
private BigDecimal fare;
}
i、IdWorker.getTimeId()是jdk提供的生成订单订单号的方法
ii、通过builderOrder方法来构造订单(
封装OrderEntity对象
)
package com.saodai.saodaimall.order.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 订单实体类
*/
@Data
@TableName("oms_order")
public class OrderEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 订单号
*/
private String orderSn;
/**
* 使用的优惠券
*/
private Long couponId;
/**
* create_time
*/
private Date createTime;
/**
* 用户名
*/
private String memberUsername;
/**
* 订单总额
*/
private BigDecimal totalAmount;
/**
* 应付总额
*/
private BigDecimal payAmount;
/**
* 运费金额
*/
private BigDecimal freightAmount;
/**
* 促销优化金额(促销价、满减、阶梯价)
*/
private BigDecimal promotionAmount;
/**
* 积分抵扣金额
*/
private BigDecimal integrationAmount;
/**
* 优惠券抵扣金额
*/
private BigDecimal couponAmount;
/**
* 后台调整订单使用的折扣金额
*/
private BigDecimal discountAmount;
/**
* 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
*/
private Integer payType;
/**
* 订单来源[0->PC订单;1->app订单]
*/
private Integer sourceType;
/**
* 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
*/
private Integer status;
/**
* 物流公司(配送方式)
*/
private String deliveryCompany;
/**
* 物流单号
*/
private String deliverySn;
/**
* 自动确认时间(天)
*/
private Integer autoConfirmDay;
/**
* 可以获得的积分
*/
private Integer integration;
/**
* 可以获得的成长值
*/
private Integer growth;
/**
* 发票类型[0->不开发票;1->电子发票;2->纸质发票]
*/
private Integer billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 收票人电话
*/
private String billReceiverPhone;
/**
* 收票人邮箱
*/
private String billReceiverEmail;
/**
* 收货人姓名
*/
private String receiverName;
/**
* 收货人电话
*/
private String receiverPhone;
/**
* 收货人邮编
*/
private String receiverPostCode;
/**
* 省份/直辖市
*/
private String receiverProvince;
/**
* 城市
*/
private String receiverCity;
/**
* 区
*/
private String receiverRegion;
/**
* 详细地址
*/
private String receiverDetailAddress;
/**
* 订单备注
*/
private String note;
/**
* 确认收货状态[0->未确认;1->已确认]
*/
private Integer confirmStatus;
/**
* 删除状态【0->未删除;1->已删除】
*/
private Integer deleteStatus;
/**
* 下单时使用的积分
*/
private Integer useIntegration;
/**
* 支付时间
*/
private Date paymentTime;
/**
* 发货时间
*/
private Date deliveryTime;
/**
* 确认收货时间
*/
private Date receiveTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 修改时间
*/
private Date modifyTime;
/**
* 订单项
*/
@TableField(exist = false)
private List<OrderItemEntity> orderItemEntityList;
}
/**
* 构建订单数据
* @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());
/**通过线程共享数据来获取地址id信息**/
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());
//设置订单相关的状态信息 CREATE_NEW(0,"待付款")
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
//设置自动确认时间(天)
orderEntity.setAutoConfirmDay(7);
//设置确认收货状态[0->未确认;1->已确认]
orderEntity.setConfirmStatus(0);
return orderEntity;
}
-
-
- 通过拦截器中线程共享数据来获取当前用户登录信息并封装到订单对象 OrderEntity
- 通过线程共享数据来获取前台提交过来的orderSubmitVo对象数据
- 通过AddrId地址Id来远程库存服务获取收货地址和运费信息来封装到订单对象OrderEntity
- 设置订单相关的状态信息为待付款
- 设置自动确认时间(天)和设置确认收货状态[0->未确认;1->已确认]
-
iii、通过builderOrderItems方法来构造所有订单项(
封装 List<OrderItemEntity>对象,指的所有订单项
)
package com.saodai.saodaimall.order.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 订单项信息
*/
@Data
@TableName("oms_order_item")
public class OrderItemEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* order_id
*/
private Long orderId;
/**
* order_sn
*/
private String orderSn;
/**
* spu_id
*/
private Long spuId;
/**
* spu_name
*/
private String spuName;
/**
* spu_pic
*/
private String spuPic;
/**
* 品牌
*/
private String spuBrand;
/**
* 商品分类id
*/
private Long categoryId;
/**
* 商品sku编号
*/
private Long skuId;
/**
* 商品sku名字
*/
private String skuName;
/**
* 商品sku图片
*/
private String skuPic;
/**
* 商品sku价格
*/
private BigDecimal skuPrice;
/**
* 商品购买的数量
*/
private Integer skuQuantity;
/**
* 商品销售属性组合(JSON)
*/
private String skuAttrsVals;
/**
* 商品促销分解金额
*/
private BigDecimal promotionAmount;
/**
* 优惠券优惠分解金额
*/
private BigDecimal couponAmount;
/**
* 积分优惠分解金额
*/
private BigDecimal integrationAmount;
/**
* 该商品经过优惠后的分解金额
*/
private BigDecimal realAmount;
/**
* 赠送积分
*/
private Integer giftIntegration;
/**
* 赠送成长值
*/
private Integer giftGrowth;
}
/**
* 构建所有订单项数据
* @param orderSn 订单号
* @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;
}
/**
* 构建某一个订单项的数据
* @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());
//设置商品sku图片
orderItemEntity.setSkuPic(items.getImage());
//设置商品sku价格
orderItemEntity.setSkuPrice(items.getPrice());
//商品购买的数量
orderItemEntity.setSkuQuantity(items.getCount());
//使用StringUtils.collectionToDelimitedString将list集合转换为String
String skuAttrValues = StringUtils.collectionToDelimitedString(items.getSkuAttrValues(), ";");
//设置商品销售属性组合(JSON)
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);
//当前订单项的实际金额.总额 - 各种优惠价格
//原来的价格 orderItemEntity.getSkuQuantity()是获取商品购买的数量
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;
}
-
-
- 远程调用购物车服务来获取购物车中的所有购物项
- 遍历通过builderOrderItem方法构造订单项的数据(封装OrderItemEntity对象,指的单个订单项)
-
-
-
-
- 远程调用商品服务来获取spu的信息
- 设置商品的sku信息
- 使用StringUtils.collectionToDelimitedString将list集合转换为String
- 设置商品的积分信息
- 设置订单项的价格信息
-
-
iv、计算订单各种价格(总价,优惠价,积分价等等)
/**
* 计算价格的方法
*/
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);
}
4、验证前端界面传来的价格和计算后的价格是否相同(验价,只要小于两个的差值0.01都可以接受)
5、保存订单信息
1>保存订单
2>保存订单项
/**
* 保存订单所有数据
* @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);
}
6、调用库存服务来进行锁库存(如果有异常要进行回滚,但是这里的回滚不是加个@Transactional可以实现的)
i、封装传给库存服务的WareSkuLockVo对象
package com.saodai.saodaimall.order.vo;
import lombok.Data;
import java.util.List;
/**
* 锁定库存的vo
**/
@Data
public class WareSkuLockVo {
//订单号
private String orderSn;
/** 需要锁住的所有库存信息 **/
private List<OrderItemVo> locks;
}
package com.saodai.saodaimall.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 Boolean hasStock;
/** 商品重量 **/
private BigDecimal weight = new BigDecimal("0.085");
}
ii、远程调用功库存服务来锁库存
/**
* 锁定库存
* @param vo
*
* 库存解锁的场景
* 1)、下订单成功,订单过期没有支付被系统自动取消或者被用户手动取消,都要解锁库存
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
*
*
* @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) {
// NO_STOCK_EXCEPTION(21000,"商品库存不足")
return R.error(NO_STOCK_EXCEPTION.getCode(),NO_STOCK_EXCEPTION.getMessage());
}
}
/**
* 为某个订单锁定库存
* @param vo
* @return
*/
@Transactional(rollbackFor = Exception.class)
@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 (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);
/**告诉RabbitMQ库存锁定成功**/
StockLockedTo lockedTo = new StockLockedTo();
//设置库存工作单的id(其实就是订单锁库存工作单的id号)
lockedTo.setId(wareOrderTaskEntity.getId());
StockDetailTo detailTo = new StockDetailTo();
BeanUtils.copyProperties(taskDetailEntity,detailTo);
lockedTo.setDetailTo(detailTo);
/**
* 把消息发给RabbitMQ队列的stock-event-exchange交换机
* 延时后的消息会被交换机stock-event-exchange根据路由键stock.release
* (这个是MyRabbitMQConfig配置中的stockDelay方法设置的)发送到队列stock.release.stock.queue
* 特别注意:只需要给交换机指定的路由键就可以路由到对应的队列,前提是要先在配置里设置好绑定关系
*
*/
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部都是锁定成功的
return true;
}
-
- 封装WareOrderTaskEntity对象,用于后面异常时的数据回溯(表示我准备要给哪个订单锁库存了,对应数据库wms_ware_order_task表)
package com.saodai.saodaimall.ware.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 订单锁库存工作单(表示我准备要给哪个订单锁库存了)
*/
@Data
@TableName("wms_ware_order_task")
public class WareOrderTaskEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* order_id
*/
private Long orderId;
/**
* order_sn
*/
private String orderSn;
/**
* 收货人
*/
private String consignee;
/**
* 收货人电话
*/
private String consigneeTel;
/**
* 配送地址
*/
private String deliveryAddress;
/**
* 订单备注
*/
private String orderComment;
/**
* 付款方式【 1:在线付款 2:货到付款】
*/
private Integer paymentWay;
/**
* 任务状态
*/
private Integer taskStatus;
/**
* 订单描述
*/
private String orderBody;
/**
* 物流单号
*/
private String trackingNo;
/**
* create_time
*/
private Date createTime;
/**
* 仓库id
*/
private Long wareId;
/**
* 工作单备注
*/
private String taskComment;
}
-
- 找到每个商品在哪个仓库都有库存(封装 List<SkuWareHasStock>对象)
/**锁库存的内部类**/
@Data
class SkuWareHasStock {
//商品id
private Long skuId;
//商品数量
private Integer num;
//商品所在仓库的id
private List<Long> wareId;
}
<!-- 查询这个商品在哪个仓库有库存-->
<select id="listWareIdHasSkuStock" resultType="java.lang.Long">
SELECT
ware_id
FROM
wms_ware_sku
WHERE
sku_id = #{skuId}
AND stock - stock_locked > 0
</select>
-
- 锁定库存(修改需要锁库存的商品的锁库存数量和封装 WareOrderTaskDetailEntity对象)
-
-
- 遍历前面封装的 List<SkuWareHasStock>对象
- 设置判断锁库存是否成功的标志位(只要有一个仓库锁定成功就可)
- 如果 List<SkuWareHasStock>对象不为空就遍历每个库存仓库,调用lockSkuStock方法来锁库存
-
-
- 锁库存成功后把消息发给RabbitMQ队列
<!-- 锁定库存-->
<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>
package com.saodai.saodaimall.ware.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 库存工作单详情(具体给订单的哪个商品锁库存)
*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private Long wareId;
/**
* 锁定状态
*/
private Integer lockStatus;
}
package com.saodai.common.to.mq;
import lombok.Data;
/**
* 发送到mq消息队列的to
**/
@Data
public class StockLockedTo {
/** 库存工作单的id **/
private Long id;
/** 工作单详情的所有信息 StockDetailTo对象内容就是上面的WareOrderTaskDetailEntity **/
private StockDetailTo detailTo;
}
7、解库存
锁库存成功后就会发消息给stock-event-exchange交换机,交换机根据路由键stock.locked把消息路由到stock.delay.queue延时队列(跟上面一样,这个延时队列的消息不会被消费掉),时间过期后就把消息根据路由键stock.release路由到stock.release.stock.queue队列,然后这个消息队列的消息是被一个专门解库存的监听器来监听(注意这里有两种解库存的监听方法,一个是自动解库存的监听,一个是订单服务的订单取消后立马解库存的监听)
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
package com.saodai.saodaimall.ware.listener;
import com.rabbitmq.client.Channel;
import com.saodai.common.to.OrderTo;
import com.saodai.common.to.mq.StockLockedTo;
import com.saodai.saodaimall.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;
/**
* RabbitMQ的监听器
* 这里有两个监听方法,这两个监听识别的依据是看传入的是StockLockedTo还是OrderTo
* 一个是监听的库存自动解锁
* 一个是监听订单取消后库存解锁
*/
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
/**
** 监听库存自动解锁
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
log.info("******收到解锁库存的信息******");
try {
System.out.println("******收到解锁库存的信息******");
//当前消息是否被第二次及以后(重新)派发过来了
// Boolean redelivered = message.getMessageProperties().getRedelivered();
//解锁库存
wareSkuService.unlockStock(to);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/**
*
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
*导致卡顿的订单,永远都不能解锁库存
* 订单释放直接和库存释放进行绑定
* @param orderTo
* @param message
* @param channel
* @throws IOException
*/
@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.saodai.common.to.mq;
import lombok.Data;
/**
* 发送到mq消息队列的to
**/
@Data
public class StockLockedTo {
/** 库存工作单的id **/
private Long id;
/** 工作单详情的所有信息 StockDetailTo对象内容就是上面的WareOrderTaskDetailEntity **/
private StockDetailTo detailTo;
}
解锁库存的思路
首先查询数据库的库存详细工作单表看看有没有成功锁定库存(如果成功锁库存了会有对应的一条记录),如果没有那就说明库存没有锁成功,那自然就不需要解锁了
- 库存详细工作单表有这条记录那就证明库存锁定成功了
-
- 具体需不需要解库存还要先看订单状态
-
-
- 先查询有没有这个订单,没有这个订单必须解锁库存(可能出现因为有异常造成的数据回滚导致订单不存在的情况,但是库存锁成功了)
- 有这个订单,不一定解锁库存,要根据订单的状态来决定是否解库存
-
-
-
-
- 订单状态是已取消状态,说明是用户没有支付订单过期了,那就必须解锁库存
- 订单状态是已支付状态,说明是用户支付成功了,那就不能解锁库存
-
-
-
-
- 除了判断上面的情况,还有考虑当前库存详细工作单的状态,只有满足订单状态是已取消状态并且是已锁定的状态那才可以解库存
-
-
-
-
- 已锁定:解锁库存
- 已解锁 :不能再解锁
-
-
/**
* (这个方法是由StockReleaseListener监听器调用的)
* 锁库存失败后的自动解锁(也就是回溯)
* @param to
*/
@Override
public void unlockStock(StockLockedTo to) {
//获取库存详细工作单类
StockDetailTo detail = to.getDetailTo();
//库存详细工作单的id
Long detailId = detail.getId();
//WareOrderTaskDetailEntity是库存详细工作单类
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>() {});
/**
* CREATE_NEW(0,"待付款"),
* PAYED(1,"已付款"),
* SENDED(2,"已发货"),
* RECIEVED(3,"已完成"),
* CANCLED(4,"已取消"),
* SERVICING(5,"售后中"),
* SERVICED(6,"售后完成");
*/
//订单不存在(因为有异常造成的数据回滚导致订单不存在)或者订单状态是取消状态(orderInfo.getStatus() == 4)才可解库存
if (orderInfo == null || orderInfo.getStatus() == 4) {
//当前库存工作单详情状态1,已锁定,只有当前库存工作单详情状态未解锁才可以解锁
if (taskDetailInfo.getLockStatus() == 1) {
//调用真正接库存的方法unLockStock
unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
}
}
} else {
//消息拒绝以后重新放在队列里面,让别人继续消费解锁
//远程调用服务失败
throw new RuntimeException("远程调用服务失败");
}
} else {
//无需解锁
}
}
/**
* 真正解锁库存的方法(自动解库存)
* @param skuId 需要解锁库存的商品id
* @param wareId 需要解锁库存的库存仓库id
* @param num 需要解锁库存的商品数量
* @param taskDetailId 库存工作单详情id
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
//setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
/**
* 订单取消了就立马解库存
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
* 导致卡顿的订单,永远都不能解锁库存
* @param orderTo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
//查一下最新的库存解锁状态,防止重复解锁库存
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
//按照工作单的id找到所有 没有解锁的库存,进行解锁(lock_status=1表示已锁定库存)
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());
}
}
自动解库存
/**
* (这个方法是由StockReleaseListener监听器调用的)
* 锁库存失败后的自动解锁(也就是回溯)
* @param to
*/
@Override
public void unlockStock(StockLockedTo to) {
//获取库存详细工作单类
StockDetailTo detail = to.getDetailTo();
//库存详细工作单的id
Long detailId = detail.getId();
//WareOrderTaskDetailEntity是库存详细工作单类
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>() {});
/**
* CREATE_NEW(0,"待付款"),
* PAYED(1,"已付款"),
* SENDED(2,"已发货"),
* RECIEVED(3,"已完成"),
* CANCLED(4,"已取消"),
* SERVICING(5,"售后中"),
* SERVICED(6,"售后完成");
*/
//订单不存在(因为有异常造成的数据回滚导致订单不存在)或者订单状态是取消状态(orderInfo.getStatus() == 4)才可解库存
if (orderInfo == null || orderInfo.getStatus() == 4) {
//当前库存工作单详情状态1,已锁定,只有当前库存工作单详情状态未解锁才可以解锁
if (taskDetailInfo.getLockStatus() == 1) {
//调用真正接库存的方法unLockStock
unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
}
}
} else {
//消息拒绝以后重新放在队列里面,让别人继续消费解锁
//远程调用服务失败
throw new RuntimeException("远程调用服务失败");
}
} else {
//无需解锁
}
}
/**
* 真正解锁库存的方法(自动解库存)
* @param skuId 需要解锁库存的商品id
* @param wareId 需要解锁库存的库存仓库id
* @param num 需要解锁库存的商品数量
* @param taskDetailId 库存工作单详情id
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
//setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
-
自动解库存的具体实现流程
-
- 获取库存详细工作单的id
package com.saodai.common.to.mq;
import lombok.Data;
/**
* 发送到mq消息队列的to
**/
@Data
public class StockLockedTo {
/** 库存工作单的id **/
private Long id;
/** 工作单详情的所有信息 **/
private StockDetailTo detailTo;
}
package com.saodai.common.to.mq;
import lombok.Data;
/**
* 其实就是库存工作单详情实体类(具体给订单的哪个商品锁库存)
**/
@Data
public class StockDetailTo {
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private Long wareId;
/**
* 锁定状态
*/
private Integer lockStatus;
}
-
- 查询数据库有没有这个库存详细工作单类
package com.saodai.saodaimall.ware.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 库存工作单详情(具体给订单的哪个商品锁库存)
*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private Long wareId;
/**
* 锁定状态
*/
private Integer lockStatus;
}
-
- 查询订单锁库存工作单(获取哪个订单要锁库存)
package com.saodai.saodaimall.ware.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 订单锁库存工作单(表示我准备要给哪个订单锁库存了)
*/
@Data
@TableName("wms_ware_order_task")
public class WareOrderTaskEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* order_id
*/
private Long orderId;
/**
* order_sn
*/
private String orderSn;
/**
* 收货人
*/
private String consignee;
/**
* 收货人电话
*/
private String consigneeTel;
/**
* 配送地址
*/
private String deliveryAddress;
/**
* 订单备注
*/
private String orderComment;
/**
* 付款方式【 1:在线付款 2:货到付款】
*/
private Integer paymentWay;
/**
* 任务状态
*/
private Integer taskStatus;
/**
* 订单描述
*/
private String orderBody;
/**
* 物流单号
*/
private String trackingNo;
/**
* create_time
*/
private Date createTime;
/**
* 仓库id
*/
private Long wareId;
/**
* 工作单备注
*/
private String taskComment;
}
-
- 根据订单号远程查询订单
package com.saodai.saodaimall.ware.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class OrderVo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 订单号
*/
private String orderSn;
/**
* 使用的优惠券
*/
private Long couponId;
/**
* create_time
*/
private Date createTime;
/**
* 用户名
*/
private String memberUsername;
/**
* 订单总额
*/
private BigDecimal totalAmount;
/**
* 应付总额
*/
private BigDecimal payAmount;
/**
* 运费金额
*/
private BigDecimal freightAmount;
/**
* 促销优化金额(促销价、满减、阶梯价)
*/
private BigDecimal promotionAmount;
/**
* 积分抵扣金额
*/
private BigDecimal integrationAmount;
/**
* 优惠券抵扣金额
*/
private BigDecimal couponAmount;
/**
* 后台调整订单使用的折扣金额
*/
private BigDecimal discountAmount;
/**
* 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
*/
private Integer payType;
/**
* 订单来源[0->PC订单;1->app订单]
*/
private Integer sourceType;
/**
* 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
*/
private Integer status;
/**
* 物流公司(配送方式)
*/
private String deliveryCompany;
/**
* 物流单号
*/
private String deliverySn;
/**
* 自动确认时间(天)
*/
private Integer autoConfirmDay;
/**
* 可以获得的积分
*/
private Integer integration;
/**
* 可以获得的成长值
*/
private Integer growth;
/**
* 发票类型[0->不开发票;1->电子发票;2->纸质发票]
*/
private Integer billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 收票人电话
*/
private String billReceiverPhone;
/**
* 收票人邮箱
*/
private String billReceiverEmail;
/**
* 收货人姓名
*/
private String receiverName;
/**
* 收货人电话
*/
private String receiverPhone;
/**
* 收货人邮编
*/
private String receiverPostCode;
/**
* 省份/直辖市
*/
private String receiverProvince;
/**
* 城市
*/
private String receiverCity;
/**
* 区
*/
private String receiverRegion;
/**
* 详细地址
*/
private String receiverDetailAddress;
/**
* 订单备注
*/
private String note;
/**
* 确认收货状态[0->未确认;1->已确认]
*/
private Integer confirmStatus;
/**
* 删除状态【0->未删除;1->已删除】
*/
private Integer deleteStatus;
/**
* 下单时使用的积分
*/
private Integer useIntegration;
/**
* 支付时间
*/
private Date paymentTime;
/**
* 发货时间
*/
private Date deliveryTime;
/**
* 确认收货时间
*/
private Date receiveTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 修改时间
*/
private Date modifyTime;
}
-
- 进行双重判断
-
-
- 先判断订单不存在(因为有异常造成的数据回滚导致订单不存在)或者订单状态是取消状态
- 在判断当前库存工作单详情状态是不是1,1表示已锁定,只有当前库存工作单详情状态未解锁才可以解锁
-
-
- 调用unLockStock方法实现真正的解库存(自动解库存)
-
-
- 更新库存的数量(还原)
- 更新工作单的状态为已解锁
-
/**
* 真正解锁库存的方法(自动解库存)
* @param skuId 需要解锁库存的商品id
* @param wareId 需要解锁库存的库存仓库id
* @param num 需要解锁库存的商品数量
* @param taskDetailId 库存工作单详情id
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
//setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
<!-- 解锁库存-->
<update id="unLockStock">
UPDATE wms_ware_sku
SET stock_locked = stock_locked - #{num}
WHERE
sku_id = ${skuId}
AND ware_id = #{wareId}
</update>
手动解库存
-
订单服务的订单取消后立马解库存的具体逻辑
-
- 首先通过订单号查询订单锁库存工作单
- 通过订单锁库存工作单的id去库存详细工作单去找对应的锁库存的记录,看有没有记录并且锁库存的状态是已锁定的状态,防止多次重复解库存(其中库存详细工作单中的工作id的值就是订单锁库存工作单的id的值)
- 最后调用真正的解库存方法来解库存
/**
* 真正解锁库存的方法(自动解库存)
* @param skuId 需要解锁库存的商品id
* @param wareId 需要解锁库存的库存仓库id
* @param num 需要解锁库存的商品数量
* @param taskDetailId 库存工作单详情id
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
//setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
/**
* 订单取消了就立马解库存
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
* 导致卡顿的订单,永远都不能解锁库存
* @param orderTo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
//查一下最新的库存解锁状态,防止重复解锁库存
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
//按照工作单的id找到所有 没有解锁的库存,进行解锁(lock_status=1表示已锁定库存)
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());
}
}