谷粒商场学习笔记(15.订单模块)

目录

一、环境搭建

1、前端配置

hosts添加域名映射
在这里插入图片描述
nginx动静分离
在这里插入图片描述
在这里插入图片描述
修改前端连接地址
在这里插入图片描述
修改每个页面的登录上显示栏
在这里插入图片描述
首页跳转
在这里插入图片描述

2、后端环境

1.启动环境

在这里插入图片描述

2.nacos,redis等默认配置

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.Spring session配置

复制之前的session配置
在这里插入图片描述

4.线程池配置

复制之前项目的配置
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、订单需求分析

电商系统涉及到 3 流, 分别时信息流, 资金流, 物流, 而订单系统作为中枢将三者有机的集合起来。订单模块是电商系统的枢纽, 在订单这个环节上需求获取多个模块的数据和信息, 同时对这些信息进行加工处理后流向下个环节, 这一系列就构成了订单的信息流通。

1、订单构成

在这里插入图片描述

1.用户信息

用户信息包括用户账号、 用户等级、 用户的收货地址、 收货人、 收货人电话等组成, 用户账户需要绑定手机号码, 但是用户绑定的手机号码不一定是收货信息上的电话。 用户可以添加多个收货信息, 用户等级信息可以用来和促销系统进行匹配, 获取商品折扣, 同时用户等级还可以获取积分的奖励等

2.订单基础信息

订单基础信息是订单流转的核心, 其包括订单类型、 父/子订单、 订单编号、 订单状态、 订单流转的时间等。

(1) 订单类型包括实体商品订单和虚拟订单商品等, 这个根据商城商品和服务类型进行区分。
(2) 同时订单都需要做父子订单处理, 之前在初创公司一直只有一个订单, 没有做父子订单处理后期需要进行拆单的时候就比较麻烦, 尤其是多商户商场, 和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
(3) 订单编号不多说了, 需要强调的一点是父子订单都需要有订单编号, 需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
(4) 订单状态记录订单每次流转过程, 后面会对订单状态进行单独的说明。
(5) 订单流转时间需要记录下单时间, 支付时间, 发货时间, 结束时间/关闭时间等等

3. 商品信息

商品信息从商品库中获取商品的 SKU 信息、 图片、 名称、 属性规格、 商品单价、 商户信息等, 从用户下单行为记录的用户下单数量, 商品合计价格等。

4.优惠信息

优惠信息记录用户参与的优惠活动, 包括优惠促销活动, 比如满减、 满赠、 秒杀等, 用户使用的优惠券信息, 优惠券满足条件的优惠券需要默认展示出来, 具体方式已在之前的优惠券篇章做过详细介绍, 另外还虚拟币抵扣信息等进行记录。

为什么把优惠信息单独拿出来而不放在支付信息里面呢?

因为优惠信息只是记录用户使用的条目, 而支付信息需要加入数据进行计算, 所以做为区分。

5.支付信息

( 1) 支付流水单号, 这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号, 财务通过订单号和流水单号与支付通道进行对账使用。
( 2) 支付方式用户使用的支付方式, 比如微信支付、 支付宝支付、 钱包支付、 快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。
( 3) 商品总金额, 每个商品加总后的金额; 运费, 物流产生的费用; 优惠总金额, 包括促销活动的优惠金额, 优惠券优惠金额, 虚拟积分或者虚拟币抵扣的金额, 会员折扣的金额等之和; 实付金额, 用户实际需要付款的金额。用户实付金额=商品总金额+运费-优惠总金额

6.物流信息

物流信息包括配送方式, 物流公司, 物流单号, 物流状态, 物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。

2、订单状态

1.待付款
用户提交订单后, 订单进行预下单, 目前主流电商网站都会唤起支付, 便于用户快速完成支付, 需要注意的是待付款状态下可以对库存进行锁定, 锁定库存需要配置支付超时时间, 超时后将自动取消订单, 订单变更关闭状态。

2.已付款/待发货
用户完成订单支付, 订单系统需要记录支付时间, 支付流水单号便于对账, 订单下放到 WMS系统, 仓库进行调拨, 配货, 分拣, 出库等操作。

3.待收货/已发货
仓储将商品出库后, 订单进入物流环节, 订单系统需要同步物流信息, 便于用户实时知悉物品物流状态

4.已完成
用户确认收货后, 订单交易完成。 后续支付侧进行结算, 如果订单存在问题进入售后状态

5.已取消
付款之前取消订单。 包括超时未付款或用户商户取消订单都会产生这种订单状态。

6.售后中
用户在付款后申请退款, 或商家发货后用户申请退换货。售后也同样存在各种状态, 当发起售后申请后生成售后订单, 售后订单状态为待审核, 等待商家审核, 商家审核通过后订单状态变更为待退货, 等待用户将商品寄回, 商家收货后订单状态更新为待退款状态, 退款到用户原账户后订单状态更新为售后成功。

3、订单流程

