【谷粒商城】分布式事务与下单

本文深入探讨了购物车及订单系统的设计与实现,重点介绍了购物车的数据结构选择、存储方案,以及订单流程中的关键环节。针对购物车,文章分析了不同存储方式的优劣,并选择了Redis的Hash结构作为最佳实践方案。对于订单系统,则详细讲解了从订单提交到支付的整个流程,包括订单状态管理、支付接口设计等内容。

本篇2.5W字,请直接ctrl+F搜索内容

一、gulimall-cart

构建gulimall-cart,复制静态资源到nginx,修改网关

购物车分为离线购物车和登录购物车

离线购物车重启浏览器了也还有

二、购物车

1、购物车需求

特点:读多写少,放入数据库并不合适

登录状态:登录购物车

  • 放入数据库
  • mongodb
  • 放入redis(采用)
    • 登录以后,将离线购物车里合并后清空离线购物车

未登录状态:离线购物车

  • 放入localstorage浏览器的技术
  • cookie
  • WebSQL
  • 放入redis(采用)
    • 浏览器重启后还在

2、购物车VO

(1) 数据结构分析

购物车

{
   
   
    skuid:123123,
    check:true, # 每一项是否被选中
    title:"apple ...",
    defaultImage:"",
    price:4999,
    count:1,
    totalPrice:4999, # 商品的总价=单价*数量
    skuSaleVO:{
   
   ...}
}

购物车不只一条数据

[
    {
   
   sku1},{
   
   sku2},{
   
   }
]

redis有5种不同数据结构,这里选择哪一种比较合适呢?Map<String,List<String>>

不好的方式:不同用户应该有独立的购物车,因此购物车应该以用户作为key来存储,value是用户的所有购车信息。这样看来基本的k-v结构就可以了。

但是,我们对购车中的商品进行增、删、改操作,基本都需要根据商品id讲行判断,为了方便后期处理,我们的购车也应该是k-v结构,key是商品id,value才是这个商品的购车信息。

一个购物车是由各个购物项组成的,但是我们用List进行存储并不合适,因为使用List查找某个购物项时需要挨个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用hash进行存储

每个人都有一个hash表,key为skuId,value为数据

(2) 购物项vo

public class CartItem {
   
   

	private Long skuId;

	/*** 是否被选中*/
	private Boolean check = true;

	private String title;
	private String image;

	private List<String> skuAttr;

	/*** 价格*/
	private BigDecimal price;
	/*** 数量*/
	private Integer count;
(3) 购物车vo

public class Cart {
   
   

	private List<CartItem> items;

	/*** 商品的数量*/
	private Integer countNum;
	/*** 商品的类型数量*/
	private Integer countType;

	/*** 整个购物车的总价*/
	private BigDecimal totalAmount;

	/*** 减免的价格*/
	private BigDecimal reduce = new BigDecimal("0.00");

	/*** 计算商品的总量*/
	public Integer getCountNum() {
   
   
		int count = 0;
		if(this.items != null && this.items.size() > 0){
   
   
			for (CartItem item : this.items) {
   
   
				count += item.getCount();
			}
		}
		return count;
	}

	public Integer getCountType() {
   
   
		int count = 0;
		if(this.items != null && this.items.size() > 0){
   
   
			for (CartItem item : this.items) {
   
   
				count += 1;
			}
		}
		return count;
	}

	public BigDecimal getTotalAmount() {
   
   
		BigDecimal amount = new BigDecimal("0");
		if(this.items != null && this.items.size() > 0){
   
   
			for (CartItem item : this.items) {
   
   
				if(item.getCheck()){
   
   
					BigDecimal totalPrice = item.getTotalPrice();
					amount = amount.add(totalPrice);
				}
			}
		}
		return amount.subtract(this.getReduce());
	}

3、 ThreadLocal用户身份鉴别

(1) threadlocal说明

threadlocal的效果是其中存储的内容只有当前线程能访问的

如果想了解更多threadlocal知识可以查看:https://blog.youkuaiyun.com/hancoder/article/details/107853513

threadlocal的原理是每个线程都有一个map,key为threadlocal对象,value为对象所对应的值

参考京东,在点击购物车时,会为临时用户生成一个nameuser-keycookie临时标识,过期时间为一个月,如果手动清除user-key,那么临时购物车的购物项也被清除,所以user-key是用来标识和存储临时购物车数据的

(2) 使用ThreadLocal进行用户身份鉴别信息传递

但是注意的是tomcat中线程可以复用,所以线程和会话不是一对一的关系。但是没有关系,会在拦截器中先判断会话有没有用户信息(cookie),

