微服务商城系统实战 购物车实现

一、加入购物车

1、解决请求参数中含特殊字符被截断问题

    点击“加入购物车”按钮后,需要把商品的 SKU id 和 数量传给后端接口http://localhost:18090/cart/add?num=xxx&xxx,在认证登录的地址中,需要把上一步所在的路径放在参数 FROM 中,比如:http://localhost:9001/oauth/login?FROM=http://localhost:8001/api/cart/add?num=1&id=1148477880913633280,对应的接口:

  @GetMapping(value = "/login")
    public String login(@RequestParam(value = "FROM")String from,Model model){
        model.addAttribute("from",from);
        return "login";
    }

但是这样获取参数,就会因为 & 符合而被阶段,FROM 只能拿到 http://localhost:8001/api/cart/add?num=1。改为:

@GetMapping(value = "/login")
    public String login(HttpServletRequest request,Model model){
        String queryString=request.getQueryString();
        String from=queryString.substring(5,queryString.length());
        System.out.println(from);
        model.addAttribute("from",from);
        return "login";
    }
2、解决访问微服务所在网关跨域问题

    在 item.html 中访问 http://localhost:8001/api/cart/add?num=2&id=1148477874492153856,这样会经过网关,会获取到令牌,然后校验,保证是用户登录后才使用的购物车,但是这样写,在 ajax 访问方法时,报了 401 的错误,因为网关所在的 8001 和 item 所在的端口号不同,跨域了。解决方法,在网关微服务所在的工程里添加配置:

