-
笔记-基础篇-1(P1-P28):https://blog.youkuaiyun.com/hancoder/article/details/106922139
-
笔记-基础篇-2(P28-P100):https://blog.youkuaiyun.com/hancoder/article/details/107612619
-
笔记-高级篇(P340):https://blog.youkuaiyun.com/hancoder/article/details/107612746
-
笔记-vue:https://blog.youkuaiyun.com/hancoder/article/details/107007605
-
笔记-elastic search、上架、检索:https://blog.youkuaiyun.com/hancoder/article/details/113922398
-
笔记-认证服务:https://blog.youkuaiyun.com/hancoder/article/details/114242184
-
笔记-分布式锁与缓存:https://blog.youkuaiyun.com/hancoder/article/details/114004280
-
笔记-集群篇:https://blog.youkuaiyun.com/hancoder/article/details/107612802
-
springcloud笔记:https://blog.youkuaiyun.com/hancoder/article/details/109063671
-
笔记版本说明:2020年提供过笔记文档,但只有P1-P50的内容,2021年整理了P340的内容。请点击标题下面分栏查看系列笔记
-
声明:
- 可以白嫖,但请勿转载发布,笔记手打不易
- 本系列笔记不断迭代优化,csdn:hancoder上是最新版内容,10W字都是在csdn免费开放观看的。
- 离线md笔记文件获取方式见文末。2021-3版本的md笔记打完压缩包共500k(云图床),包括本项目笔记,还有cloud、docker、mybatis-plus、rabbitMQ等个人相关笔记
-
本项目其他笔记见专栏:https://blog.youkuaiyun.com/hancoder/category_10822407.html
本篇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为对象所对应的值
参考京东,在点击购物车时,会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月,如果手动清除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.htmlorder/list中放入【订单页】的静态资源。index.html重命名为list.htmlorder/confirm中放入【结算页】的静态资源。index.html重命名为confirm.htmlorder/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流,分别是信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。
订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

订单状态
-
- 待付款
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要汪意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。
-
- 已付款/待发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调拨,配货,分拣,出库等操作。
-
- 待收货/已发货
仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态
-
- 已完成
用户确认收货后,订单交易完成。后续支付亻则进行结算,如果订单存在间题进入售后状态
- 已完成
-
- 已取消
付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
-
- 售后中
用户在付款后申请退款,或商家发货后用户申请退换货。
售后也同样存在各种状态,
- 当发起售后申请后生成售后订单,
- 售后订单状态为待审核,等待商家审核,
- 商家审核过后订单状态变更为待退货,等待用户将商品机会,
- 商家收到货后订单
订单流程
订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与O2O订单等,所以需要根据不同的类型进行构建订单流程。
不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:
订单生成一>支付订单一>卖家发货一>确认收货一>交易成功。
而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图

六、服务通信数据共享问题
订单登录拦截
因为订单系统必然涉及到用户信息,因此进入订单系统的请求必须是已经登录的,所以我们需要通过拦截器对未登录订单请求进行拦截
- 先注入拦截器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);,每个请求来了都会执行,
- 获取上一个请求的参数
- 重新建立新的参数
- 设置到XXContextHolder
- 父类的service()处理请求
- 恢复request
- 发布事件
2)远程调用丢失用户信息
feign远程调用的请求头中没有含有JSESSIONID的cookie,所以也就不能得到服务端的session数据,也就没有用户数据,cart认为没登录,获取不了用户信息

我们追踪远程调用的源码,可以在SynchronousMethodHandler.targetRequest()方法中看到他会遍历容器中的RequestInterceptor进行封装
Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}
根据追踪源码,我们可以知道我们可以通过给容器中注入RequestInterceptor,从而给远程调用转发时带上cookie
但是在feign的调用过程中,会使用容器中的RequestInterceptor对RequestTemplate进行处理,因此我们可以通过向容器中导入定制的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

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

被折叠的 条评论
为什么被折叠?



