文章目录
- 一、加入购物车
- 1、解决请求参数中含特殊字符被截断问题
- 2、解决访问微服务所在网关跨域问题
- 3、登录授权后令牌的传递
- 二、查看购物车列表
- 1、解决 feign.FeignException: status 401 reading xxx
- 2、@RestController 和 @Controller 的区别
- 3、解决 thymeleaf 渲染页面找不到 css 样式
- 4、span 获取 value
- 5、th:each 使用注意事项
- 6、th:if 判断集合是否为空
- 三、删除购物车中商品
- 1、jquery 处理复选框 checkedbox
- 2、html 中删除节点
一、加入购物车
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)是行不通的,只对第一行有效,在我的这篇博客里也有提到:。
详细代码:
根据 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
同样需要注意 * 。