  • 首先明确每次拦截器都会重新设置threadlocal
  • 没有的话创建一个临时用户,回去的时候告诉用户的临时cookie。threadlocal中只封装临时用户信息
  • 有的话把临时用户和登录用户封装到一起,设置到threadlocal中
拦截器拦截会话

购物车拦截器的配置

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
   
   
    //拦截所有请求
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
   
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

购物车拦截器

  • 可以看到有句session.getAttribute(AuthServerConstant.LOGIN_USER);
  • 看cookie中有没有临时数据,就是cookie带过来的
  • 将用户信息放到threadlocal中让当前用户使用threadLocal.set(userInfoTo);
public class CartInterceptor implements HandlerInterceptor {
   
   

	// 静态,
	public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
   

		// 准备好要设置到threadlocal里的user对象
		UserInfoTo userInfoTo = new UserInfoTo();
		HttpSession session = request.getSession();
		// 获取loginUser对应的用户value,没有也不去登录了。登录逻辑放到别的代码里,需要登录时再重定向
		MemberRespVo user = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
		if (user != null){
   
    // 用户登陆了,设置userId
			userInfoTo.setUsername(user.getUsername());
			userInfoTo.setUserId(user.getId());
		}
		
		// 不登录也没关系,可以访问临时用户购物车
		// 去查看请求带过来的cookies里的临时购物车cookie
		Cookie[] cookies = request.getCookies();
		if(cookies != null && cookies.length > 0){
   
   
			for (Cookie cookie : cookies) {
   
   
				String name = cookie.getName();
				if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
   
   
					userInfoTo.setUserKey(cookie.getValue());
					userInfoTo.setTempUser(true);
				}
			}
		}
		// 如果没有临时用户 则分配一个临时用户 // 分配的临时用户在postHandle的时候放到cookie里即可
		if (StringUtils.isEmpty(userInfoTo.getUserKey())){
   
   
			String uuid = UUID.randomUUID().toString().replace("-","");
			userInfoTo.setUserKey("GULI-" + uuid);//临时用户
		}
		threadLocal.set(userInfoTo);
		return true;
		// 还有一个登录后应该删除临时购物车的逻辑没有实现
	}

	/**
	 * 执行完毕之后分配临时用户让浏览器保存
	 */
	@Override
	public void postHandle(HttpServletRequest request, 
						   HttpServletResponse response, Object handler,
						   ModelAndView modelAndView) throws Exception {
   
   

		UserInfoTo userInfoTo = threadLocal.get();
		// 如果是临时用户,返回临时购物车的cookie
		if(!userInfoTo.isTempUser()){
   
   
			Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
			// 设置这个cookie作用域 过期时间
			cookie.setDomain("gulimall.com");
			cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIME_OUT);
			response.addCookie(cookie);
		}
	}
}

3. 添加商品到购物车

需要的服务:gateway、product、ware、cart、seckill、search、auth

/*** 添加商品到购物车
	 *  RedirectAttributes.addFlashAttribute():将数据放在session中,可以在页面中取出,但是只能取一次
	 *  RedirectAttributes.addAttribute():将数据拼接在url后面,?skuId=xxx
	 * */
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
                        @RequestParam("num") Integer num,
                        RedirectAttributes redirectAttributes)  // 重定向数据, 会自动将数据添加到url后面
    throws ExecutionException, InterruptedException {
   
   

    // 添加数量到用户购物车
    cartService.addToCart(skuId, num);
    // 返回skuId告诉哪个添加成功了
    redirectAttributes.addAttribute("skuId", skuId);
    // 重定向到成功页面
    return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}

// 添加sku到购物车响应页面
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam(value = "skuId",required = false) Object skuId, Model model){
   
   
    CartItem cartItem = null;
    // 然后在查一遍 购物车
    if(skuId == null){
   
   
        model.addAttribute("item", null);
    }else{
   
   
        try {
   
   
            cartItem = cartService.getCartItem(Long.parseLong((String)skuId));
        } catch (NumberFormatException e) {
   
   
            log.warn("恶意操作! 页面传来skuId格式错误");
        }
        model.addAttribute("item", cartItem);
    }
    return "success";
}
获取用户购物车数据