订单流程是指从订单产生到完成整个流转的过程, 从而行程了一套标准流程规则。 而不同的产品类型或业务类型在系统中的流程会千差万别, 比如上面提到的线上实物订单和虚拟订单的流程, 线上实物订单与 O2O 订单等, 所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程, 对应的场景就是购买商品和退换货流程, 正向流程就是一个正常的网购步骤: 订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后, 订单是如何在多系统之间交互流转的, 可概括如下图:
在这里插入图片描述

1.订单创建与支付 (重点)

1.订单创建前需要预览订单, 选择收货信息等
2.订单创建需要锁定库存, 库存有才可创建, 否则不能创建
3.订单创建后超时未支付需要解锁库存
4.支付成功后, 需要进行拆单, 根据商品打包方式, 所在仓库, 物流等进行拆单
5.支付的每笔流水都需要记录, 以待查账
6.订单创建, 支付成功等状态都需要给 MQ 发送消息, 方便其他系统感知订阅

2.逆向流程

1.修改订单, 用户没有提交订单, 可以对订单一些信息进行修改, 比如配送信息,优惠信息, 及其他一些订单可修改范围的内容, 此时只需对数据进行变更即可。

2.订单取消, 用户主动取消订单和用户超时未支付, 两种情况下订单都会取消订单, 而超时情况是系统自动关闭订单, 所以在订单支付的响应机制上面要做支付的限时处理, 尤其是在前面说的下单减库存的情形下面, 可以保证快速的释放库存。另外需要需要处理的是促销优惠中使用的优惠券, 权益等视平台规则, 进行相应补回给用户。

3.退款, 在待发货订单状态下取消订单时, 分为缺货退款和用户申请退款。 如果是全部退款则订单更新为关闭状态, 若只是做部分退款则订单仍需进行进行, 同时生成一条退款的售后订单, 走退款流程。 退款金额需原路返回用户的账户。

4.发货后的退款, 发生在仓储货物配送, 在配送过程中商品遗失, 用户拒收, 用户收货后对商品不满意, 这样情况下用户发起退款的售后诉求后, 需要商户进行退款的审核, 双方达成一致后, 系统更新退款状态, 对订单进行退款操作, 金额原路返回用户的账户, 同时关闭原订单数据。 仅退款情况下暂不考虑仓库系统变化。 如果发生双方协调不一致情况下, 可以申请平台客服介入。 在退款订单商户不处理的情况下, 系统需要做限期判断, 比如 5 天商户不处理, 退款单自动变更同意退款。

三、订单登录拦截

需求:去结算、查看订单必须是登录用户之后的,这里编写一个拦截器。

用户登录 放行
用户未登录:跳转到登录页面

1、controller跳转类

@Controller
public class OrderWebController {

    @GetMapping("/toTrade")
    public String toTrade(){
        return "confirm";
    }
}

修改前端:
在这里插入图片描述

2、登录拦截器,HandlerInterceptor

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
//登录的ThreadLocal,从session中获得的登录信息会放到这里面
//以后都通过这个ThreadLocal获取登录信息
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
 
    /**
     * 用户登录拦截器
     * @param request
     * @param response
     * @param handler
     * @return 
     *      用户登录:放行
     *      用户未登录:跳转到登录页面
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//直接放行的页面
        String uri = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/order/order/status/**", uri);
        boolean match1 = antPathMatcher.match("/payed/notify", uri);
        if (match || match1) {
            return true;
        }
//不直接放行的页面,检测登录状态
        //获取登录的用户信息
        MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
 
        if (attribute != null) {
            //把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);
 
            return true;
        } else {
            //未登录,返回登录页面
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
            // session.setAttribute("msg", "请先进行登录");
            // response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}

3、WebMvcConfigurer 配置类添加拦截器

@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
 
    @Autowired
    LoginUserInterceptor interceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

四、订单确认页

1、vo类抽取

1.用户信息vo

@Data
public class MemberAddressVo {
    /**
     * id
     */
    @TableId
    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;
}

2.订单页面返回数据vo

@Data
public class OrderConfirmVo {

    /**
     * 收货地址,ums_member_receive_address 表
     */
    List<MemberAddressVo> addressVos;

    /**
     * 所有选中的购物车项
     */
    List<OrderItemVo> items;

    // 发票记录。。。

    /**
     * 优惠券信息
     */
    Integer integration;

    /**
     * 订单总额
     */
    BigDecimal total;

    /**
     * 应付价格
     */
    BigDecimal pryPrice;
}

3.购物车项vo

@Data
public class OrderItemVo {
    /**
     * 商品Id
     */
    private Long skuId;
    /**
     * 商品标题
     */
    private String title;
    /**
     * 商品图片
     */
    private String image;
    /**
     * 商品套餐信
     */
    private List<String> skuAttr;
    /**
     * 商品价格
     */
    private BigDecimal price;
    /**
     * 数量
     */
    private Integer count;
    /**
     * 小计价格
     */
    private BigDecimal totalPrice;

}

