订单服务-----功能实现逻辑

article/2025/7/13 15:05:38

订单服务的实现流程(确认订单->提交订单->支付)

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*/@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();//放大作用域cookieSerializer.setDomainName("saodaimall.com");cookieSerializer.setCookieName("SAODAISESSION");return cookieSerializer;}/*** 配置Session放到redis存储的格式为json(其实就是json序列化)* @return*/@Beanpublic 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<>();@Overridepublic 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;}}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic 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 {@Autowiredprivate LoginUserInterceptor loginUserInterceptor;@Overridepublic 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 @Setterprivate String orderToken;@Getter @SetterMap<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请求

@Autowiredprivate 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*/@Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {//构建OrderConfirmVoOrderConfirmVo 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();//获取全部商品的idList<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集合转换为mapMap<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>远程调用会员服务来查询所有的收获地址列表

  @Autowiredprivate 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查询会员的所有地址(用于获取订单时远程的查询地址)@Overridepublic List<MemberReceiveAddressEntity> getAddress(Long memberId) {//MemberReceiveAddressEntity对象就是上面的用户收货信息类MemberAddressVoList<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();}@Resourceprivate CartService cartService;/*** 获取当前用户的购物车商品项(订单生成需要查询用户购物车中选择的购物项)* @return*/@GetMapping(value = "/currentUserCartItems")@ResponseBodypublic 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*/@Overridepublic 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;}@Autowiredprivate 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方法要用到的)@Overridepublic 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"> &nbsp ¥<b id="fare"></b></span></p><p class="qian_y"><span>服务费: </span><span class="rmb"> &nbsp ¥0.00</span></p><p class="qian_y"><span>退换无忧: </span><span class="rmb"> &nbsp ¥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*/@Overridepublic 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();//截取用户手机号码最后一位作为我们的运费计算//1558022051String 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<>();@Autowiredprivate MemberFeignService memberFeignService;@Autowiredprivate CartFeignService cartFeignService;@Autowiredprivate WmsFeignService wmsFeignService;@Autowiredprivate ProductFeignService productFeignService;@Autowiredprivate OrderItemService orderItemService;@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate RabbitTemplate rabbitTemplate; /***  创建订单(下单)* // @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别* // @Transactional(propagation = Propagation.REQUIRED)   设置事务的传播级别**/@Transactional(rollbackFor = Exception.class)@Overridepublic 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集合转换为StringString 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*/@TableIdprivate 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*/@TableIdprivate 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集合转换为StringString 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)@Overridepublic 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*/@TableIdprivate 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>对象)
   /**锁库存的内部类**/@Dataclass SkuWareHasStock {//商品idprivate Long skuId;//商品数量private Integer num;//商品所在仓库的idprivate List<Long> wareId;}
<!--    查询这个商品在哪个仓库有库存-->
<select id="listWareIdHasSkuStock" resultType="java.lang.Long">SELECTware_idFROMwms_ware_skuWHEREsku_id = #{skuId}AND stock - stock_locked > 0
</select>
    • 锁定库存(修改需要锁库存的商品的锁库存数量和封装 WareOrderTaskDetailEntity对象)
      • 遍历前面封装的 List<SkuWareHasStock>对象
      • 设置判断锁库存是否成功的标志位(只要有一个仓库锁定成功就可)
      • 如果 List<SkuWareHasStock>对象不为空就遍历每个库存仓库,调用lockSkuStock方法来锁库存
    • 锁库存成功后把消息发给RabbitMQ队列
<!--    锁定库存--><update id="lockSkuStock">UPDATE wms_ware_skuSET stock_locked = stock_locked + #{num}WHEREsku_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*/@TableIdprivate 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 {@Autowiredprivate WareSkuService wareSkuService;/**** 监听库存自动解锁*/@RabbitHandlerpublic 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*/@RabbitHandlerpublic 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*/@Overridepublic void unlockStock(StockLockedTo to) {//获取库存详细工作单类StockDetailTo detail = to.getDetailTo();//库存详细工作单的idLong 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) {//调用真正接库存的方法unLockStockunLockStock(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)@Overridepublic 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*/@Overridepublic void unlockStock(StockLockedTo to) {//获取库存详细工作单类StockDetailTo detail = to.getDetailTo();//库存详细工作单的idLong 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) {//调用真正接库存的方法unLockStockunLockStock(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*/@TableIdprivate 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*/@TableIdprivate 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_skuSET stock_locked = stock_locked - #{num}WHEREsku_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)@Overridepublic 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());}}


http://chatgpt.dhexx.cn/article/XL51p86Z.shtml

相关文章

SMBMS订单管理系统(手把手教细致讲解实现全过程) (七)

实现用户管功能 刚刚访问密码直接走前端 现在要发起请求走Servlet&#xff0c;Servlet处理后返回前端页面 Servlet 处理请求调用业务返回页面 业务要查询用户列表&#xff0c;查询角色列表&#xff0c;为了实现分页&#xff0c;需查询pageSize总数。查询从Service层到Dao层…

【电商开发手册】订单-下单

下单需求 所谓下单&#xff0c;本质上就是买卖双方通过确认一系列信息并且签订电子合同的过程 在电商平台的下单过程中&#xff0c;也需要确定买卖双方的一系列信息&#xff1a; 买方&#xff1a;用户确认收货地址、支付方式、配送方式等等 卖方&#xff1a;卖方需要进行供…

如何规范业务管理过程?低代码平台助力订单管理系统建设

编者按&#xff1a;本文介绍了订单管理系统的概念以及作用&#xff0c;并进一步展现了低代码平台是如何为企业实现订单管理科学化&#xff0c;规范业务管理过程的。 关键词&#xff1a;老厂商&#xff0c;流程管理&#xff0c;订单管理 什么是订单管理系统 订单管理系统(OMS)…

【网课平台】Day13.订单支付模式:生成支付二维码与查询支付

文章目录 一、需求&#xff1a;生成支付二维码1、需求分析2、表设计3、接口定义4、接口实现5、完善controller 二、需求&#xff1a;查询支付结果1、需求分析2、表设计与模型类3、接口定义4、接口实现步骤一&#xff1a;查询支付结果步骤二&#xff1a;保存支付结果&#xff08…

简单的订单系统

目录 一、数据库方面 二、jdbc配置文件 三、JDBC工具类 三、Users类 四、功能实现类 五、运行结果 一、数据库方面 USE foodmenu; DROP TABLE IF EXISTS menu7; CREATE TABLE menu7(num INT PRIMARY KEY,TypeDishes VARCHAR(255))CHARSETutf8; INSERT INTO menu7(num,Type…

天翎低代码平台实现的订单管理系统

编者按&#xff1a; 本文 主要介绍了基于天翎低代码平台实现的 订单管理系统以及 优势 &#xff0c;并进一步展现了低代码平台是如何为企业实现订单管理科学化 和 规范业务管理过程。 关键词&#xff1a;低代码平台、订单管理系统 1.订单管理系统是什么&#xff1f; 订单管理系…

初学订单-支付流程(思路)

主要说的是 生成订单的一系列操作 生成订单号---确认支付---生成支付链接--支付流程 支付流程 ---1.获取支付链接 1.1 三方接口&#xff0c;发送数据 ----1.2 返回数据解析&#xff08;包含支付订单id&#xff09;将链接也返回前端 ----2.进行支付 2.1 扫码支付 2.2 支付成…

订单系统的代码实现

面向接口的编程&#xff1a; 面向接口编程(Interface Oriented Programming:OIP)是一种编程思想&#xff0c;接口作为实体抽象出来的一种表现形式&#xff0c;用于抽离内部实现进行外部沟通&#xff0c;最终实现内部变动而不影响外部与其他实现交互&#xff0c;可以理解成按照这…

【码学堂】教师如何在码学堂上组织教学活动?

码学堂简介 码学堂是由贵州师范学院数学与大数据学院研发的智慧教学平台&#xff0c;学生可以自主练习&#xff0c;教师可以组织练习、考试、竞赛、共享题库、共享教学资源&#xff0c;支持判断题、单项选择题、多项选择题、填空题、程序函数题、程序填空题、编程题、主观题8种…

如何在码学堂组织练习、考试、竞赛?

组织练习、考试、竞赛时就是将多个题目组成题目集&#xff0c;然后加入学生组完成。题目集是由多个题目构成的集合&#xff0c;可以理解为组卷、出卷&#xff0c;码学堂上“练习/作业”、“考试”或“竞赛”操作方式一致&#xff0c;故下面以考试为例来说明操作方法。 1 设置题…

如何开发出一款直播APP项目实践篇 -【原理篇】

【 主要模块】 主播端&#xff1a; 把主播实时录制的视频&#xff0c;经过&#xff08;采集、美颜处理、编码&#xff09;推送到服务器服务器&#xff1a; 处理&#xff08;转码、录制、截图、鉴黄&#xff09;后分发给用户播放端播放器&#xff1a; 获取服务器地址&#xff0…

短视频小视频直播app开发定制解决方案

一、直播APP的市场前景 随着智能移动手机端的普及,人们对于线上的娱乐的要求越发感兴趣,很多互联网电商平台也将直播APP作为销售的主战场之一。将线上与线下的方式相结合才能更好的促进企业的发展。当然对于直播APP的开发也是我们需要了解的。相关数据表明,目前直播APP对于…

直播APP开发过程

直播是2016年火爆的产业&#xff0c;看起来很炫&#xff0c;玩起来很方便、很贴近生活&#xff0c;开发一款直播App不仅耗时还非常昂贵&#xff0c;那么&#xff0c;开发一款直播App到底分几步走&#xff1f; 第一步&#xff1a;分解直播App的功能&#xff0c;我们以X客为例 1…

直播app开发必备五步流程

直播app开发搭建是最近几年比较火的技术&#xff0c;本文从技术角度分析一套直播app开发必备的几个流程。 从主播录制视频开始到最后直播间播放&#xff0c;涉及到的流程包括&#xff1a; 音视频采集—>编码和封装—>推流到流媒体服务器—>流媒体服务器播流分发—&g…

金融直播APP方案开发

分享一下英唐众创开发的金融直播APP解决方案。随着视频直播风靡全球&#xff0c;视频直播已成为众多传统行业和互联网行业争夺的“香饽饽”。金融行业当然也不例外&#xff0c;在当今“互联网”的大时代下&#xff0c;金融行业作为走在前沿的产业&#xff0c;不但开辟出互联网金…

如何开发出一款仿映客直播APP项目实践篇 -【原理篇】

前言&#xff1a;每个成功者多是站在巨人的肩膀上&#xff01;在做直播开发时 碰到了很多问题&#xff0c;在收集了许多人博客的基础上做出来了成功的直播项目并做了整理&#xff0c;并在最后奉上我的全部代码。 其中采用博客的博主开篇在此感谢&#xff0c;本着开源分享的精神…

cmd的炫酷玩法教程

在我们看电影的时候&#xff0c;经常看到黑客在电脑是一顿猛如虎的操作。然后电脑上就出现一系列花里胡哨的画面&#xff0c;其实那种画面我们用cmd的一行代码就能搞定。 第一步 按WinR&#xff0c;输入cmd&#xff0c;打开小黑框。 第二部 如果什么属性都不设置&#xff…

一行代码让你伪装成黑客惊艳世人

今天给大家带来一行代码让你伪装成黑客惊艳世人&#xff0c;保证让你成为学校机房最亮的崽 新建一个文本文档&#xff0c;输入tree c: CtrlS保存 重命名修改后缀名为.bat 这就OK了&#xff0c;不知道这个代码你有没有学废了&#xff01;

小bat大装逼(▼へ▼メ)

直接上代码 echo off cls color echo come!!! color 1a color 2b color 3c color 4d color 5e color 6f color 70 tree d: dir /s %0把代码粘贴到一个【文件名.bat】文件中&#xff0c;例如 复制粘贴完成&#xff0c;别忘记【Ctrls】进行保存操作啊。 然后打开就行了。很疯狂…

使用cmd命令行装逼,让命令行滚动起来

使用cmd命令行装逼&#xff0c;让命令行滚动起来 一、滚动cmd二、清理垃圾总结 一、滚动cmd color a扫描当前所有目录 dir /s二、清理垃圾 创建txt文件 echo offdel/f/s/q %systemdrive%\*.tmp del/f/s/q %systemdrive%\*._mp del/f/s/q %systemdrive%\*.logdel/f/s/q %sys…