先获取redis里该用户购物车的那个map,每个用户的购物车都是个map,map名为ATGUIGU:cart:用户id

登录用户优先

private BoundHashOperations<String, Object, Object> getCartOps() {
   
   
    // 1. 这里我们需要知道操作的是离线购物车还是在线购物车
    UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  
    String cartKey = CART_PREFIX; //  "ATGUIGU:cart:";
    if(userInfoTo.getUserId() != null){
   
   
        log.debug("\n用户 [" + userInfoTo.getUsername() + "] 正在操作购物车");
        // 已登录的用户购物车的标识
        cartKey += userInfoTo.getUserId();
    }else{
   
   
        log.debug("\n临时用户 [" + userInfoTo.getUserKey() + "] 正在操作购物车");
        // 未登录的用户购物车的标识
        cartKey += userInfoTo.getUserKey();
    }
    // 绑定这个 key 以后所有对redis 的操作都是针对这个key
    return stringRedisTemplate.boundHashOps(cartKey);
}
购物车service
  • 若购物车中已经存在该商品,只需增添数量
  • 否则需要查询商品购物项所需信息,并添加新商品至购物车
    • map的key是skuId,value是数量
@Override // CartServiceImpl
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
   
   
    // 获取当前用户的map
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    // 查看该用户购物车里是否有指定的skuId
    String res = (String) cartOps.get(skuId.toString());

    // 查看用户购物车里是否已经有了该sku项
    if(StringUtils.isEmpty(res)){
   
   
        CartItem cartItem = new CartItem();
        // 异步编排
        CompletableFuture<Void> getSkuInfo = CompletableFuture.runAsync(() -> {
   
   
            // 1. 远程查询当前要添加的商品的信息
            R skuInfo = productFeignService.SkuInfo(skuId);
            SkuInfoVo sku = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
   
   });
            // 2. 填充购物项
            cartItem.setCount(num);
            cartItem.setCheck(true);
            cartItem.setImage(sku.getSkuDefaultImg());
            cartItem.setPrice(sku.getPrice());
            cartItem.setTitle(sku.getSkuTitle());
            cartItem.setSkuId(skuId);
        }, executor);

        // 3. 远程查询sku销售属性,销售属性是个list
        CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
   
   
            List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
            cartItem.setSkuAttr(values);
        }, executor);
        // 等待执行完成
        CompletableFuture.allOf(getSkuInfo, getSkuSaleAttrValues).get();

        // sku放到用户购物车redis中
        cartOps.put(skuId.toString(), JSON.toJSONString(cartItem));
        return cartItem;
    }else{
   
   //购物车里已经有该sku了,数量+1即可
        CartItem cartItem = JSON.parseObject(res, CartItem.class);
        // 不太可能并发,无需加锁
        cartItem.setCount(cartItem.getCount() + num);
        cartOps.put(skuId.toString(), JSON.toJSONString(cartItem));
        return cartItem;
    }
}

4. 展示购物车

  • 若用户未登录,则直接使用user-key获取购物车数据
  • 否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车
@RequestMapping("/cart.html")
public String getCartList(Model model) {
   
   
    CartVo cartVo=cartService.getCart();
    model.addAttribute("cart", cartVo);
    return "cartList";
}


@Override
public Cart getCart() throws ExecutionException, InterruptedException {
   
   
    UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    Cart cart = new Cart();
    // 临时购物车的key
    String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
    // 是否登录
    if(userInfoTo.getUserId() != null){
   
   
        // 已登录 对用户的购物车进行操作
        String cartKey = CART_PREFIX + userInfoTo.getUserId();
        // 1 如果临时购物车的数据没有进行合并
        List<CartItem> tempItem = getCartItems(tempCartKey);
        if(tempItem != null){
   
   
            // 2 临时购物车有数据 则进行合并
            log.info("\n[" + userInfoTo.getUsername() + "] 的购物车已合并");
            for (CartItem cartItem : tempItem) {
   
   
                addToCart(cartItem.getSkuId(), cartItem.getCount());
            }
            // 3 清空临时购物车,防止重复添加
            clearCart(tempCartKey);
            // 设置为非临时用户
            userInfoTo.setTempUser(false);
        }
        // 4 获取登录后的购物车数据 [包含合并过来的临时购物车数据]
        List<CartItem> cartItems = getCartItems(cartKey);
        cart.setItems(cartItems);
    }else {
   
   
        // 没登录 获取临时购物车的所有购物项
        cart.setItems(getCartItems(tempCartKey));
    }
    return cart;
}