2、获取订单详情页面

业务流程:

1、远程查询所有的地址列表
2、远程查询购物车所有选中的购物项
3、查询用户积分
4、其他数据自动计算
5、防重令牌

1.controller类跳转

@Controller
public class OrderWebController {

    @Autowired
    OrderService orderService;

    //去结算确认页
    @GetMapping("/toTrade")
    public String toTrade(Model model){
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        model.addAttribute("OrderConfirmData",confirmVo);
        return "confirm";
    }
}

2.Impl具体实现订单页展示

@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    @Autowired
    MemberFeignService memberFeignService;

    @Autowired
    CartFeignService cartFeignService;

    /**
     * 订单确认页返回需要用的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {

        //构建响应模型类OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //从拦截器ThreadLocal获取当前用户登录的信息
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //开启第一个异步任务
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        }, threadPoolExecutor);

        //开启第二个异步任务
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, threadPoolExecutor).thenRunAsync(() -> {
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream()
                    .map((itemVo -> itemVo.getSkuId()))
                    .collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});

            if (skuStockVos != null && skuStockVos.size() > 0) {
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },threadPoolExecutor);

        //3、查询用户积分
        Integer integration = memberResponseVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4、价格数据自动计算

        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);


        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;
    }


}

3、获取会员信息【会员模块】

1.远程调用feign接口

在这里插入图片描述


@FeignClient("gulimall-member")
public interface MemberFeignService {

    /**
     * 返回会员所有的收货地址列表
     * @param memberId 会员ID
     * @return
     */
    @GetMapping("/member/memberreceiveaddress/{memberId}/address")
    List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);

}

2.会员controller类,创建方法

com.atguigu.gulimall.member.controller.MemberReceiveAddressController

    @GetMapping("/{memberId}/address")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
        return memberReceiveAddressService.getAddress(memberId);
    }

3.Impl实现类

com.atguigu.gulimall.member.service.impl.MemberReceiveAddressServiceImpl

    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
        return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
    }

4、获取当前用户的所有购物项【购物车模块】

1.首先通过用户ID在Redis中查询到购物车中的所有的购物项
2.通过 filter 过滤 用户购物车中被选择的购物项
3.查询数据库中当前购物项的价格,不能使用之前加入购物车的价格
4.编写远程 gulimall-product 服务中的 查询sku价格接口

1.feign接口远程调用购物车中方法

@FeignClient("gulimall-cart")
public interface CartFeignService {
 
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();


2.controller创建相关请求方法【购物车模块】

com.atguigu.gulimall.cart.controller.CartController

package com.atguigu.cart.controller;
 
@Controller
public class CartController {
 
    @Autowired
    CartService cartService;
 
    @GetMapping("/currentUserCartItems")
    @ResponseBody
    public List<CartItem> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }
  
  	//....
}

3.Impl实现查询购物车选中信息【购物车模块】

com.atguigu.gulimall.cart.service.impl.CartServiceImpl

@Autowired
ProductFeignService productFeignService;
 
/**
* 获取用户选择的所有购物项
* @return
*/
@Override
public List<CartItem> getUserCartItems() {
  UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  if (userInfoTo.getUserId() == null) {
    return null;
  } else {
    String cartKey = CART_PREFIX + userInfoTo.getUserId();
    // 获取所有用户选择的购物项
    List<CartItem> collect = getCartItems(cartKey).stream()
      .filter(item -> item.getCheck())
      .map(item->{
        // TODO 1、更新为最新价格
        R price = productFeignService.getPrice(item.getSkuId());
        String data = (String) price.get("data");
        item.setPrice(new BigDecimal(data));
        return item;
      })
      .collect(Collectors.toList());
    return collect;
  }
}

4.购物车模块远程调用商品模块查询商品sku价格

package com.atguigu.cart.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {
 
    //.....
 
    @GetMapping("/product/skuinfo/{skuId}/price")
    R getPrice(@PathVariable("skuId") Long skuId);
}

5.商品模块controller创建查询sku价格方法【商品模块】

com.atguigu.gulimall.product.app.SkuInfoController