package com.changgou.filter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class GwCorsFilter {

  @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true); // 允许cookies跨域
        config.addAllowedOrigin("*");// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
        config.addAllowedHeader("*");// #允许访问的头信息,*表示全部
        config.setMaxAge(18000L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.addAllowedMethod("OPTIONS");// 允许提交请求的方法类型,*表示全部允许
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");

        org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource source =
                new org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

    想从登录,授予令牌,然后访问商城首页,再由首页点进商品详情页,再点击加入购物车,需要令牌传递起来,就得让商城首页经过网关的过滤:

- id: changgou_search_web_route
              uri: lb://search-web
              predicates:
              - Path=/api/search/list
              filters:
              - StripPrefix=1
              - name: RequestRateLimiter
                  args:
                    key-resolver: "#{@ipKeyResolver}"
                    redis-rate-limiter.replenishRate: 1
                    redis-rate-limiter.burstCapacity: 4

    访问 http://localhost:8001/api/cart/add?num=1&id=1 ,出现了问题,无令牌时,它并没有跳转至登录页面:
在这里插入图片描述
    这是因为请求路径里带了参数的关系,而网关微服务中,只是匹配了路径:
    比如,不带参数,只是访问 http://localhost:8001/api/cart/add,它是会跳转到登录页面的。

解决方法:
在这里插入图片描述
    使用 - Query 。Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。
    这样配置,只要请求中包含 num 属性的参数即可匹配路由。
    还可以将 Query 的值以键值对的方式进行配置,这样在请求过来时会对属性值和正则进行匹配,匹配上才会走路由,比如 写成 -Query=num,1,那么,只有当请求参数为 ?num=1 时,才会进行匹配。
    
    从商城首页,点进商品详情页,然后点击“加入购物车”,这时访问的是 在这里插入图片描述
因为 8001/api/cart/add?num=xxx&id=xxx 是经过网关的,所以未携带令牌时需要跳转到登录页,至此逻辑是没有问题的,但是跳转到登录页面,需要访问 9001 端口,这时 跨域了 org。

好,这就解决跨域问题:
在这里插入图片描述
然后解决了跨域问题,又出现了新问题,因为这里并不是直接访问加入购物车接口,而是通过 ajax 使用 get 方法调用的:
在这里插入图片描述
访问 8001/api/cart/add?num=xxx&id=xxx,没有获取到令牌,那么,会跳转到 http://localhost:9001/oauth/login,但是呢,页面并不会跳转,它只是执行了这个方法,也就是进行了 get 请求,而已。而 8001/api/cart/add?num=xxx&id=xxx 路径的状态码为 303 seeother… 。后续可考虑进行一个判断,如果此时无令牌,就跳转至登录页面。目前的做法是,不使用 ajax 了,直接访问路径:
在这里插入图片描述

3、登录授权后令牌的传递

    在访问 http://localhost:9001/oauth/login 输入用户名和密码后,会在 Headers 中生成 Autherization ,而且,还有 Set-Cookie,此后,Cookie 中就会带有令牌信息:
在这里插入图片描述
这时的 Cookie 里还没有 Autherization 信息呢:
在这里插入图片描述
    但是再从登录成功跳转到商城首页,虽然商城首页的地址是 http://localhost:18086/search/list,并没有经过网关,但是因为在 Cookie 里有 Autherization 信息了,再从商城首页跳到详情页,再从详情页点击加入购物车按钮,都是带着 Cookie 信息的,然后 “加入购物车按钮” 访问的是 http://localhost:8001/api/cart/add?num=xxx&id=xxx ,是经过网关的,网关会进行过滤,虽然 Http Headers 里没有 Authorization,但是 Cookie 里有,所以,这时访问的实际地址是 order 微服务,是需要进行令牌校验的,而因为经过网关,会把获取到的令牌放置在 Http Headers 里,所以,检验通过。
    

二、查看购物车列表

查看购物车列表的控制层:

@RestController
@RequestMapping(value = "/cart")
public class CartController {
    @CrossOrigin
    @GetMapping("/list")
    public Result<List<OrderItem>> list(){

提供对应的 feign:

@FeignClient(name = "order")
@RequestMapping(value = "/cart")
public interface CartFeign {

    @GetMapping("/list")
    public Result<List<OrderItem>> list();
}

    
    进行调用:

@RestController
@RequestMapping("/page")
public class PageController {

    @Autowired
    private CartFeign cartFeign;

    @RequestMapping("/cart")
    public Result cartList() {
        Result<List<OrderItem>> list = cartFeign.list();
        return new Result(true,StatusCode.OK,"查询购物车成功",list);
    }
}
1、解决 feign.FeignException: status 401 reading xxx

运行结果:
在这里插入图片描述
控制台报错:
feign.FeignException: status 401 reading CartFeign#list()
at feign.FeignException.errorStatus(FeignException.java:78) ~[feign-core-10.1.0.jar:na]
    401 主要是没有权限,这个问题是因为调用 feign 时,没有走网关过滤,然后就没办法拿到令牌。解决方法:
提供 Feign 拦截器,从请求的 Headers 中获取所有的参数,并放置在 headers 中 :

public class FeignInterceptor implements RequestInterceptor {
    /**
     * Feign 执行之前进行拦截
     *
     * @param requestTemplate
     */
    @Override
    public void apply(RequestTemplate requestTemplate) {

        /***
         *  获取用户令牌
         */
        try {
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

            if (requestAttributes != null) {


                // 获取所有 Http Headers 的 key
                Enumeration<String> headerNames = requestAttributes.getRequest().getHeaderNames();

                if (headerNames != null) {
                    while (headerNames.hasMoreElements()) {
                        // Http Headers 的 key
                        String name = headerNames.nextElement();

                        // Http Headers 的 value
                        /*String values = request.getHeader(name);*/
                        String values = requestAttributes.getRequest().getHeader(name);

                        // 将令牌数据添加到 Http Headers 中
                        requestTemplate.header(name, values);

                        System.out.println(name + ":" + values);
                    }
                }
            }
        } catch (Exception e) {

        }
    }
}

在微服务启动类中生成该拦截器:
在这里插入图片描述
这还不够,需要把之前 hystrix 中的 enable:true 改成 SEMPHORE 模式:
在这里插入图片描述
再访问(需要携带令牌):
在这里插入图片描述
    OKK 解决。
    这时使用 Template 渲染页面:

@RestController
@RequestMapping("/page")
public class PageController {
 
    @RequestMapping("/cart")
    public String cartList(Model model) {
        Result<List<OrderItem>> list = cartFeign.list();
        model.addAttribute("list",list.getData());
        return "cart";
    }

运行效果:
在这里插入图片描述
    因为调用了 cartFeign,虽然上面提供了 Feign 的过滤器,但是,本来就没有令牌,所以 401 了, 而想让访问购物车列表时携带令牌,可以设计成 未登录不允许访问 的方式,把地址配置到网关中,网关没有拿到令牌,就跳转到登录页面。
在这里插入图片描述

2、@RestController 和 @Controller 的区别

    @RestController注解,相当于@Controller+@ResponseBody两个注解的结合,返回 json 数据不需要在方法前面加 @ResponseBody 注解了,但使用@RestController这个注解,就不能返回 jsp,html 页面,视图解析器无法解析 jsp,html 页面。
     @ResponseBody 表示该方法的返回结果直接写入 HTTP response body 中,一般在异步获取数据时使用【也就是AJAX】,在使用 @RequestMapping后,返回值通常解析为跳转路径,但是加上 @ResponseBody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP response body 中。
    所以,使用 thymeleaf 渲染页面时,控制层不能写成 @RestController,一定要是 @Controller。

3、解决 thymeleaf 渲染页面找不到 css 样式

     具体的报错是 Failed to load resource: the server responded with a status of 404 ():
在这里插入图片描述
在 html 里的配置是:

<link rel="stylesheet" type="text/css" href="/css/all.css"/>
<link rel="stylesheet" type="text/css" href="/css/pages-cart.css"/>

    css 是放在 static 包下的,但是却访问不到。原因是在 application.yml 里,我对静态资源路径的配置是:

Spring:
  resources:
    static-locations: classpath:/

其实在 Spring Boot 中,默认的静态资源路径配置就是classpath:/static。
     不建议修改静态资源文件的访问目录为 classpath:/,因为这会带来一个隐患,就是 classpath 下的所有文件都是可以被访问到的。这个工程里这么写主要是因为有 item.html,会生成静态页面,可能会获取到 static 外的文件。

     解决方法:修改为

<link rel="stylesheet" type="text/css" href="/static/css/all.css"/>
<link rel="stylesheet" type="text/css" href="/static/css/pages-cart.css"/>

我以为我解决了,其实还有问题:
在这里插入图片描述
    这下确实是到 static 包下找了,好过之前直接去的 css 包,但是,还是没找到,因为希望是登录了的用户才能使用购物车,所以使用的是经过网关的,所以它是处在 8001 端口下的,这时需要给 8001 所在的服务下提供 static 资源。
在这里插入图片描述
这样 css 样式就能加载正常了:
在这里插入图片描述
小计的数据是写死的:

<ul class="goods-list yui3-g"  th:each="goods:${cartList}">
<li class="yui3-u-1-8">
    <span class="sum" th:text="count(${goods.price})"></span>
 </li>

需要做个计算,由用户输入数量,使用 onblur 方法,当输入框失去焦点时,设置 “小计” 的值,
注意,不能直接这样获取值:

function total(){
		var num=document.getElementById("goodsnum");
		var price=document.getElementById("SinglePrice");
		document.getElementById("total").innerText=num*price;
	}

这样求出来的是 NaN,因为使用 th:text 渲染的,没有赋值,需要用 th:value 赋予值:


<span class="price" th:text="${goods.price}" th:id="SinglePrice" th:value="${goods.price}"></span>
<input autocomplete="off" type="text" value="1" minnum="1" class="itxt" th:id="goodsnum"
										   th:onblur="totalPrice()"/>
function totalPrice(){
		var price=document.getElementById("SinglePrice").value;
		console.log(price);
		var num=document.getElementById("goodsnum").value;
		alert("num:"+num+",price:"+price);
		document.getElementById("total").innerText=num*price;
	}
4、span 获取 value

奇怪的是,还是无法得到总价,明明可以在控制台获取到 span 的 value,但是使用 var price=document.getElementById(“SinglePrice”).value: 获取到的是 undefined:
在这里插入图片描述

var price=document.getElementById("SinglePrice").getAttribute("value");

    如果是 jquery 的话,是 $(“SinglePrice”).attr(“value”);
    

5、th:each 使用注意事项

    然后还有一个问题,这时虽然用的是 th:onblur ,但是并不是对每一行记录起作用,不管哪一行,鼠标离开输入框,都获取到的是第一行记录的单价和数目。解决方法:参考之前后台管理页面:
在这里插入图片描述
    把单价和数量传过去,还不够,id 也是要传的,不然都是给第一行赋值,这就是使用 th:each 要注意的事项,比如说表格有好几行,必须给每一行一个 id,这样才能区别操作,只是使用 th:id=“total” 和 getElementById(total)是行不通的,只对第一行有效,在我的这篇博客里也有提到:https://blog.csdn.net/weixin_41750142/article/details/116608586
详细代码:
根据 total+id 显示总金额:

<script type="text/javascript">
	function totalPrice(price,num,id){
		document.getElementById('total'+id).innerText=num*price;
	}
</script>
<li class="yui3-u-6-24">
	<div class="good-item">
		<div class="item-img">
			<img th:src="${goods.image}"/>
		</div>
		<div class="item-msg" th:text="${goods.name}"></div>
	</div>
</li>
<li class="yui3-u-5-24">
	<div class="item-txt"></div>
</li>
<li class="yui3-u-1-8">
	<span class="price" th:text="${goods.price}" th:id="SinglePrice" th:value="${goods.price}"></span>
</li>
<li class="yui3-u-1-8">
	<input type="text" th:value="1" minnum="1" class="itxt"
		   th:onblur="totalPrice([[${goods.price}]],this.value,[[${goodsState.index}]])" />
</li>
<li class="yui3-u-1-8">
	<span class="sum" th:id="'total'+${goodsState.index}" th:text="${goods.price}*${goods.num}"></span></span>
</li>

    在 thymeleaf 中,是可以直接使用加减乘除的,比如 “小计” 里,th:text="${goods.price}*${goods.num}"
    页面效果如下所示:
在这里插入图片描述

6、th:if 判断集合是否为空

    还要注意,当用户购物车中没有任何商品时,需要进行判空处理,弹框提示。使用 th:if
判断集合不为空:

th:if="not ${#lists.isEmpty(cartList)}

以及判断集合为空:

th:if=${#lists.isEmpty(cartList)}

    

三、删除购物车中商品

1、jquery 处理复选框 checkedbox

     同样地,删除商品也是需要跟 id 关联的,有多个复选框 checkbox 时,需要给个 name,然后遍历所有的复选框,判断复选框是否被选中,先试试在控制台打印选中的行的 id,使用 jquery(我的版本是 2.4.16 ) ,试啊试,找到了一个方式:

<input type="checkbox" name="chk_list" th:id="'checkbox'+${goodsState.index}" value=""
									checked="checked"/>

提供专属的 id,checked 设置成 checked,也就是说,默认是选中的(也且符合购物车的需求),在 jquery 的 $function() 中处理,比如,第 2 行中的复选框:

document.getElementById("checkbox1").checked)

     默认所有行是选中的,所以 document.getElementById("checkbox1").checked 值为 true,如果取消勾选,值就是 false 了。
    换个写法,比如

	var boxs=$("input[name='chk_list']");
	var boxNum = boxs.length;
 	 for (var i=0; i<boxNum; i++){
       console.log(boxs[i].getAttribute("checked"));
}

或者 什么 attr … … 并没有用。
    
还有一个地方,“总价” 处应该显示用户已经勾选的商品的总价,默认是小计的总和,先给 div 一个 id:

<div class="sumprice">
		<span>
			<em>总价(不含运费) :</em>
                 <!--默认是-->
					<i class="summoney" id="summoney"></i>
		</span>
</div>

计算并给 总价 赋值的方法写在 $(function () 里:

$(function () {
        console.log(document.getElementById("checkbox1").checked);
        // 打开页面时总价为所有小计之和
        var summoney = 0;
        // 遍历
        var num = document.getElementsByClassName("sum").length;
        for (var i = 0; i < num; i++) {
            summoney = summoney + Number(document.getElementById("total" + i).innerText);
        }
        document.getElementById("summoney").innerText = summoney;
    }

而复选框有变化(选中或者取消选中)时,需要对总价进行重新赋值,给复选框提供 onclick 方法:

 <li class="yui3-u-1-24">
     <input type="checkbox" name="chk_list" th:id="'checkbox'+${goodsState.index}" onclick="checkbpxFun()"     checked="checked"/>
</li>
  function checkbpxFun(){
        var summoney = 0;
        // 遍历
        var num = document.getElementsByClassName("sum").length;
        for (var i = 0; i < num; i++) {
            if(document.getElementById("checkbox"+i).checked)
            summoney = summoney + Number(document.getElementById("total" + i).innerText);
        }
        document.getElementById("summoney").innerText = summoney;

    };
2、html 中删除节点

     删除商品,既对页面中的记录进行删除,又要对 Redis 中的记录进行删除。对页面删除行需要先获取到父节点,然后执行 即可。

先给购物车列表所在的 div 一个 id,还有每一行也需要一个 id:
在这里插入图片描述

然后提供方法:

function deleteCart(){

		// 获取所有复选框
		var boxs=$("input[name='chk_list']");

		// 复选框个数
		var boxNum = boxs.length;

		// 遍历复选框
		for(var i=0;i<boxNum;i++){
			
			// 复选框被选中
			if(document.getElementById("checkbox"+i).checked){
				console.log(i);
				
				// 获取父节点
				var parent=document.getElementById("cart-list");
				
				// 删除子节点
				parent.removeChild(document.getElementById("ul"+i));
			}
		}
	}

页面删除后,还需要调用后端接口对数据进行删除,需要把用户购物车的整个商品列表传过来,然后根据复选框选中的行,获取到对应的下标,进而获取到商品的 skuid:

<a th:onclick="deleteCart([[${cartList}]])" class="sum-btnn">删除选中的商品</a>
function deleteCart(cartlist){
		// 获取所有复选框
		var boxs=$("input[name='chk_list']");

		// 复选框个数
		var boxNum = boxs.length;

		// 遍历复选框
		for(var i=0;i<boxNum;i++){
			// 被选中
			if(document.getElementById("checkbox"+i).checked){
				/**页面删除节点**/
				
				// 调用后端接口删除商品
				var good=cartlist[i];
				console.log(good.skuId);
				
				$.ajax(
						{
							url: "http://localhost:18090/cart/delete/"+skuid,
							type: "GET",
							dataType: "JSON",
							contentType: "application/json;charset=UTF-8",
							success: function (result) {
								alert("删除商品成功");
							},
							error: function (result) {
								alert("删除商品失败");
							},
							cache: false
						}
				)
			}
		}
	}

直接去访问 18090 端口,也就是 order 服务,有一个问题,报 401 了,因为 没有经过网关过滤,拿不到令牌,所以,把 /cart/delete 路径在 order 工程里放行:
在 order 工程的 ResourceServerConfig 类中:

  public void configure(HttpSecurity http) throws Exception {
        //所有请求必须认证通过
        http.authorizeRequests()
                .antMatchers("/cart/delete/**")
                .permitAll()
                .anyRequest()
                .authenticated();    //其他地址需要认证授权
    }

这样 delete 路径就被放行了,就可以不带令牌地访问了。然鹅… … 删除商品是需要用户名的,所以… 必须有令牌,得让网关过滤:
在这里插入图片描述
注意 *,否则过滤无效。

在网关微服务中进行配置:

 - id: changgou_delete_cart_route
              uri: lb://order
              predicates:
              - Path=/api/cart/delete/*
              filters:
              - StripPrefix=1
 

同样需要注意 * 。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值