/**
	 * 获取购物车所有项
	 */
private List<CartItem> getCartItems(String cartKey){
   
   
    BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(cartKey);
    // key不重要,拿到值即可
    List<Object> values = hashOps.values();
    if(values != null && values.size() > 0){
   
   
        return values.stream().map(obj -> JSON.parseObject(JSON.toJSONString(obj) , CartItem.class)).collect(Collectors.toList());
    }
    return null;
}

5. 选中购物车项

更改购物项选中状态

@RequestMapping("/checkCart")
public String checkCart(@RequestParam("isChecked") Integer isChecked,@RequestParam("skuId")Long skuId) {
   
   
    cartService.checkItem(skuId, isChecked);
    return "redirect:http://cart.gulimall.com/cart.html";
}

//修改skuId对应购物车项的选中状态
@Override
public void checkItem(Long skuId, Integer check) {
   
   
    // 获取要选中的购物项 // 信息还是在原来的缓存中,更新即可
    CartItem cartItem = getCartItem(skuId);
    cartItem.setCheck(check==1?true:false);
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    cartOps.put(skuId.toString(), JSON.toJSONString(cartItem));
}

@Override
public CartItem getCartItem(Long skuId) {
   
   
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    String o = (String) cartOps.get(skuId.toString());
    return JSON.parseObject(o, CartItem.class);
}

6. 修改购物项数量

@RequestMapping("/countItem")
public String changeItemCount(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num) {
   
   
    cartService.changeItemCount(skuId, num);
    return "redirect:http://cart.gulimall.com/cart.html";
}

@Override
public void changeItemCount(Long skuId, Integer num) {
   
   
    BoundHashOperations<String, Object, Object> ops = getCartItemOps();
    String cartJson = (String) ops.get(skuId.toString());
    CartItemVo cartItemVo = JSON.parseObject(cartJson, CartItemVo.class);
    cartItemVo.setCount(num);
    ops.put(skuId.toString(),JSON.toJSONString(cartItemVo));
}

7. 删除购物车项

@RequestMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId) {
   
   
    cartService.deleteItem(skuId);
    return "redirect:http://cart.gulimall.com/cart.html";
}

@Override
public void deleteItem(Long skuId) {
   
   
    BoundHashOperations<String, Object, Object> ops = getCartItemOps();
    ops.delete(skuId.toString());
}

三、消息队列

https://blog.youkuaiyun.com/hancoder/article/details/114297652

四、Session共享

这部分的内容请去认证服务笔记里看https://blog.youkuaiyun.com/hancoder/article/details/114242184

思想就是用redis存储session,并且cookie的作用域跨大到*.gulimall.com

如果域名不同可以用单点登录解决,思想为创建登录服务器,去登录服务器获取用户的redis-key,然后在自己的服务里请求redis对应的用户后保存到自己的session里

五、订单模型

资料源码中等待付款是订单详情页;订单页是用户订单列表;结算页是订单确认页;收银页是支付页cd

在nginx中新建目录order

  • 放到IDEA-order项目中
  • order/detail中放入【等待付款】的静态资源。index.html重命名为detail.html
  • order/list中放入【订单页】的静态资源。index.html重命名为list.html
  • order/confirm中放入【结算页】的静态资源。index.html重命名为confirm.html
  • order/pay中放入【收银页】的静态资源。index.html重命名为pay.html
  • 修改HOSTS,192.168.56.10 order.gulimall.com
  • nginx中已经配置过转发
  • 在gateway中新增order路由
  • 修改html中的路径/static前缀。比如/static/order/confirm
  • 注意一下有的同学在@GetMapping("/memverOrder.html")里的参数没有匹配好,第一个参数可以直接定义为String
  • 注意一下看看数据库里用户和订单表的对应情况,你登录该用户才能看到他的订单
订单概念

订单中心:

电商系统涉及到3流,分别是信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。

订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

img

订单状态
    1. 待付款

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

    1. 已付款/待发货

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

    1. 待收货/已发货

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

    1. 已完成
      用户确认收货后,订单交易完成。后续支付亻则进行结算,如果订单存在间题进入售后状态
    1. 已取消

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

    1. 售后中