@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
    @Autowired
    private SkuInfoService skuInfoService;

    /**
     * 获取指定商品的价格
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}/price")
    public R getPrice(@PathVariable("skuId") Long skuId) {
        SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
        return R.ok().setData(skuInfoEntity.getPrice().toString());
    }

5、Feign远程调用丢失请求头问题

1.问题分析

问题 :Feign远程调用的时候会丢失请求头

原因:远程调用是一个新的请求,不携带之前请求的cookie,导致购物车服务得不到请求头cookie里的登录信息。

解决:加上feign远程调用的请求拦截器。(RequestInterceptor)

因为feign在远程调用之前会执行所有的RequestInterceptor拦截器
在这里插入图片描述

2.解决:订单模块添加请求拦截器

com.atguigu.gulimall.order.config.GuliFeignConfig

@Configuration
public class GuliFeignConfig {

    /**
     * feign在远程调用之前会执行所有的RequestInterceptor拦截器
     * @return
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate requestTemplate) {
                // 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes!=null){
                    HttpServletRequest request = attributes.getRequest();
                    // 2、同步请求头数据,Cookie
                    String cookie = request.getHeader("Cookie");
                    // 给新请求同步了老请求的cookie
                    requestTemplate.header("Cookie",cookie);
                }
            }
        };
    }

}

6、异步线程丢失主线程请求头问题

问题
由于 RequestContextHolder底层使用的是线程共享数据 ThreadLocal,我们知道线程共享数据的域是 当前线程下,线程之间是不共享的。所以在开启异步后,异步线程获取不到主线程请求的信息,自然也就无法共享cookie了。

解决
向异步 RequestContextHolder 线程域中放主线程的域。

在这里插入图片描述
在这里插入图片描述

7、前端,订单页面渲染

			<div class="to_right">
				<h5>商家:谷粒学院自营</h5>
				<div><button>换购</button><span>已购满20.00元,再加49.90元,可返回购物车领取赠品</span></div>
				<!--图片-->
				<div class="yun1" th:each="item:${orderConfirmData.items}">
					<img style="width: 50px;height: 100px;" src="${item.image}" class="yun"/>
					<div class="mi">
						<p>[[${item.title}]]<span style="color: red;"> ¥ [[${#numbers.formatDecimal(item.price,1,2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
						<p> <span>0.095kg</span></p>
						<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
					</div>
				</div>

在这里插入图片描述

8、获取商品库存信息【库存模块】

1.远程调用库存模块查询库存方法

在这里插入图片描述

2.调用库存模块查询库存方法

在这里插入图片描述

3.创建库存vo返回信息

com.atguigu.gulimall.order.vo.SkuStockVo

import lombok.Data;

@Data
public class SkuStockVo {
    private Long skuId;
    private Boolean hasStock;
}

4.修改Impl查询结算页面实现类,增加库存查询

        //开启第二个异步任务
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, executor).thenRunAsync(() -> {
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream()
                    .map((itemVo -> itemVo.getSkuId()))
                    .collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkusHasStock(skuIds);
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});

            if (skuStockVos != null && skuStockVos.size() > 0) {
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },executor);

8、根据用户地址ID,返回详细地址并计算物流费【库存模块】

1.返回数据vo

在这里插入图片描述

2.库存模块controller

gulimall-ware仓储服务编写 根据用户地址,返回详细地址并计算物流费h

package com.atguigu.gulimall.ware.controller;
 
@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
    @Autowired
    private WareInfoService wareInfoService;
 
    @GetMapping("/fare")
    public R getFare(@RequestParam("addrId") Long addrId){
        FareVo fare = wareInfoService.getFare(addrId);
        return R.ok().setData(fare);
    }
  //...
}

3.仓库模块远程调用用户模块,查地址信息

@FeignClient("gulimall-member")
public interface MemberFeignService {

    /**
     * 根据地址id查询地址的详细信息
     * @param id
     * @return
     */
    @RequestMapping("/member/memberreceiveaddress/info/{id}")
    R addrInfo(@PathVariable("id") Long id);
}

4.service,根据地址id获取地址信息和费用

gulimall-ware 服务中 com.atguigu.gulimall.ware.service.impl路径下 WareInfoServiceImpl 类

    @Override
    public FareVo getFare(Long addrId) {

        FareVo fareVo = new FareVo();
        R r = memberFeignService.addrInfo(addrId);
        MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
        });
        if (data!=null) {
            // 简单处理:截取手机号最后一位作为邮费
            String phone = data.getPhone();
            String substring = phone.substring(phone.length() - 1, phone.length());
            BigDecimal bigDecimal = new BigDecimal(substring);
            fareVo.setAddressVo(data);
            fareVo.setFare(bigDecimal);
            return fareVo;
        }
        return null;
    }

5.前端页面渲染

在这里插入图片描述
在这里插入图片描述

function highlight(){
   $(".addr-item p").css({"border": "2px solid gray"});
   $(".addr-item p[def='1']").css({"border": "2px solid red"});
}
$(".addr-item p").click(function () {
   $(".addr-item p").attr("def","0");
   $(this).attr("def","1");
   highlight();
   // 获取当前地址id
   var addrId = $(this).attr("addrId");
   // 发送ajax获取运费信息
   getFare(addrId);
});
function getFare(addrId) {
   $.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
      console.log(resp);
      $("#fareEle").text(resp.data.fare);
      var total = [[${orderConfirmData.total}]]
      // 设置运费信息
      $("#payPriceEle").text(total*1 + resp.data.fare*1);
      // 设置收货人信息
      $("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
      $("#reveiverEle").text(resp.data.address.name);
   })
}

五、提交订单

1、保证接口幂等性,防重复提交表单

1.什么是幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。

2.哪些情况需要防止

用户多次点击按钮
用户页面回退再次提交
微服务互相调用,由于网络问题,导致请求失败。feign触发重试机制
其他业务情况

