# 订单模块 # 一、资源整合   我们需要把相关的静态资源拷贝到nginx,然后动态模板文件拷贝到order项目的templates目录下,然后调整资源的路径。在网关中设置对应的路由即可。 # 二、整合SpringSession   结合官网,导入对应的依赖,然后添加对应的配置信息,redis配置信息,Cookie的配置一级域名和二级域名。 # 三、订单中心   订单中心涉及到的模块 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/50facb2b205e4e57a7759e89cc1b0b52.png) 订单的状态: 1. 待付款:提交订单,订单预下单 2. 已付款/待发货:完成支付,订单系统需要记录支付时间,支付流水号便于对账,订单下放到wms系统,仓库进行调拨,配货,分拣,出库等操作 3. 待收款/已发货:仓库将商品出库,订单进入物流环节 4. 已完成:用户确认收货,订单交易完成,后续支付侧进行结算,如果订单存在问题就进入售后状态 5. 已取消:付款之前取消订单。 6. 售后中:用户在付款后申请退款,或商家发货后用户申请退换货。 订单流程: ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/563690b8538a499fb75be45db0a16524.png) # 四、认证拦截   订单服务中的所有的请求都必须是在认证的状态下处理的,所有我们需要添加一个校验是否认证的拦截器 ```java package com.msb.mall.order.interceptor; import com.msb.common.constant.AuthConstant; import com.msb.common.vo.MemberVO; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class AuthInterceptor implements HandlerInterceptor { public static ThreadLocal threadLocal = new ThreadLocal(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 通过HttpSession获取当前登录的用户信息 HttpSession session = request.getSession(); Object attribute = session.getAttribute(AuthConstant.AUTH_SESSION_REDIS); if(attribute != null){ MemberVO memberVO = (MemberVO) attribute; threadLocal.set(memberVO); return true; } // 如果 attribute == null 说明没有登录,那么我们就需要重定向到登录页面 session.setAttribute(AuthConstant.AUTH_SESSION_MSG,"请先登录"); response.sendRedirect("http://auth.msb.com/login.html"); return false; } } ``` 然后注册该拦截器即可 ``` @Configuration public class MyWebInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**"); } } ``` # 五、订单确认页 ## 1.订单确认页VO抽取 ```java public class OrderConfirmVo { // 订单的收货人 及 收货地址信息 @Getter @Setter List address; // 购物车中选中的商品信息 @Getter @Setter List items; // 支付方式 // 发票信息 // 优惠信息 //Integer countNum; public Integer getCountNum(){ int count = 0; if(items != null){ for (OrderItemVo item : items) { count += item.getCount(); } } return count; } // BigDecimal total ;// 总的金额 public BigDecimal getTotal(){ BigDecimal sum = new BigDecimal(0); if(items != null ){ for (OrderItemVo item : items) { BigDecimal totalPrice = item.getPrice().multiply(new BigDecimal(item.getCount())); sum = sum.add(totalPrice); } } return sum; } // BigDecimal payTotal;// 需要支付的总金额 public BigDecimal getPayTotal(){ return getTotal(); } } ``` ## 2.确认页数据获取   通过Fegin远程调用对应的服务,获取会员的数据和购物车中的商品信息。 ```java @Override public OrderConfirmVo confirmOrder() { OrderConfirmVo vo = new OrderConfirmVo(); MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get(); // 获取到 RequestContextHolder 的相关信息 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); CompletableFuture future1 = CompletableFuture.runAsync(() -> { // 同步主线程中的 RequestContextHolder RequestContextHolder.setRequestAttributes(requestAttributes); // 1.查询当前登录用户对应的会员的地址信息 Long id = memberVO.getId(); List addresses = memberFeginService.getAddress(id); vo.setAddress(addresses); }, executor); CompletableFuture future2 = CompletableFuture.runAsync(() -> { RequestContextHolder.setRequestAttributes(requestAttributes); // 2.查询购物车中选中的商品信息 List userCartItems = cartFeginService.getUserCartItems(); vo.setItems(userCartItems); }, executor); try { CompletableFuture.allOf(future1,future2).get(); } catch (Exception e) { e.printStackTrace(); } // 3.计算订单的总金额和需要支付的总金额 VO自动计算 return vo; } ``` 在Fegin调用远程服务的时候会出现请求Header丢失的问题。 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/12fab57056b84296b2e370c07b8835d6.png) ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/abdf9be8827644fbabc0b8d84c291d74.png) 首先我们创建 `RequestInterceptor`的实现来绑定Header信息,同时在异步处理的时候我们需要从主线程中获取Request信息,然后绑定在子线程中。 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/03b15a2ea3754fab8c19d466bb6edeb1.png) 然后在订单确认页中渲染数据的展示 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/474288fa974c417c9dcddb34bcc53424.png) ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/c03fe7d9450b4abab9f8e9cca4863105.png) ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/af9331a66a104e39901173c8517707ad.png) 最后的页面效果 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/53b4910decb94a2eb86564168b127c07.png) # 六、接口幂等性处理 幂等性: 多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。 ## 1.天然的幂等行为 以SQL语句为例: ```sql select * from t_user where id = 1; update t_user set age = 18 where id = 2; delete from t_user where id = 1 insert into (userid,username)values(1,'波哥') ; # userid 唯一主键 ``` 不具备幂等行为的 ```SQL update t_user set age = age + 1 where id = 1; insert into (userid,username)values(1,'波哥'); # userid 不是主键 可以重复 ``` ## 2.**需要使用幂等的场景** 需要使用幂等的场景 : * 前端重复提交 * 接口超时重试 * 消息队列重复消费 ## 3.解决方案 1. **token机制** :①客户端请求获取token,服务端生成一个唯一ID作为token存在redis中;②客户端第二次请求时携带token,服务端校验token成功则执行业务操作并删除token,服务端校验token失败则表示重复操作。 2. **基于mysql** :①新建去重表;②服务端将客户端请求时提交的部分信息放入表中,其中有唯一索引字段;③成功插入则没有重复请求,插入失败则重复请求。 3. **基于redis** :①客户端请求服务端拿本次请求的标识字段;②服务端将标识字段以setnx方式存入redis并设置过期时间;③设置成功则说明非重复操作,设置失败则表示重复操作。 4. 状态机、悲观锁、乐观锁等。 # 七、提交订单 ## 1.防重提交   在订单提交的时候我们通过防重Token来保证请求的幂等性 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/e9b7179e6bdf4ad992183b461cfff5d4.png) ## 2.生成Token   我们在获取订单结算页数据的service中我们需要生成对应的Token,并且保存到Redis中同时绑定到页面。 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/87dedfea617d4ccab41db943c44c3a45.png) 页面中的处理 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/336759f08fe24878b085cb201d29c127.png) ## 3.提交订单   然后在提交订单的逻辑中我们先创建对应的VO ```java @Data public class OrderSubmitVO { // 收获地址的id private Long addrId; // 支付方式 private Integer payType; // 防重Token private String orderToken; // 买家备注 private String note; } ``` 然后在订单确认页中创建对应的form表单 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/14c2e5ea197944b493c88d15e604f0d1.png) 然后把数据提交到后端服务中。 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/bfc4432db59a455e8be85ffcc1abda52.png) ## 4.防重检查   订单数据提交到后端服务,我们在下订单前需要做防重提交的校验。 ```java try{ lock.lock();//加锁 String redisToken = redisTemplate.opsForValue().get(key); if(redisToken != null && redisToken.equals(vo.getOrderToken())){ // 表示是第一次提交 // 需要删除Token redisTemplate.delete(key); }else{ // 表示是重复提交 return responseVO; } }finally { lock.unlock(); //释放锁 } ``` 上面我们是通过Lock加锁的方式来实现Redis中的查询和删除操作的原子性,我们同时可以使用Redis中脚本来实现原子性处理。 ``` // 获取当前登录的用户信息 MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get(); // 1.验证是否重复提交 保证Redis中的token 的查询和删除是一个原子性操作 String key = OrderConstant.ORDER_TOKEN_PREFIX+":"+memberVO.getId(); String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0"; Long result = redisTemplate.execute(new DefaultRedisScript(script, Long.class) , Arrays.asList(key) , vo.getOrderToken()); if(result == 0){ // 表示验证失败 说明是重复提交 return responseVO; } ``` # 八、生成订单 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/1150ae4c71d94b92bfa1404921227016.png) 一个是需要生成订单信息一个是需要生成订单项信息。具体的核心代码为 ```java /** * 创建订单的方法 * @param vo * @return */ private OrderCreateTO createOrder(OrderSubmitVO vo) { OrderCreateTO createTO = new OrderCreateTO(); // 创建订单 OrderEntity orderEntity = buildOrder(vo); createTO.setOrderEntity(orderEntity); // 创建OrderItemEntity 订单项 List orderItemEntitys = buildOrderItems(orderEntity.getOrderSn()); createTO.setOrderItemEntitys(orderItemEntitys); return createTO; } /** * 通过购物车中选中的商品来创建对应的购物项信息 * @return */ private List buildOrderItems(String orderSN) { List orderItemEntitys = new ArrayList<>(); // 获取购物车中的商品信息 选中的 List userCartItems = cartFeginService.getUserCartItems(); if(userCartItems != null && userCartItems.size() > 0){ // 统一根据SKUID查询出对应的SPU的信息 List spuIds = new ArrayList<>(); for (OrderItemEntity orderItemEntity : orderItemEntitys) { if(!spuIds.contains(orderItemEntity.getSpuId())){ spuIds.add(orderItemEntity.getOrderId()); } } // 远程调用商品服务获取到对应的SPU信息 List spuInfos = productService.getOrderItemSpuInfoBySpuId((Long[]) spuIds.toArray()); Map map = spuInfos.stream().collect(Collectors.toMap(OrderItemSpuInfoVO::getId, item -> item)); for (OrderItemVo userCartItem : userCartItems) { // 获取到商品信息对应的 SPU信息 OrderItemSpuInfoVO spuInfo = map.get(userCartItem.getSpuId()); OrderItemEntity orderItemEntity = buildOrderItem(userCartItem,spuInfo); // 绑定对应的订单编号 orderItemEntity.setOrderSn(orderSN); orderItemEntitys.add(orderItemEntity); } } return orderItemEntitys; } /** * 根据一个购物车中的商品创建对应的 订单项 * @param userCartItem * @return */ private OrderItemEntity buildOrderItem(OrderItemVo userCartItem,OrderItemSpuInfoVO spuInfo) { OrderItemEntity entity = new OrderItemEntity(); // SKU信息 entity.setSkuId(userCartItem.getSkuId()); entity.setSkuName(userCartItem.getTitle()); entity.setSkuPic(userCartItem.getImage()); entity.setSkuQuantity(userCartItem.getCount()); List skuAttr = userCartItem.getSkuAttr(); String skuAttrStr = StringUtils.collectionToDelimitedString(skuAttr, ";"); entity.setSkuAttrsVals(skuAttrStr); // SPU信息 entity.setSpuId(spuInfo.getId()); entity.setSpuBrand(spuInfo.getBrandName()); entity.setCategoryId(spuInfo.getCatalogId()); entity.setSpuPic(spuInfo.getImg()); // 优惠信息 忽略 // 积分信息 entity.setGiftGrowth(userCartItem.getPrice().intValue()); entity.setGiftIntegration(userCartItem.getPrice().intValue()); return entity; } private OrderEntity buildOrder(OrderSubmitVO vo) { // 创建OrderEntity OrderEntity orderEntity = new OrderEntity(); // 创建订单编号 String orderSn = IdWorker.getTimeId(); orderEntity.setOrderSn(orderSn); MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get(); // 设置会员相关的信息 orderEntity.setMemberId(memberVO.getId()); orderEntity.setMemberUsername(memberVO.getUsername()); // 根据收获地址ID获取收获地址的详细信息 MemberAddressVo memberAddressVo = memberFeginService.getAddressById(vo.getAddrId()); orderEntity.setReceiverCity(memberAddressVo.getCity()); orderEntity.setReceiverDetailAddress(memberAddressVo.getDetailAddress()); orderEntity.setReceiverName(memberAddressVo.getName()); orderEntity.setReceiverPhone(memberAddressVo.getPhone()); orderEntity.setReceiverPostCode(memberAddressVo.getPostCode()); orderEntity.setReceiverRegion(memberAddressVo.getRegion()); orderEntity.setReceiverProvince(memberAddressVo.getProvince()); // 设置订单的状态 orderEntity.setStatus(OrderConstant.OrderStateEnum.FOR_THE_PAYMENT.getCode()); return orderEntity; } ``` 锁定库存的操作,需要操作ware仓储服务。 ```java /** * 锁定库存的操作 * @param vo * @return */ @Transactional @Override public Boolean orderLockStock(WareSkuLockVO vo) { List items = vo.getItems(); // 首先找到具有库存的仓库 List collect = items.stream().map(item -> { SkuWareHasStock skuWareHasStock = new SkuWareHasStock(); skuWareHasStock.setSkuId(item.getSkuId()); List wareSkuEntities = this.baseMapper.listHashStock(item.getSkuId()); skuWareHasStock.setWareSkuEntities(wareSkuEntities); skuWareHasStock.setNum(item.getCount()); return skuWareHasStock; }).collect(Collectors.toList()); // 尝试锁定库存 for (SkuWareHasStock skuWareHasStock : collect) { Long skuId = skuWareHasStock.getSkuId(); List wareSkuEntities = skuWareHasStock.wareSkuEntities; if(wareSkuEntities == null && wareSkuEntities.size() == 0){ // 当前商品没有库存了 throw new NoStockExecption(skuId); } // 当前需要锁定的商品的梳理 Integer count = skuWareHasStock.getNum(); Boolean skuStocked = false; // 表示当前SkuId的库存没有锁定完成 for (WareSkuEntity wareSkuEntity : wareSkuEntities) { // 循环获取到对应的 仓库,然后需要锁定库存 // 获取当前仓库能够锁定的库存数 Integer canStock = wareSkuEntity.getStock() - wareSkuEntity.getStockLocked(); if(count <= canStock){ // 表示当前的skuId的商品的数量小于等于需要锁定的数量 Integer i = this.baseMapper.lockSkuStock(skuId,wareSkuEntity.getWareId(),count); count = 0; skuStocked = true; }else{ // 需要锁定的库存大于 可以锁定的库存 就按照已有的库存来锁定 Integer i = this.baseMapper.lockSkuStock(skuId,wareSkuEntity.getWareId(),canStock); count = count - canStock; } if(count <= 0 ){ // 表示所有的商品都锁定了 break; } } if(count > 0){ // 说明库存没有锁定完 throw new NoStockExecption(skuId); } if(skuStocked == false){ // 表示上一个商品的没有锁定库存成功 throw new NoStockExecption(skuId); } } return true; } ``` 没有库存或者锁定库存失败我们通过自定义的异常抛出 ```java /** * 自定义异常:锁定库存失败的情况下产生的异常信 */ public class NoStockExecption extends RuntimeException{ private Long skuId; public NoStockExecption(Long skuId){ super("当前商品["+skuId+"]没有库存了"); this.skuId = skuId; } public Long getSkuId() { return skuId; } public void setSkuId(Long skuId) { this.skuId = skuId; } } ``` 如果下订单操作成功(订单数据和订单项数据)我们就会操作锁库存的行为 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/afe0338ac81c4328aa67e1065ccc7318.png) 锁定库存失败通过抛异常来使订单操作回滚 ![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/b19db8f073bd4f07be7652ac167ef026.png)