用户在付款后申请退款,或商家发货后用户申请退换货。

售后也同样存在各种状态,

  • 当发起售后申请后生成售后订单,
  • 售后订单状态为待审核,等待商家审核,
  • 商家审核过后订单状态变更为待退货,等待用户将商品机会,
  • 商家收到货后订单
订单流程

订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与O2O订单等,所以需要根据不同的类型进行构建订单流程。

不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:

订单生成一>支付订单一>卖家发货一>确认收货一>交易成功。

而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图

img

六、服务通信数据共享问题

订单登录拦截

因为订单系统必然涉及到用户信息,因此进入订单系统的请求必须是已经登录的,所以我们需要通过拦截器对未登录订单请求进行拦截

  • 先注入拦截器HandlerInterceptor组件
  • 在config中实现WebMvcConfigurer接口.addInterceptor()方法
  • 拦截器和认证器的关系我在前面认证模块讲过,可以翻看,这里不赘述了
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
   
   

	public static ThreadLocal<MemberRespVo> threadLocal = new ThreadLocal<>();

	@Override
	public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
   
   

		String uri = request.getRequestURI();
		// 这个请求直接放行
		boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
		if(match){
   
   
			return true;
		}
		// 获取session
		HttpSession session = request.getSession();
		// 获取登录用户
		MemberRespVo memberRespVo = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
		if(memberRespVo != null){
   
   
			threadLocal.set(memberRespVo);
			return true;
		}else{
   
   
			// 没登陆就去登录
			session.setAttribute("msg", AuthServerConstant.NOT_LOGIN);
			response.sendRedirect("http://auth.gulimall.com/login.html");
			return false;
		}
	}
}
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
    }
}

加上ThreadLocal共享数据,是为了登录后把用户放到本地内存,而不是每次都去远程session里查

在auth-server中登录成功后会把会话设置到session中

MemberRespVo data = login.getData("data",new TypeReference<MemberRespVo>);
session.setAttribute(AuthServerConstant.LOGIN_USER,data);

异步线程的request数据与远程调用cookie的携带

1)新线程没有用户数据的问题RequestContextHolder

RequestContextHolder可以解决的问题:

  • 正常来说在service层是没有request和response的,然而直接从controlller传过来的话解决方法太粗暴。解决方法是SpringMVC提供的RequestContextHolder
  • 用线程池执行任务时非主线程是没有请求数据的,可以通过该方法设置线程中的request数据,原理还是用的threadlocal

RequestContextHolder推荐阅读:https://blog.youkuaiyun.com/asdfsadfasdfsa/article/details/79158459

在spring mvc中,为了随时都能取到当前请求的request对象,可以通过RequestContextHolder的静态方法getRequestAttributes()获取Request相关的变量,如request, response等

RequestContextHolder顾名思义,持有上下文的Request容器.使用是很简单的,具体使用如下:

//两个方法在没有使用JSF的项目中是没有区别的
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
//                                    RequestContextHolder.getRequestAttributes();
//从session里面获取对应的值
String str = (String) requestAttributes.getAttribute("name",RequestAttributes.SCOPE_SESSION);

HttpServletRequest  request  = ((ServletRequestAttributes)requestAttributes).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();

什么时候把request和response设置进去的:mvc的service()方法里有processRequest(request, response);,每个请求来了都会执行,

  1. 获取上一个请求的参数
  2. 重新建立新的参数
  3. 设置到XXContextHolder
  4. 父类的service()处理请求
  5. 恢复request
  6. 发布事件
2)远程调用丢失用户信息

feign远程调用的请求头中没有含有JSESSIONIDcookie,所以也就不能得到服务端的session数据,也就没有用户数据,cart认为没登录,获取不了用户信息

我们追踪远程调用的源码,可以在SynchronousMethodHandler.targetRequest()方法中看到他会遍历容器中的RequestInterceptor进行封装

Request targetRequest(RequestTemplate template) {
   
   
  for (RequestInterceptor interceptor : requestInterceptors) {
   
   
    interceptor.apply(template);
  }
  return target.apply(template);
}

根据追踪源码,我们可以知道我们可以通过给容器中注入RequestInterceptor,从而给远程调用转发时带上cookie

但是在feign的调用过程中,会使用容器中的RequestInterceptorRequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie

public class GuliFeignConfig {
   
   
    @Bean
    public RequestInterceptor requestInterceptor() {
   
   
        return new RequestInterceptor() {
   
   
            @Override
            public void apply(RequestTemplate template) {
   
   
                //1. 使用RequestContextHolder拿到老请求的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (requestAttributes != null) {
   
   
                    HttpServletRequest request = requestAttributes.getRequest();
                    if (request != null) {
   
   
                        //2. 将老请求得到cookie信息放到feign请求上
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
    }
}

注意:上面在封装cookie的时候要拿到原来请求的cookie,设置到新的请求中

RequestContextHolder为SpingMVC中共享request数据的上下文,底层由ThreadLocal实现,也就是说该请求只对当前访问线程有效,如果new了新线程就找不到原来request了

3)线程异步丢失上下文问题

P268

因为异步编排的原因,他会丢掉ThreadLocal中原来线程的数据,从而获取不到loginUser,这种情况下我们可以在方法内的局部变量中先保存原来线程的信息,在异步编排的新线程中拿着局部变量的值重新设置到新线程中即可。

由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie

在这种情况下,我们需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去

OrderServiceImpl.confirmOrder()代码

// 从主线程获取用户数据 放到局部变量中
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
   
   
    // 把旧RequestAttributes放到新线程的RequestContextHolder中
    RequestContextHolder.setRequestAttributes(attributes);
    // 远程查询所有的收获地址列表
    List<MemberAddressVo> address;
    try {
   
   
        address = memberFeignService.getAddress(MemberRespVo.getId());

此外远程获取价格的时候应该用R

七、订单确认页

1)订单确认页VO

点击"去结算"就会跳到订单确认页

  • 展示当前用户收获地址list
  • 所有选中的购物项list
  • 支付方式
  • 送货清单,价格也是最新价格,不是加入购物车时的价格
  • 优惠信息

跳转到确认页时需要携带的数据模型。

  • 要注意生成订单的时候,价格得重新算
  • 在后面的修改中,会让提交订单时不带着购物车数据,而是在后台重新 查询购物车选项。
  • 会带着总价,比对新总价和就总价是否一致
public class OrderConfirmVo {
   
    // 跳转到确认页时需要携带的数据模型。

    @Getter
    @Setter
    /** 会员收获地址列表 **/
    private List<MemberAddressVo> memberAddressVos;

    @Getter @Setter
    /** 所有选中的购物项 **/
    private List<OrderItemVo> items;

    /** 发票记录 **/
    @Getter @Setter
    /** 优惠券(会员积分) **/
    private Integer integration;

    /** 防止重复提交的令牌 **/
    @Getter @Setter
    private String orderToken;

    @Getter @Setter
    Map<Long,Boolean> stocks;

    public Integer getCount() {
   
    // 总件数
        Integer count = 0;
        if (items != null && items.size() > 0) {
   
   
            for (OrderItemVo item : items) {
   
   
                count += item.getCount();
            }
        }
        return count;
    }


    /** 计算订单总额**/
    //BigDecimal total;
    public BigDecimal getTotal() {
   
    
        BigDecimal totalNum = BigDecimal.ZERO;
        if (items != null && items.size() > 0) {
   
   
            for (OrderItemVo item : items) {
   
   
                //计算当前商品的总价格
                BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
                //再计算全部商品的总价格
                totalNum = totalNum.add(itemPrice);
            }
        }
        return totalNum;
    }


    /** 应付价格 **/
    //BigDecimal payPrice;
    public BigDecimal getPayPrice() {
   
   
        return getTotal();
    }
}
2)订单确认页数据获取
  • 异步:查询购物项(redis)、库存和收货地址(数据库)都要调用远程服务,串行会浪费大量时间,因此我们使用CompletableFuture进行异步编排
  • 为了防止多次重复点击“订单提交按钮”。我们在返回订单确认页时,在redis中生成一个随机的令牌,过期时间为30min,提交订单时会携带这个令牌,我们将会在订单提交的处理页面核验此令牌。

在购物车页面点击去结算,点击事件是window.location.href = "http://order.gulimall.com/toTrade";

返回订单确认页
// Order服务里的controller
@RequestMapping("/toTrade") // 用于返回订单确认页
public String toTrade(Model model) {
   
   
    // 内容是从登录用户里获取,所以不用带过来
    OrderConfirmVo confirmVo = orderService.confirmOrder();
    // 订单确认页要显示的数据
    model
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值