3.什么情况下需要幂等

以SQL为例,有些操作是天然幂等的。
SELECT*FROMtableWHERid=?,无论执行多少次都不会改变状态,是天然的幂等。
UPDATEtab1SETcol1=1WHEREcol2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
deletefromuserwhereuserid=1,多次操作,结果一样,具备幂等性
insertintouser(userid,name)values(1,‘a’)如userid为唯一主键,即重复操作上面的业务,只
会插入一条用户数据,具备幂等性。

UPDATEtab1SETcol1=col1+1WHEREcol2=2,每次执行的结果都会发生变化,不是幂等的。
insertintouser(userid,name)values(1,‘a’)如userid不是主键,可以重复,那上面业务多次操
作,数据都会新增多条,不具备幂等性。

4.幂等解决方案

1、token机制

1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,
就必须在执行业务前,先去获取token,服务器会把token保存到redis中。
2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业
务。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样
就保证了业务代码,不被重复执行。

危险性:
1、先删除token还是后删除token;
(1)先删除可能导致,业务确实没有执行,重试还带上之前token,由于防重设计导致,
请求还是不能执行。
(2)后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除token,别
人继续重试,导致业务被执行两边
(3)我们最好设计为先删除token,如果业务调用失败,就重新获取token再次请求。

2、Token获取、比较和删除必须是原子性
(1) redis.get(token)、token.equals、redis.del(token)如果这两个操作不是原子,可能导
致,高并发下,都get到同样的数据,判断都成功,继续业务并发执行
(2)可以在redis使用lua脚本完成这个操作

2、各种锁机制

1、数据库悲观锁
select*fromxxxxwhereid=1forupdate;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
另外要注意的是,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会
非常麻烦。

2、数据库乐观锁
这种方法适合在更新的场景中,
updatet_goodssetcount=count-1,version=version+1wheregood_id=2andversion=1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候
带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务
version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订
单服务传如的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变
为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题

3、业务层分布式锁
如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数
据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断
这个数据是否被处理过。

3、各种唯一约束

1、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。
我们在数据库层面防止重复。
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键
的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要
不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

2、redis set 防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,
每次处理数据,先看这个MD5是否已经存在,存在就不处理。

4、防重表
使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且
他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避
免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个
事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
之前说的redis防重也算

5、全局请求唯一id
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。
可以使用nginx设置每一个请求的唯一id;
proxy_set_header X-Request-Id $request_id

2、业务流程

需求:用户进入订单确认页,在不刷新、不重进的情况下,重复点击“提交订单”,只有一次能提交成功。

确认订单: 生成令牌:redis添加数据,key为"order:token"+用户id,value为防重复提交表单的uuid作为token,并设置30min过期时间。
在这里插入图片描述

提交订单(下一篇文章详细讲):

验令牌:先获取前端传来的token,再根据用户id查询Redis里的token,比较两个token是否相等,相等则代表是同一个的订单。因为uuid能保证唯一性,它是根据时间戳和mac地址生成的。

原子性验删令牌:验令牌和删除令牌写成一个lua脚本,Redis传参键值对并执行lua脚本,执行成功代表验证成功,执行失败代表验证失败。

3、代码实现,防重复提交表单,唯一序列号方式保证幂等性

 
 
/**
 * 下单操作:验令牌、创建订单、验价格、验库存
 * @param vo
 * @return
 */
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    SubmitOrderResponseVo response = new SubmitOrderResponseVo();
 
    // 从拦截器中拿到当前的用户
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
    // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(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();
    // 原子验证令牌和删除令牌
    Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(),
            OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), orderToken);
    if (result == 0L) {
        // 令牌验证失败
        response.setCode(1);
        return response;
    } else {
        // 令牌验证成功
        return response;
    }
}

前端显示令牌

    <form action="http://order.gulimall.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>

1.订单提交vo数据创建

/**
 *
 * 封装订单提交的数据
 *
 */
@Data
public class OrderSubmitVo {
    private Long addrId; //收货地址的id

    private Integer payType; //支付方式
    //无需提交需要购买的商品,去购物车再获取一遍
    //优惠,发票

    private String orderToken;//放重令牌
    private BigDecimal payPrice;//应付价格,验价

    private String note;//备注
    //用户相关信息去session中取出
}

com.atguigu.gulimall.order.vo.SubmitOrderResponseVo

修改 SubmitOrderResponseVo 类编写验证令牌操作

@Data
public class SubmitOrderResponseVo {

    private OrderEntity order;
    private Integer code; //0成功  错误状态码
}

2.订单提交controller方法创建

    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo vo){

        SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
        //下单:去创建订单,验证令牌,验证价格,锁库存
        //下单成功去支付页面
        System.out.println("订单提交数据。。。。"+vo);
        if(responseVo.getCode() == 0){
            return "pay";
        }else {
            return "redirect:http://order.yjlmall.com/toTrade";
        }

    }

4、初始化新订单,包含订单、订单项等信息

1.抽取订单创建传输类vo

@Data
public class OrderCreateTo {
 
    private OrderEntity order;
 
    private List<OrderItemEntity> orderItems;
 
    /** 订单计算的应付价格 **/
    private BigDecimal payPrice;
 
    /** 运费 **/
    private BigDecimal fare;
 
}

2.封装运费模型类vo

package com.atguigu.gulimall.order.vo;
 
@Data
public class FareVo {
    private MemberAddressVo address;
    private BigDecimal fare;
}

3.远程调用仓库服务,计算运费和详细地址的接口【库存模块】

 
@FeignClient("gulimall-ware")
public interface WareFeignService {
 
    @PostMapping("/ware/waresku/hasstock")
    R getSkusHasStock(@RequestBody List<Long> skuIds);
 
    /**
     * 计算运费和详细地址
     * @param addrId
     * @return
     */
    @GetMapping("/ware/wareinfo/fare")
    R getFare(@RequestParam("addrId") Long addrId);
}

4.Impl构建初始化订单实现类

获取创建初始化订单

/**
 * 创建订单、订单项等信息
 * @return
 */
private OrderCreateTo createOrder(){
    OrderCreateTo createTo = new OrderCreateTo();
    // 1、生成一个订单号。IdWorker.getTimeId()是Mybatis提供的生成订单号方法,ID=Time+Id
    String orderSn = IdWorker.getTimeId();
    // 2、构建一个订单
    OrderEntity orderEntity = buildOrder(orderSn);
    // 3、获取到所有的订单项
    List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
    // 4、计算价格、积分等相关信息
    computePrice(orderEntity,itemEntities);
 
    createTo.setOrder(orderEntity);
    createTo.setOrderItems(itemEntities);
    return createTo;
}

构建一个订单:输入数据

/**
 * 构建订单
 * @param orderSn
 * @return
 */
private OrderEntity buildOrder(String orderSn) {
    MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
    OrderEntity entity = new OrderEntity();
    entity.setOrderSn(orderSn);
    entity.setMemberId(respVp.getId());
 
    OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
    // 1、获取运费 和 收货信息
    R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
    FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
    });
    // 2、设置运费
    entity.setFreightAmount(fareResp.getFare());
    // 3、设置收货人信息
    entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
    entity.setReceiverProvince(fareResp.getAddress().getProvince());
    entity.setReceiverRegion(fareResp.getAddress().getRegion());
    entity.setReceiverCity(fareResp.getAddress().getCity());
    entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
    entity.setReceiverName(fareResp.getAddress().getName());
    entity.setReceiverPhone(fareResp.getAddress().getPhone());
    // 4、设置订单的相关状态信息
    entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    // 5、默认取消信息
    entity.setAutoConfirmDay(7);
    return entity;
}

5、构造每一个订单项数据

1.抽取商品信息vo

package com.atguigu.gulimall.order.vo;
 
@Data
public class SpuInfoVo {
    /**
     * 商品id
     */
    @TableId
    private Long id;
    /**
     * 商品名称
     */
    private String spuName;
    /**
     * 商品描述
     */
    private String spuDescription;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     *
     */
    private BigDecimal weight;
    /**
     * 上架状态[0 - 新建,1 - 上架,2-下架]
     */
    private Integer publishStatus;
    /**
     *
     */
    private Date createTime;
    /**
     *
     */
    private Date updateTime;
}

2.订单服务远程调用商品服务【商品模块】

@FeignClient("gulimall-product")
public interface ProductFeignService {
 
    @GetMapping("/product/spuinfo/skuId/{id}")
    R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}

3.【商品模块】通过skuId查询spu信息

controller类:

package com.atguigu.gulimall.product.app;
 
@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
    @Autowired
    private SpuInfoService spuInfoService;
 
 
    /**
     * 查询指定sku的spu信息
     * @param skuId
     * @return
     */
    @GetMapping("/skuId/{id}")
    public R getSpuInfoBySkuId(@PathVariable("id") Long skuId) {
        SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
        return R.ok().setData(entity);
    }

方法实现:

package com.atguigu.gulimall.product.service.impl;
 
@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
    SkuInfoEntity byId = skuInfoService.getById(skuId);
    Long spuId = byId.getSpuId();
    SpuInfoEntity spuInfoEntity = getById(spuId);
    return spuInfoEntity;
}

4.Impl实现订单构建方法

/**
 * 构建所有订单项数据
 * @return
 */
private  List<OrderItemEntity> buildOrderItems(String orderSn) {
    // 最后确定每个购物项的价格
    List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
    if (currentUserCartItems != null && currentUserCartItems.size()>0){
        List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
            OrderItemEntity itemEntity = buildOrderItem(cartItem);
            itemEntity.setOrderSn(orderSn);
            return itemEntity;
        }).collect(Collectors.toList());
        return itemEntities;
    }
    return null;
}
 
/**
 * 构建某一个订单项
 * @param cartItem
 * @return
 */
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
    OrderItemEntity itemEntity = new OrderItemEntity();
    // 1、订单信息:订单号 v
    // 2、商品的spu信息
    Long skuId = cartItem.getSkuId();
    R r = productFeignService.getSpuInfoBySkuId(skuId);
    SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
    });
    itemEntity.setSpuId(data.getId());
    itemEntity.setSpuBrand(data.getBrandId().toString());
    itemEntity.setSpuName(data.getSpuName());
    itemEntity.setCategoryId(data.getCatalogId());
    // 3、商品的sku信息  v
    itemEntity.setSkuId(cartItem.getSkuId());
    itemEntity.setSkuName(cartItem.getTitle());
    itemEntity.setSkuPic(cartItem.getImage());
    itemEntity.setSkuPrice(cartItem.getPrice());
    itemEntity.setSkuQuantity(cartItem.getCount());
    itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
    // 4、优惠信息【不做】
    // 5、积分信息
    itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    // 6、订单项的价格信息
    itemEntity.setPromotionAmount(new BigDecimal("0"));
    itemEntity.setCouponAmount(new BigDecimal("0"));
    itemEntity.setIntegrationAmount(new BigDecimal("0"));
    // 当前订单项的实际金额 总额-各种优惠
    BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
    BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
            subtract(itemEntity.getCouponAmount()).
            subtract(itemEntity.getIntegrationAmount());
    itemEntity.setRealAmount(subtract);
    return itemEntity;
}

5.计算订单价格

/**
 * 计算价格
 * @param orderEntity
 * @param itemEntities
 */
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
    BigDecimal total = new BigDecimal("0.0");
    BigDecimal coupon = new BigDecimal("0.0");
    BigDecimal integration = new BigDecimal("0.0");
    BigDecimal promotion = new BigDecimal("0.0");
    BigDecimal gift = new BigDecimal("0.0");
    BigDecimal growth = new BigDecimal("0.0");
    // 1、订单的总额,叠加每一个订单项的总额信息
    for (OrderItemEntity entity : itemEntities) {
        total = total.add(entity.getRealAmount());
        coupon = coupon.add(entity.getCouponAmount());
        integration = integration.add(entity.getIntegrationAmount());
        promotion = promotion.add(entity.getPromotionAmount());
        gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
        growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
    }
    // 订单总额
    orderEntity.setTotalAmount(total);
    // 应付总额
    orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
    orderEntity.setCouponAmount(coupon);
    orderEntity.setIntegrationAmount(integration);
    orderEntity.setPromotionAmount(promotion);
    // 设置积分等信息
    orderEntity.setIntegration(gift.intValue());
    orderEntity.setGrowth(growth.intValue());
    orderEntity.setDeleteStatus(0);//0 未删除
}

6、库存锁定

1.【公共模块】无库存异常类

package com.atguigu.common.exception;
 
public class NoStockException extends RuntimeException{
    private Long skuId;
 
    public NoStockException(Long skuId){
        super("商品id:"+skuId+";没有足够的库存了!");
    }
 
    public NoStockException(String message) {
        super(message);
    }
 
    @Override
    public String getMessage() {
        return super.getMessage();
    }
 
    public Long getSkuId() {
        return skuId;
    }
 
    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }
 
}

2.锁定库存的vo类【库存模块】

/**
 * @Description: 锁定库存的vo
 **/
 
@Data
public class WareSkuLockVo {
 
    private String orderSn;
 
    /** 需要锁住的所有库存信息 **/
    private List<OrderItemVo> locks;
 
 
 
}

3.订单服务远程调用仓库服务

package com.atguigu.gulimall.order.feign;
 
@FeignClient("gulimall-ware")
public interface WareFeignService {
        //....
 
    /**
     * 锁定指定订单的库存
     * @param vo
     * @return
     */
    @PostMapping("/ware/waresku/lock/order")
    R orderLockStock(@RequestBody WareSkuLockVo vo);
}

4.锁定库存controller【库存模块】

    /**
     * 锁定订单项库存
     * @param vo
     * @return
     */
    @PostMapping("/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo){
        try {
            Boolean stock = wareSkuService.orderLockStock(vo);
            return R.ok();
        } catch (NoStockException e){
            return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(), BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
        }
    }

5.dao,根据sku_id查询在有库存的仓库

gulimall-ware服务中com.atguigu.gulimall.ware.dao路径下的 WareSkuDao 类:

    /**
     * 通过skuId查询在哪个仓库有库存
     * @param skuId
     * @return  仓库的编号
     */
    List<Long> listWareIdHasSkuStock(@Param("skuId") Long skuId);

    /**
     * 锁库存
     * @param skuId
     * @param wareId
     * @param num
     * @return
     */
    Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);

gulimall-ware服务中gulimall-ware/src/main/resources/mapper/ware路径下的 WareSkuDao.xml:

    <select id="listWareIdHasSkuStock" resultType="java.lang.Long">
        SELECT ware_id FROM `wms_ware_sku` WHERE sku_id=#{skuId} AND stock-stock_locked>0
    </select>

    <select id="lockSkuStock">
        UPDATE `wms_ware_sku` SET stock_locked = stock_locked+#{num}
        WHERE sku_id=#{skuId} AND ware_id=#{wareId} AND stock-stock_locked>=#{num}
    </select>

6.锁定指定订单的库存service

package com.atguigu.gulimall.ware.service.impl;
 
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
 
    @Autowired
    WareSkuDao wareSkuDao;
 
    @Autowired
    ProductFeignService productFeignService;
        //......
 
    /**
     * 锁定指定订单的库存
     * @param vo
     * @return
     * (rollbackFor = NoStockException.class)
     * 默认只要是运行时异常都会回滚
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        // 1、每个商品在哪个库存里有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHashStock> collect = locks.stream().map(item -> {
            SkuWareHashStock stock = new SkuWareHashStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            // 查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHashSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());
 
        // 2、锁定库存
        for (SkuWareHashStock hashStock : collect) {
            Boolean skuStocked = false;
            Long skuId = hashStock.getSkuId();
            List<Long> wareIds = hashStock.getWareId();
            if (wareIds == null || wareIds.size()==0){
                // 没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            for (Long wareId : wareIds) {
                // 成功就返回1,否则就返回0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hashStock.getNum());
                if (count == 1){
                    skuStocked = true;
                    break;
                } else {
                    // 当前仓库锁失败,重试下一个仓库
 
                }
            }
            if (skuStocked == false){
                // 当前商品所有仓库都没有锁住,其他商品也不需要锁了,直接返回没有库存了
                throw new NoStockException(skuId);
            }
        }
 
        // 3、运行到这,全部都是锁定成功的
        return true;
    }
    @Data
    class SkuWareHashStock{
        private Long skuId;     // skuid
        private Integer num;    // 锁定件数
        private List<Long> wareId;  // 锁定仓库id
    }
 
}

7.前端页面修改

<div class="Jdbox_BuySuc">
  <dl>
    <dt><img src="/static/order/pay/img/saoyisao.png" alt=""></dt>
    <dd>
      <span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
      <span>应付金额<font>[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font>元</span>
    </dd>
    <dd>
      <span>推荐使用</span>
      <span>扫码支付请您在<font>24小时</font>内完成支付,否则订单会被自动取消(库存紧订单请参见详情页时限)</span>
      <span>订单详细</span>
    </dd>
  </dl>
</div>

五、测试

测试发现,刷新页面,令牌会更改。回退后重新进入确认页,令牌会更改。

我在不刷新的情况下,连续点击2次提交,传到后端的token都是一样的,记为token1,Redis存"order:token66"—>token1。

线程A:“提交订单”的controller接收到token1,原子性对比redis里的token1和删除,校验通过;
线程B:“提交订单”的controller接收到token1,原子性对比redis里的token1和删除,因为线程A已经删除成功,所以现在校验失败或者删除失败,所以校验失败。

我刷新一下,再次点击2次提交,传到后端的token都是一样的,记为token2,Redis存"order:token66"—>token2。

线程C:“提交订单”的controller接收到token2,原子性对比redis里的token2和删除,校验通过;
线程D:“提交订单”的controller接收到token2,原子性对比redis里的token2和删除,因为线程C已经删除成功,所以现在校验失败或者删除失败,所以校验失败。
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
谷粒商城订单模块中,redis主要应用在以下几个方面: 1. 订单状态缓存 谷粒商城订单系统中,订单的状态包括待支付、已支付、已发货、已完成等,这些状态的变化会影响订单的展示和处理。为了提高订单查询的效率,可以将订单的状态信息缓存到redis中,以便快速获取订单状态信息。 2. 订单超时处理 在谷粒商城订单系统中,订单支付后需要在一定时间内完成支付,否则订单将被取消。为了实现订单超时处理,可以使用redis的过期时间特性。当订单创建时,将订单号作为key存入redis中,并设置过期时间为支付有效期,当订单完成支付时,将订单号从redis中删除,如果订单超时未支付,redis会自动删除该订单key,此时可以根据key是否存在来判断订单是否超时。 3. 订单限流 在谷粒商城订单系统中,为了避免系统负载过高,需要对订单的并发量进行限制,可以使用redis的计数器特性。当有新订单创建时,先判断当前计数器的值是否超过限制,如果未超过则允许创建订单,并将计数器加1,反之则拒绝创建订单。 4. 订单消息队列 在谷粒商城订单系统中,订单的创建、支付、发货等操作会涉及多个系统和服务,为了保证订单处理的可靠性和效率,可以使用redis作为订单消息队列。当有新订单创建时,将订单信息作为消息放入redis队列中,订单处理服务从队列中获取消息并进行订单处理,处理完成后将订单状态更新到数据库中,并发送订单处理完成消息,其他系统和服务根据消息进行后续处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值