谷粒商城之高级篇
谷粒商城之高级篇
@
- 谷粒商城之高级篇
- 前言
- 2 商城业务
- {subscribe.submit}:属性值叫什么
- 2.3 检索业务
- 2.4 商品详情
- 2.5 认证服务
- 2.6 购物车服务
- 2.7 订单服务
- 2.7.1 环境搭建
- 2.7.2 整合SpringSession
- 2.7.3 订单基本概念
- 2.7.4 订单登录拦截
- 2.7.5 订单确认页模型抽取
- 2.7.6 订单确认页数据获取
- 2.7.7 Feign远程调用丢失请求头问题
- 2.7.8 Feign异步调用丢失上下文的问题
- 2.7.9 订单确认页渲染
- 2.7.10 订单确认页库存查询
- 2.7.11 订单确认页模拟运费效果
- 2.7.12 订单确认页细节显示
- 2.7.13 接口幂等性讨论
- 2.7.14 订单确认页完成
- 2.7.15 原子验证令牌
- 2.7.16 构造订单数据
- 2.7.17 构造订单项数据
- 2.7.18 订单验价
- 2.7.19 保存订单数据
- 2.7.20 锁定库存
- 2.7.21 提交订单的问题
前言
高级篇正式开始啦!
PS 第一章 ElasticSearch 参见 另外一篇文章 谷粒商城之高级篇知识补充。
2 商城业务
2.1 商品上架
2.1.1 商品Mapping
ES是将数据存储在内存中,所以在检索中优于mysql。ES也支持集群,数据分片存储。
需求:
上架的商品才可以在网站上展示,没有上架的商品存储在数据库中。
上架的商品需要可以被检索。
分析sku在es中如何存储:
商品mapping
分析:商品上架在es中是存sku还是spu?
1)检索的时候输入名字,是需要按照sku的title进行全文检索的
2)检索使用商品规格,规格是spu的公共属性,每个spu是一样的
3)按照分类id进去的都是直接列出spu的,还可以切换。
4〕我们如果将sku的全量信息保存到es中(包括spu属性〕就太多字段了
方案1:方便检索
{
skuId:1
spuId:11
skyTitile:华为xx
price:999
saleCount:99
attr:[
{尺寸:5},
{CPU:高通945},
{分辨率:全高清}
]
}
缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu对应的sku的规格参数都一样
冗余:
举例:100万*20=2000MB=2G
方案2:分布式
sku索引
{
spuId:1
skuId:11
xxx
}
attr索引
{
skuId:11
attr:[
{尺寸:5},
{CPU:高通945},
{分辨率:全高清}
]
}
举例:
先找到4000个符合要求的spu,再根据4000个spu查询对应的属性,封装了4000个id,long 8B*4000=32000B=32KB
1K个人检索,就是32MB
结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络拥堵
因此选用方案1,以空间换时间
![1669197427060](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669197427060.png)
建立product索引
最终选用的数据模型:
PUT product
{
"mappings":{
"properties": {
"skuId":{ "type": "long" },
"spuId":{ "type": "keyword" }, # 不可分词
"skuTitle": {
"type": "text",
"analyzer": "ik_smart" # 中文分词器
},
"skuPrice": { "type": "keyword" },
"skuImg" : { "type": "keyword" },
"saleCount":{ "type":"long" },
"hasStock": { "type": "boolean" },
"hotScore": { "type": "long" },
"brandId": { "type": "long" },
"catalogId": { "type": "long" },
"brandName": {"type": "keyword"},
"brandImg":{
"type": "keyword",
"index": false, # 不可被检索,不生成index
"doc_values": false # 不可被聚合
},
"catalogName": {"type": "keyword" },
"attrs": { # attrs:当前sku的属性规格
"type": "nested",
"properties": {
"attrId": {"type": "long" },
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {"type": "keyword" }
}
}
}
}
}
其中
“type”: “keyword” 保持数据精度问题,可以检索,但不分词
“index”:false 代表不可被检索
“doc_values”: false 不可被聚合,es就不会维护一些聚合的信息
冗余存储的字段:不用来检索,也不用来分析,节省空间
库存是bool。
检索品牌id,但是不检索品牌名字、图片
用skuTitle检索
nested嵌入式对象
属性是"type": “nested”,因为是内部的属性进行检索
数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)
user.name=["aaa","bbb"]
user.addr=["ccc","ddd"]
这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的
数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)
![1669204852792](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669204852792.png)
nested阅读:https://blog.csdn.net/weixin_40341116/article/details/80778599
参考使用聚合:https://blog.csdn.net/kabike/article/details/101460578
课件内容:
分析:商品上架在es 中是存sku 还是spu?
1)、检索的时候输入名字,是需要按照sku 的title 进行全文检索的
2)、检索使用商品规格,规格是spu 的公共属性,每个spu 是一样的
3)、按照分类id 进去的都是直接列出spu 的,还可以切换。
4)、我们如果将sku 的全量信息保存到es 中(包括spu 属性)就太多量字段了。
5)、我们如果将spu 以及他包含的sku 信息保存到es 中,也可以方便检索。但是sku 属于
spu 的级联对象,在es 中需要nested 模型,这种性能差点。
6)、但是存储与检索我们必须性能折中。
7)、如果我们分拆存储,spu 和attr 一个索引,sku 单独一个索引可能涉及的问题。
检索商品的名字,如“手机”,对应的spu 有很多,我们要分析出这些spu 的所有关联属性,
再做一次查询,就必须将所有spu_id 都发出去。假设有1 万个数据,数据传输一次就
10000*4=4MB;并发情况下假设1000 检索请求,那就是4GB 的数据,,传输阻塞时间会很
长,业务更加无法继续。
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数
据库范式。
index:
默认true,如果为false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能
当做检索条件。
doc_values:
默认true,设置为false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。
还可以通过设定doc_values 为true,index 为false 来让字段不能被搜索但可以用于排序、聚
合以及脚本操作:
2.1.2 上架细节
上架是将后台的商品放在es 中可以提供检索和查询功能。
1)、hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要
更新一下es
2)、库存补上以后,也需要重新更新一下es
3)、hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新
热度值。
4)、下架就是从es 中移除检索项,以及修改mysql 状态
商品上架步骤:
1)、先在es 中按照之前的mapping 信息,建立product 索引。
2)、点击上架,查询出所有sku 的信息,保存到es 中
3)、es 保存成功返回,更新数据库的上架状态信息。
2.1.3 数据一致性
1)、商品无库存的时候需要更新es 的库存信息
2)、商品有库存也要更新es 的信息
2.1.4 代码实现
POST /product/spuinfo/{spuId}/up
- SpuInfoController:
/**
* /product/spuinfo/{spuId}/up
* 商品上架功能
*/
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
product里组装好,search里保存到es中,进行商品上架
- 商品上架entity
商品上架需要在es中保存spu信息并更新spu的状态信息,由于SpuInfoEntity与索引的数据模型并不对应,所以我们要建立专门的vo进行数据传输。
//商品在 es中保存的数据模型
@Data
public class SkuEsModel {
private Long skuId;
private Long spuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Long saleCount;
private Boolean hasStock;
private Long hotScore;
private Long brandId;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attrs> attrs;
@Data
public static class Attrs {
private Long attrId;
private String attrName;
private String attrValue;
}
}
- 商品上架service
sku的规格参数相同,因此我们要将查询规格参数提前,只查询一次
1)在ware微服务里添加"查询sku是否有库存"的controller
WareSkuController
//查询sku 是否有库存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List<Long> skuIds){
//sku_id,stock
List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
return R.ok().setData(vos);
}
WareSkuServiceImpl
@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
SkuHasStockVo vo = new SkuHasStockVo();
//查询当前 sku的总库存量
//SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id = 1
Long count = baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count==null?false:count>0);
return vo;
}).collect(Collectors.toList());
return collect;
}
WareSkuDao
Long getSkuStock(Long skuId);//一个参数的话,可以不用写@Param,多个参数一定要写,方便区分
WareSkuDao.xml
</update>
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id = #{skuId}
</select>
SkuHasStockVo
@Data
public class SkuHasStockVo {
private Long skuId;
private Boolean hasStock;
}
然后用feign调用
在 package com.atguigu.gulimall.product.feign下:
@FeignClient("gulimall-ware") //说明调用哪一个 远程服务
public interface WareFeignService {
/**
* 1、R设计的时候可以加上泛型
* 2、直接返回我们想要的结果
* 3、自己封装解析结果
* @param skuIds
* @return
*/
@PostMapping("/ware/waresku/hasstock")//注意路径复制完全
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
2)将 R 工具类进行改装
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
//利用 阿里巴巴提供的fastjson 进行逆转
public <T> T getData(TypeReference<T> typeReference){
Object data = get("data");//默认是map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public R setData(Object data){
put("data",data);
return this;
}
...
3)收集成map的时候,toMap()参数为两个方法,如 SkyHasStockVo::getSkyId,item->item.getHasStock()
将封装好的SkuInfoEntity,调用search的feign,保存到es中
![1669219523241](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669219523241.png)
ElasticSaveController
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
//上架商品
// 添加@RequestBody 将 请求体中的 List<SkuEsModel> 集合转换为json数据,因此请求方式必须为 @PostMapping
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels){
// 如果返回的是 boolean 类型的false,说明我们的 sku数据有问题
//如果返回的是 catch里面的内容,可能是 es 客户端连接不上了
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModels);
}catch (Exception e){
log.error("ElasticSaveController商品上架错误: {}",e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if (!b){
return R.ok();
}else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
ProductSaveServiceImpl
@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1.给 es 中建立索引。product,建立好映射关系。
//2.给 es 中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel model : skuEsModels) {
//1.构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(model.getSkuId().toString());
String s = JSON.toJSONString(model);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
//BulkRequest bulkRequest, RequestOptions options
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//TODO 1、如果批量错误
boolean b = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.info("商品上架完成:{},返回数据:{}",collect,bulk.toString());
return b;
}
}
EsConstant
public class EsConstant {
public static final String PRODUCT_INDEX = "product"; //sku数据在 es中的索引
}
fenign 调用: gulimall-product 调用 gulimall-search
SearchFeignService
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
4)上架失败返回R.error(错误码,消息)
此时再定义一个错误码枚举。在接收端获取他返回的状态码
BizCodeEnume
PRODUCT_UP_EXCEPTION(11000,"商品上架异常");
5)上架后再让数据库中变为上架状态
这里在 gulimall-common 包下的 ProductConstant 创建一个新的枚举类
public class ProductConstant {
...
public enum StatusEnum {
NEW_SPU(0,"新建"), SPU_UP(1,"商品上架"),SPU_DOWN(2,"商品下架");
private int code;
private String msg;
StatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
}
6)mybatis为了能兼容接收null类型,要把long
改为Long
debug时很容易远程调用异常,因为超时了
商品上架代码
SpuInfoController
/**
* /product/spuinfo/{spuId}/up
* 商品上架功能
*/
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
SpuInfoServiceImpl
@Override
public void up(Long spuId) {
//1.查出当前 spuid 对应的所有 sku信息、品牌的名字
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//TODO 4、查询当前sku的所有可以用来被检索的规格属性,
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> { //返回所有属性的id
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);//因为是kv 键值对,转换成 set 集合比较方便
// 从 baseAttrs 集合中 过滤 出 attrValueEntities 集合
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> { //将 set集合 映射 成 map集合
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);//属性对拷:item 是数据库中查出来的数据
return attrs;
}).collect(Collectors.toList());
//TODO 1、发送远程调用,库存系统查询是否有库存
//由于远程调用可能出现网络问题,所以需要进行try - catch处理一下
Map<Long, Boolean> stockMap = null;
try {
R r = wareFeignService.getSkuHasStock(skuIdList);
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>(){
};
stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
}catch (Exception e){
log.error("库存服务查询异常:原因{}",e);
}
//2.封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> upProducts = skus.stream().map(sku -> {//通过 stream API 将 skus中的 数据遍历
//组装我们需要的数据
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku, esModel);//属性对拷,将 sku中的属性 拷贝到 esmodel中
//需要单独处理的数据 ,SkuInfoEntity 和 SkuEsModel中相比少的数据。
//skuPrice,skuImg
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//hotScore(热度评分) hasStock(库存)
//设置库存信息
//如果远程调用出现问题,默认给 true值;如果没有问题,那就赋真正的值
if (finalStockMap == null){
esModel.setHasStock(true);
}else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//TODO 2、热度评分。0
esModel.setHotScore(0L);//这里的热度评分应该是一个比较复杂的操作,这里简单处理一下
//TODO 3、查询品牌和分类的名字信息
//品牌
BrandEntity brand = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brand.getName());
esModel.setBrandImg(brand.getLogo());
//分类
CategoryEntity category = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(category.getName());
//设置检索属性
esModel.setAttrs(attrsList);
return esModel;
}).collect(Collectors.toList());
//TODO 5、将数据发送给 es 进行保存,gulimall-search
R r = searchFeignService.productStatusUp(upProducts);
if (r.getCode() == 0){
//远程调用成功
//TODO 6、修改当前spu的状态
baseMapper.updataSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
}else {
//远程调用失败
//TODO 7、重复调用?接口幂等性;重试机制? xxx
//Feign 调用流程
/**
* 1.构造请求数据,将对象转为json;
* RequestTemplate template = buildTemplateFromArgs.create(argv);
* 2.发送请求进行执行(执行成功会解码响应数据);
* executeAndDecode(template)'
* 3.执行请求会有重试机制
* while(true){
* try{
* executeAndDecode(template);
* }catch(){
* try{ retryer.continueOrPropagate(e);}catch(){throw ex;
* continue;
* }
* }
*
*/
}
}
![1669257801032](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669257801032.png)
Feign
![1669219830792](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669219830792.png)
这里再次 将 feign 接口代码展示出来:
gulimall-product 调用 gulimall-search 将 商品上架内容保存在 ElasticSearch中,方便全文检索:
SearchFeignService
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
gulimall-product 调用 gulimall-ware 将 查询 商品库存:
WareFeignService
@FeignClient("gulimall-ware") //说明调用哪一个 远程服务
public interface WareFeignService {
/**
* 1、R设计的时候可以加上泛型
* 2、直接返回我们想要的结果
* 3、自己封装解析结果
* @param skuIds
* @return
*/
@PostMapping("/ware/waresku/hasstock")//注意路径复制完全
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
ps:
这里可以用到的idea 快捷键:
- ctrl + e 可以快速调出最近使用的(打开最近修改的文件)
![1669258047712](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669258047712.png)
快速从 controller 跳转 到 实现类
ctrl + shift + 鼠标左键
从 controller 跳转到 接口
ctrl + 鼠标左键
生成 try-catch等(surround with)
alt + shift +z
生成构造器/get/set/toString
alt + shift + s
7)效果展示
商品成功上架,显示状态 为 已上架
![1669277105482](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669277105482.png)
2.2 商城系统首页
![1669277707859](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669277707859.png)
不使用前后端分离开发了,管理后台用vue
nginx发给网关集群,网关再路由到微服务静态资源放到nginx中
2.2.1 渲染首页
- 依赖
导入thymeleaf依赖
<!--模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- html\首页资源\index 放到 gulimall-product 下的static文件夹
index.html 放到 templates中
![1669279333669](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669279333669.png)
- 关闭thymeleaf缓存,方便开发实时看到更新
thymeleaf:
cache: false
- web开发放到web包下,原来的controller是前后分离对接手机等访问的,所以可
以改成app,对接app应用。
web 包:存放专门进行页面跳转的controller
rest 接口对接的使我们分离的项目(比如手机的一些 app ):将controller 改名为 app
- 效果展示:访问首页
![1669279445355](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669279445355.png)
2.2.2 渲染一级分类数据
编写 处理首页的controller
gulimall-product的 web 包下新建 IndexController
@Controller
public class IndexController {
@Autowired
CategoryService categoryService;
@GetMapping({"/","/index.html"})
public String indexPage(Model model){
//TODO 1.查出所有的1级分类
List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();
//spring mvc提供了一个 model 接口
// 给 model 中放的数据,就会默认放到页面的请求域中,因为是转发。所以使用addAttribute
//给首页 放一个属性 ,属性名: categorys 属性值:categoryEntities------以后来到 index页面,就可以直接取出 属性。
model.addAttribute("categorys",categoryEntities);
// 如果返回的 是 逻辑视图(也就是页面地址) ,就会进行拼串
//视图解析器进行拼串:
//classpath:/ 表示类路径下 :resources下:文件夹右下角 有一个小图标
//默认规则:默认前缀:public static final String DEFAULT_PREFIX = "classpath:/templates/";
// 默认后缀:public static final String DEFAULT_SUFFIX = ".html";
// classpath:/templates/ + 返回值 + .html
return "index";
}
}
编写 获取 1级分类的实现
CategoryServiceImpl
/**
* 查找 1级分类
* parent_cid = 0 或者 cat_level = 1
* @return
*/
@Override
public List<CategoryEntity> getLevel1Categorys() {
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
引入 热部署依赖devtools使页面实时生效
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
首页遍历一级分类菜单数据
修改 index.html
<!--轮播主体内容-->
<div class="header_main">
....
<div class="header_main_left">
<ul>
<li th:each="category : ${categorys}">
<a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}" ><b th:text="${category.name}">家用电器</b></a>
</li>
</ul>
</div>
......
thymeleaf 知识小补充(复习):
thymeleaf官网:https://www.thymeleaf.org/
- ${}:动态取值
th:text="${category.name}"
- th:each:遍历
<tr th:each="prod : ${prods}">
prod : 当前元素
${prods}:要遍历的对象
th:each="category : ${categorys}"
- 自定义属性:我们需要获得 分类的 id
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
value:属性名叫什么
{subscribe.submit}:属性值叫什么
th:attr="ctg-data=${category.catId}"
原生属性:
th:value="#{subscribe.submit}"
效果展示:
![1669283016658](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669283016658.png)
2.2.3 渲染二级三级分类数据
当 鼠标滑到 1级分类时,展示 它的二级分类数据及三级分类数据。
利用 catalogLoader.js
来获取请求,解析展示数据。
![1669302197638](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669302197638.png)
按照 此json 数据方式
![1669302065051](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669302065051.png)
新建 Catelog2Vo封装 数据
/**
* 2级分类 vo
*
* @author wystart
* @create 2022-11-24 21:53
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {
private String catalog1Id;//1级分类id
private List<Catelog3Vo> catalog3List; //三级子分类
private String id;
private String name;
/**
* 三级分类 vo
* "catalog2Id":"61",
* "id":"610",
* "name":"商务休闲鞋"
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class Catelog3Vo {
private String catalog2Id; //父分类,2级分类 id
private String id;
private String name;
}
}
IndexController
//index/catalog.json
@ResponseBody
@GetMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatalogJson() {
Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson();
return catalogJson;
}
CategoryServiceImpl
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//1.查出所有1级分类
List<CategoryEntity> level1Categorys = getLevel1Categorys();
//2.封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1.每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getParentCid()));
//2.封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//1.找到当前二级分类的三级分类,封装成 vo
List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
// 三级分类有数据的情况下
if (level3Catelog != null){
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
//2.封装成指定格式
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return parent_cid;
}
效果展示:
访问 http://localhost:10000/index/catalog.json
得到 json 数据
![1669301819816](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669301819816.png)
首页展示效果:http://localhost:10000
![1669301854810](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669301854810.png)
模板引擎总结
* 5.模板引擎 * 1)、thymeleaf-starter: 关闭缓存 * 2)、静态资源都放在static 文件夹下就可以按照路径直接访问 * 3)、页面放在 templates下,直接访问 * SpringBoot,访问项目的时候,默认会找 index * 4)、页面修改不重启服务器实时更新 * 1)、引入 dev-tools * 2)、修改完页面 ctrl + shift + f9 或者 ctrl + f9,重新自动编译下页面(注意:如果代码配置等修改,建议重启) *
2.2.4 nginx 搭建域名访问环境
![1669306447448](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669306447448.png)
我们利用反向代理:让 Nginx 配合网关 搭建我们的访问环境,将我们的各个微服务放在内网中,避免端口直接暴露带来的危险。
利用 SwitchHosts软件可以快速修改hosts文件,注意需要以管理员身份运行。
![1669303542850](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669303542850.png)
原理:
![1669306617881](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669306617881.png)
查看本机localhost对应的IP地址:
ipconfig:查看本机IP:ipconfig:
192.168.1.103 (windows的localhost地址)
192.168.56.1(linux虚拟机的localhost地址)
两者都可以,都算本机
![1669304892211](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669304892211.png)
Nginx的配置文件详解:
![1669306432874](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669306432874.png)
![1669306804896](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669306804896.png)
可以加载 外部配置文件的配置,这样可以避免Nginx的配置文件过大。(总配置文件)
- Nginx 反向代理配置
接下来我们配置 server块:
先复制一份,留作备份:
![1669306701061](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669306701061.png)
修改配置文件:
![1669305063974](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669305063974.png)
proxy_pass:代理通过:相当于代理给谁(转交给谁)
gulimall下的所有请求都代理给 192.168.56.1下的10000端口。
![1669305318783](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669305318783.png)
Nginx的所有配置都以 ; 结尾,否则报错。
通过域名访问:gulimall.com
![1669305370580](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669305370580.png)
原理解析:
1.首先浏览器访问 gulimall.com----我们在windows里面指定了 gulimall.com 映射的是虚拟机IP:192.168.56.10,所以浏览器访问 gulimall.com 先会来到我们的虚拟机;
2.虚拟机里面的 Nginx又监听了80端口,在Nginx的配置文件中,它监听了来自80端口的所有请求,而且域名是 gulimall.com;所以符合以上条件,Nginx就会帮我们代理到我们本机:proxy_pass http://192.168.56.1:10000;
3.最后我们就又回到了本机
4.最后总结就是:域名来到 Nginx,Nginx 配置了gulimall.com ,代理到10000端口服务;
分布式情况下:商城系统有很多,不止一个,那需要每次修改 Nginx的代理配置?
太麻烦!!!
让Nginx 将请求代理给网关,由网关自动转发给我们各个服务;网关就能动态发现哪些服务上线,哪些服务下线;而且网关还具有负载均衡功能。
Nginx将请求交给网关,由网关从注册中心动态发现商品服务都在那,进而由网关负载均衡到商品服务;
网关也会部署多个,Nginx可以将请求负载均衡到某一个网关,然后由网关在进行转发。
-
Nginx 搭配网关 实现 负载均衡到网关
-
Nginx
修改 总配置 nginx.conf 在 http 块内:
![1669341418073](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669341418073.png)
在server 块内:
修改 server配置:gulimall.conf:相当于 是 负载均衡的配置,直接路由到上游服务器网关,由网关进行转发
![1669341538959](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669341538959.png)
效果就是:访问 gulimall.com ,代理 给 Nginx ,Nginx 转交 给网关 ,网关再转给商品服务。
-
网关配置:
- id: gulimall_host_route uri: lb://gulimall-product predicates: - Host=**.gulimall.com,gulimall.com # 只要是 gulimall下的所有请求都转给 gulimall-product
注意这个配置 一定要放在 最后:因为如果放在前面 ,它会禁用下面其他的网关配置:比如,http://gulimall.com//product/attrattrgrouprelation/list
这个api 接口访问,它会首先到 gulimall.com,然后因为没有进行 截串 设置(截取 /api前缀),出现 404 访问不到。
-
-
测试效果
这里出现 404 问题:原因:Nginx 转发给网关的时候,会丢失很多请求头信息,这里就缺失了 host 地址,这里我们暂时只配置 上 host 地址,以后缺啥补啥。
![1669342458806](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669342458806.png)
重启测试:
直接访问域名成功:gulimall.com
![1669343410733](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669343410733.png)
访问接口 也成功。http://gulimall.com//product/attrattrgrouprelation/list
![1669343483954](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669343483954.png)
最后总结:
最终原理:
首先浏览器访问 gulimall.com
因为我们在Windows配置了host映射:gulimall.com 映射IP 192.168.56.10(虚拟机Ip)
所以会直接来到虚拟机又因为 浏览器访问 默认不带端口,那就是访问80端口,所以会来到 Nginx,我们又配置 了 80端口监听 gulimall.com 这个域名;此外由于 location/下的配置:代理转发:
Nginx 又代理给网关,这里注意一个细节:由于Nginx 转发会丢失 一些请求头信息,所以我们要加上请求头的配置,这里暂时只配置 host地址,之后的其他请求头配置我们用到的时候在进行添加;
网关发现 域名 是gulimall.com,进而就会找到 对应的配置:路由到商品服务,进而就转给了商品服务,这处网关配置一定要放在最后面,避免放在前面禁用后面的其他截串配置。
- 域名映射效果:
- 请求接口 gulimall.com
- 请求页面 gulimall.com
- nginx 直接代理给网关,网关判断
- 如果是/api/***,转交给对应的服务器
- 如果是满足域名,转交给对应的服务
重要!!!!
关于 第3章 性能与压力测试 和 第4章 缓存与分布式锁单独写在另外一篇文档:谷粒商城之高级篇知识补充。
2.3 检索业务
2.3.1 页面环境搭建
①秉承动静分离的原则,我们将 静态资源放到 Nginx下:
在Nginx新建一个 文件夹search,用来存放相关静态资源。
![1669625868815](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669625868815.png)
②修改 index页面下的静态资源前缀
静态资源
![1669625610230](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669625610230.png)
![1669625695767](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669625695767.png)
加上 thymeleaf 的名称空间
![1669627210900](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669627210900.png)
③域名映射
![1669626589774](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669626589774.png)
④ *.gulimall.com 表示所有请求Nginx都处理,最后的结果就是Nginx转发给 网关
![1669626693068](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669626693068.png)
最终的转发效果就是:
![1669627748923](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669627748923.png)
⑤网关配置
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=gulimall.com #这里和之前的相比有修改
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
⑥访问 http://search.gulimall.com/
![1669627011493](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669627011493.png)
2.3.2 调整页面跳转
为了以后开发方便,我们加上 热部署依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
关闭thymeleaf 的缓存
spring.thymeleaf.cache=false
我们可以通过检索页面的以下两个地方跳转会商城首页
一个是超链接,一个是图标。
![1669638677570](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669638677570.png)
首先是超链接修改:
search服务里面的index页面,改为 http://gulimall.com
![1669638937119](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669638937119.png)
接着是图标处:改为 http://gulimall.com
![1669639060928](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669639060928.png)
修改域名映射:让gulimall.com和带其子域名的都转发给网关。
![1669628933564](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669628933564.png)
测试:成功跳转回首页。
![1669639260858](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669639260858.png)
接下来,我们在首页上可以有这么两个地方,可以跳转到我们的检索页面:
①关键字搜索:搜索按钮![1669639381807](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669639381807.png)
②点击分类,跳转到检索页面。![1669639442078](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669639442078.png)
③修改配置:
-
通过分类点击到检索页面
-
将 检索页面 重命名为 list.html
![1669639624407](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669639624407.png)
-
创建SearchController
@Controller public class SearchController { @GetMapping("/list.html") public String listPage() { return "list"; } }
-
避坑:
如果点击 分类跳转 到 检索页面,报错,然后控制台域名是:search.gmall.com开头,那么我们需要去
Nginx 下的 html/static/index/js,在 catelogLoader中搜索gmall,替换为 gulimall
-
![1669637851626](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669637851626.png)
-
通过首页的搜索图标跳转到检索页面
修改 gulimall-product下的index.html页面:
搜索 search:
search方法应该是这样:之前修改前缀的时候多加了/static,所以一直访问不到,下面这个是正确的。
![1669640002551](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669640002551.png)
另外图标处修改为:
![1669640089958](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669640089958.png)
ps: 注意一定要把product商品服务中的application.yaml配置文件中 thymeleaf 的页面缓存设置为false,之前测试缓存的时候给设为 开启了,开发中我们关闭。
![1669640254206](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669640254206.png)
- 测试,都成功跳转到检索页面。
ps:测试的时候,注意浏览器缓存问题,不然有时候测试不成功。
2.3.3 检索返回结果模型分析抽取
1、检索业务分析
商品检索三个入口:
1)、选择分类进入商品检索
![1669643862464](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669643862464.png)
2)、输入检索关键字展示检索页![1669643878433](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669643878433.png)
3)、选择筛选条件进入
![1669643894216](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669643894216.png)
检索条件&排序条件
- 全文检索:skuTitle
- 排序: saleCount、hotScore、skuPrice
- 过滤:hasStock、skuPrice 区间、brandId、catalogId、attrs
- 聚合:attrs
完整的url 参数
keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1
&catalogId=1&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏
修改 SearchController
@Controller
public class SearchController {
@Autowired
MallSearchService mallSearchService;
/**
* 创建SearchParam:避免controller 方法参数位置接收太多的请求参数
* 自动将页面提交过来的所有请求查询参数封装成指定的对象
* @param param
* @return
*/
@GetMapping("/list.html")
public String listPage(SearchParam param, Model model) {
//1、根据传递过来的页面的查询参数,去es中检索商品
SearchResult result = mallSearchService.search(param);
//放到 model 中,方便页面取值
model.addAttribute("result",result);
return "list";
}
}
创建 SearchParam类(vo包下):封装页面所有可能传递过来的查询条件:请求参数模型
/**
* 封装页面所有可能传递过来的查询条件
*
* catalog3Id=225&keyword=小米&sort=saleCount_asc&hasStock=0/1&brandId=1&brandId=2&attrs=1_5寸:6寸&attrs=2_16G:8G
*/
@Data
public class SearchParam {
private String keyword;//页面传递过来的全文匹配关键字
private Long catalog3Id;//页面传递过来的三级分类id
/**
* sort=saleCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hostScore_asc/desc
*
*/
private String sort;//排序条件
/**
* 好多的过滤条件
* hasStock(是否有货)、skuPrice 区间、brandId、catalogId、attrs
* hasStock=0/1 :0有货;1无货
* skuPrice=1_500/500_/_500
* brandId=1
* attrs=2_5寸:6寸
*
*/
private Integer hasStock = 1;//是否只显示有货
private String skuPrice;//价格区间查询
private List<Long> brandId;//按照品牌进行查询,可以多选
private List<String> attrs;//按照属性进行筛选
private Integer pageNum = 1;//页码
}
创建 SearchResult :封装页面所有可能返回的结果:响应数据模型
/**
* 封装页面所有可能返回的结果
*/
@Data
public class SearchResult {
//查询到的所有商品信息
private List<SkuEsModel> products;
/**
* 以下是分页信息
*/
private Integer pageNum;//当前页码
private Long total;//总记录数
private Integer totalPages;//总页码
private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的所有分类
private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的所有属性
//============================以上是返回给页面的所有信息============================
@Data
public static class BrandVo{
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo{
private Long catalogId;
private String catalogName;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
创建 MallSearchService 及其实现
MallSearchService
public interface MallSearchService {
/**
*
* @param param 检索的所有参数
* @return 返回检索的结果,里面包含页面所需要的所有信息
*/
SearchResult search(SearchParam param);
}
MallSearchServiceImpl
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Override
public Object search(SearchParam param) {
return null;
}
}
分析结果 见 上面的 SearchParam类及SearchResult类。
2.3.4 检索DSL语句
在 Kibana中进行检索DSL语句测试。
- 查询部分
最终检索语句:
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": { #模糊匹配-全文检索
"skuTitle": "华为"
}
}
],
"filter": [ #过滤条件
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"1",
"2",
"9"
]
}
},
{
"nested": { #嵌套查询
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "15"
}
}
},
{
"terms": {
"attrs.attrValue": [
"海思(Hisilicon)",
"以官网信息为准"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [ #排序
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0, #分页
"size": 1,
"highlight": {#高亮
"fields": {
"skuTitle": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
}
}
整个查询条件:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析。
- 接下来就是聚合分析部分。
这里我们希望可以通过品牌属性等也可以检索到商品。
所以加上 品牌属性等检索条件。
报错:
![1669691892973](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669691892973.png)
修改映射,让他们都可以进行聚合分析。
![1669691237526](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669691237526.png)
创建新的映射
PUT gulimall_product
{
"mappings": {
"properties": {
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
},
"brandId": {
"type": "long"
},
"brandImg": {
"type": "keyword"
},
"brandName": {
"type": "keyword"
},
"catalogId": {
"type": "long"
},
"catalogName": {
"type": "keyword"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"saleCount": {
"type": "long"
},
"skuId": {
"type": "long"
},
"skuImg": {
"type": "keyword"
},
"skuPrice": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"spuId": {
"type": "keyword"
}
}
}
}
数据迁移
# 数据迁移
POST _reindex
{
"source":{
"index":"product"
},
"dest":{
"index":"gulimall_product"
}
}
查询
GET gulimall_product/_search
迁移成功。
修改 EsConstant
代码
![1669691730027](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669691730027.png)
最终聚合分析语句:
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg":{
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg":{
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg":{
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
- 整个查询的检索DSL语句:
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"1",
"2",
"9"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "15"
}
}
},
{
"terms": {
"attrs.attrValue": [
"海思(Hisilicon)",
"以官网信息为准"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 1,
"highlight": {
"fields": {
"skuTitle": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}
2.3.5 检索语句构建&&结果提取封装
1、构建请求参数
/**
* 准备检索请求
* # 整个查询条件
* # 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
*
* @return
*/
private SearchRequest buildSearchRequest(SearchParam param) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
/**
* 查询:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
*/
//1、构建 bool - query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//1.1、must - 模糊匹配
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
//1.2、bool - filter - 按照三级分类id查询
if (param.getCatalog3Id() != null) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}
//1.2、bool - filter - 按照品牌id查询
if (param.getBrandId() != null) {
boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
}
//1.2、bool - filter - 按照所有指定的属性进行查询
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
for (String attrStr : param.getAttrs()) {
// attrs=1_5寸:6寸&attrs=2_16G:8G
BoolQueryBuilder nestedboolQuery = QueryBuilders.boolQuery();
//attrs=1_5寸:6寸
String[] s = attrStr.split("_");
String attrId = s[0];//检索属性的id
String[] attrValues = s[1].split(":");//这个属性的检索用的值
nestedboolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
nestedboolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
//每一个必须都得生成一个nested查询
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedboolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
//1.2、bool - filter - 按照库存是否有进行查询
boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
//1.2、bool - filter - 按照价格区间进行查询
if (!StringUtils.isEmpty(param.getSkuPrice())) {
//1_500/_500/500_
// "range": {
// "skuPrice": {
// "gte": 0,
// "lte": 6000
// }
// }
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] s = param.getSkuPrice().split("_");
if (s.length == 2) {
//价格区间
rangeQuery.gte(s[0]).lte(s[1]);
} else if (s.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQuery.lte(s[0]);
}
if (param.getSkuPrice().endsWith("_")) {
rangeQuery.lte(s[0]);
}
}
boolQuery.filter(rangeQuery);
}
//把以前的所有条件都拿出来进行封装
sourceBuilder.query(boolQuery);
/**
* 排序,分页,高亮
*/
//2.1、排序
if (!StringUtils.isEmpty(param.getSort())) {
String sort = param.getSort();
//sort=hotScore_asc/desc
String[] s = sort.split("_");
SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
sourceBuilder.sort(s[0], order);
}
//2.2、分页 pageSize:5
// pageNum:1 from:0 size:5 [0,1,2,3,4]
// pageNum:2 from:5 size:5
// from = (pageNum - 1)*size
sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
//2.3、高亮
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder builder = new HighlightBuilder();
builder.field("skuTitle");
builder.preTags("<b style='color:red'>");
builder.postTags("</b>");
sourceBuilder.highlighter(builder);
}
/**
* 聚合分析
*/
//1.品牌聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
brand_agg.field("brandId").size(50);
//品牌聚合的子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
// TODO 1、聚合 brand
sourceBuilder.aggregation(brand_agg);
//2.分类聚合 catalog_agg
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
// TODO 2、聚合 catalog
sourceBuilder.aggregation(catalog_agg);
//3.属性聚合 attr_agg
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
//聚合出当前所有的attrId
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
//聚合分析出当前 attr_id对应的名字
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
//聚合分析出当前attr_id对应的所有可能的属性值attrValue
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
attr_agg.subAggregation(attr_id_agg);
// TODO 3、聚合 attr
sourceBuilder.aggregation(attr_agg);
String s = sourceBuilder.toString();
System.out.println("构建的DSL" + s);
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
return searchRequest;
}
2、响应结果封装
/**
* 构建结果数据
*
* @return
*/
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
SearchResult result = new SearchResult();
//1、返回的所有查询到的商品
SearchHits hits = response.getHits();
List<SkuEsModel> esModels = new ArrayList<>();
if (hits.getHits() != null && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
if (!StringUtils.isEmpty(param.getKeyword())){
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String string = skuTitle.getFragments()[0].string();
esModel.setSkuTitle(string);
}
esModels.add(esModel);
}
;
}
result.setProducts(esModels);
// //2、当前所有商品涉及到的所有属性信息
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
ParsedNested attr_agg = response.getAggregations().get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//1、得到属性的id
long attrId = bucket.getKeyAsNumber().longValue();
//2、得到属性的名字
String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
//3、得到属性的所有值
List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
String keyAsString = ((Terms.Bucket) item).getKeyAsString();
return keyAsString;
}).collect(Collectors.toList());
attrVo.setAttrId(attrId);
attrVo.setAttrName(attrName);
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
// //3、当前所有商品涉及到的所有品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
//1、得到品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
//2、得到品牌的名字
String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
//3、得到品牌的图片
String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
brandVo.setBrandId(brandId);
brandVo.setBrandName(brandName);
brandVo.setBrandImg(brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
// //4、当前所有商品涉及到的所有分类信息
ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
for (Terms.Bucket bucket : buckets) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
//得到分类id
String keyAsString = bucket.getKeyAsString();
catalogVo.setCatalogId(Long.parseLong(keyAsString));
//得到分类名
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalog_name);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
// ===============以上从聚合信息中获取==================
// //5、分页信息-页码
result.setPageNum(param.getPageNum());
// //5、分页信息-总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
// //5、分页信息-总页码-计算 11/2 = 5 ..1
int totalPages = (int) total % EsConstant.PRODUCT_PAGESIZE == 0 ? (int) total / EsConstant.PRODUCT_PAGESIZE : ((int) total / EsConstant.PRODUCT_PAGESIZE + 1);
result.setTotalPages(totalPages);
return result;
}
3、编写这些代码完了之后,利用postman进行测试。
![1669731372249](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669731372249.png)
测试过后,以上代码暂时没有错误。
4、总的检索方法:SearchResult
//去 es进行检索
@Override
public SearchResult search(SearchParam param) {
//1.动态构建出查询需要的DSL语句
SearchResult result = null;
//1、准备检索请求
SearchRequest searchRequest = buildSearchRequest(param);
try {
//2、执行检索请求
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//3、分析响应数据封装成我们需要的格式
result = buildSearchResult(response,param);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
2.3.6 页面基本数据渲染
修改 list.html页面。
①页面商品展示:
![1669732023872](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669732023872.png)
![1669732083135](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669732083135.png)
![1669732226462](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669732226462.png)
![1669732326809](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669732326809.png)
![1669732382863](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669732382863.png)
![1669732660152](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669732660152.png)
![1669733273920](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669733273920.png)
th:utext:不转义,可以让我们搜索的关键字高亮显示。
测试:
![1669733376461](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669733376461.png)
②品牌、分类等显示。
![1669733780249](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669733780249.png)
效果:
![1669733795992](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669733795992.png)
![1669734233498](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669734233498.png)
效果:
![1669734251149](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669734251149.png)
显示全部:
修改代码:SearchParam
private Integer hasStock;//是否只显示有货
MallSearchServiceImpl -> buildSearchRequest
//1.2、bool - filter - 按照库存是否有进行查询
if (param.getHasStock() != null){
boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
}
效果:
![1669734506185](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669734506185.png)
加粗:
![1669734596736](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669734596736.png)
接下来,我们把分类一栏也显示出来。
<div class="JD_pre">
<div class="sl_key">
<span><b>分类:</b></span>
</div>
<div class="sl_value">
<ul>
<li th:each="catalog:${result.catalogs}">
<a href="/static/search/#" th:text="${catalog.catalogName}">5.56英寸及以上</a>
</li>
</ul>
</div>
<div class="sl_ext">
<a href="/static/search/#">
更多
<i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
<b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
</a>
<a href="/static/search/#">
多选
<i>+</i>
<span>+</span>
</a>
</div>
</div>
最终效果:
![1669735609704](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669735609704.png)
![1669735623970](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669735623970.png)
2.3.7 页面筛选条件渲染
当我们选择比如品牌,分类,型号等自动拼接上参数。
函数:
function searchProducts(name,value){
//原来的页面
var href = location.href + "";
if(href.indexOf("?")!=-1){
location.href = location.href + "&"+name+"="+value;
}else{
location.href = location.href + "?"+name+"="+value;
}
}
品牌:
![1669736596036](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669736596036.png)
th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}"
测试:
![1669736576069](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669736576069.png)
分类:
![1669737909309](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669737909309.png)
th:href="${'javascript:searchProducts("catalog3Id",'+catalog.catalogId+')'}"
其他属性:
![1669737973606](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669737973606.png)
th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+val+'")'}"
2.3.8 页面分页数据渲染
①修改搜索导航:搜索的时候地址栏加上关键字。
![1669787163213](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669787163213.png)
![1669787179361](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669787179361.png)
搜索关键字:华为,地址栏加上华为。
![1669787198657](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669787198657.png)
回显搜索过的关键字:
![1669790455925](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790455925.png)
![1669790335463](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790335463.png)
<div class="header_form">
<input id="keyword_input" type="text" placeholder="手机" th:value="${param.keyword}"/>
<a href="javascript:searchByKeyword();">搜索</a>
</div>
function searchByKeyword() {
searchProducts("keyword", $("#keyword_input").val());
}
② 分页调整。
因为我的商品这里总共就17个,为了分页显示效果,设为每页显示8条。
![1669790548407](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790548407.png)
页面修改:
![1669790726169](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790726169.png)
<div class="filter_page">
<div class="page_wrap">
<span class="page_span1">
<a class="page_a" th:attr="pn=${result.pageNum - 1}" href="/static/search/#"
th:if="${result.pageNum>1}">
< 上一页
</a>
<a class="page_a"
th:attr="pn=${nav},style=${nav == result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
th:each="nav:${result.pageNavs}">[[${nav}]]</a>
<a class="page_a" th:attr="pn=${result.pageNum + 1}"
th:if="${result.pageNum<result.totalPages}">
下一页 >
</a>
</span>
<span class="page_span2">
<em>共<b>[[${result.totalPages}]]</b>页 到第</em>
<input type="number" value="1">
<em>页</em>
<a>确定</a>
</span>
</div>
$(".page_a").click(function () {
var pn = $(this).attr("pn");
var href = location.href;
if (href.indexOf("pageNum") != -1) {
//替换pageNum的值
location.href = replaceParamVal(href, "pageNum", pn);
} else {
location.href = location.href + "&pageNum=" + pn;
}
return false;
});
function replaceParamVal(url, paramName, replaceVal) {
var oUrl = url.toString();
var re = eval('/(' + paramName + '=)([^&]*)/gi');
var nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
return nUrl;
};
代码修改:
![1669790799655](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790799655.png)
![1669790836623](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790836623.png)
/**
* 构建结果数据
*
* @return
*/
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
...
//导航页
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);//可遍历的页码
}
result.setPageNavs(pageNavs);
效果展示:
![1669790911567](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790911567.png)
![1669790957966](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669790957966.png)
还有一些分页效果在这里就暂时不做了。
![1669791798247](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669791798247.png)
![1669791783574](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669791783574.png)
2.3.9 页面排序功能
![1669794740307](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669794740307.png)
function replaceAndAddParamVal(url, paramName, replaceVal) {
var oUrl = url.toString();
//1.如果没有就添加,有就替换;
if (oUrl.indexOf(paramName) != -1) {
var re = eval('/(' + paramName + '=)([^&]*)/gi');
var nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
return nUrl;
} else {
var nUrl = "";
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + '=' + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + '=' + replaceVal;
}
return nUrl;
}
};
$(".sort_a").click(function () {
//1.当前被点击的元素变为选中状态
// color: #FFF;border-color: #e4393c;background: #e4393c;
//改变当前元素以及兄弟元素的样式
changeStyle(this);
//2.跳转到指定位置 sort=skuPrice_asc/desc
var sort = $(this).attr("sort");
sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc";
location.href = replaceAndAddParamVal(location.href, "sort", sort);
//禁用默认行为
return false;
});
function changeStyle(ele) {
$(".sort_a").css({"color": "#333", "border-colo": "#CCC", "background": "#FFF"});
$(".sort_a").each(function () {
var text = $(this).text().replace("↓", "").replace("↑", "");
$(this).text(text);
});
$(ele).css({"color": "#FFF", "border-colo": "#e4393c", "background": "#e4393c"});
//改变升降序
$(ele).toggleClass("desc");//加上就是降序,不加就是升序
if ($(ele).hasClass("desc")) {
//降序
var text = $(ele).text().replace("↓", "").replace("↑", "");
text = text + "↓";
$(ele).text(text);
} else {
var text = $(ele).text().replace("↓", "").replace("↑", "");
text = text + "↑";
$(ele).text(text);
}
}
效果:有上下箭头
![1669794825490](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669794825490.png)
按价格排序
![1669794906503](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669794906503.png)
2.3.10 页面排序字段回显
![1669798477914](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669798477914.png)
<div class="filter_top">
<div class="filter_top_left" th:with="p = ${param.sort}">
<a sort="hotScore"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
综合排序[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') &&
#strings.endsWith(p,'desc')) ?'↑':'↓' }]]</a>
<a sort="saleCount"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
销量[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') &&
#strings.endsWith(p,'desc'))?'↑':'↓' }]]</a>
<a sort="skuPrice"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
价格[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') &&
#strings.endsWith(p,'desc'))?'↑':'↓' }]]</a>
<a href="/static/search/#">评论分</a>
<a href="/static/search/#">上架时间</a>
</div>
这里很容易写错!!!!出错的话,可以参考别人的课件:
![1669798600150](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669798600150.png)
效果:
![1669798646828](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669798646828.png)
2.3.11 页面价格区间搜索&&仅显示有货
- 页面价格区间搜索
![1669800573706](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669800573706.png)
<div class="filter_top">
<div class="filter_top_left" th:with="p = ${param.sort},priceRange = ${param.skuPrice}">
<a sort="hotScore"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
综合排序[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') &&
#strings.endsWith(p,'desc')) ?'↑':'↓' }]]</a>
<a sort="saleCount"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
销量[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') &&
#strings.endsWith(p,'desc'))?'↑':'↓' }]]</a>
<a sort="skuPrice"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
价格[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') &&
#strings.endsWith(p,'desc'))?'↑':'↓' }]]</a>
<a href="/static/search/#">评论分</a>
<a href="/static/search/#">上架时间</a>
<input id="skuPriceFrom" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
style="width: 100px; margin-left: 30px">
-
<input id="skuPriceTo" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
style="width: 100px">
<button id="skuPriceSearchBtn">确定</button>
</div>
$("#skuPriceSearchBtn").click(function () {
//1、拼上价格区间的查询条件
var from = $("#skuPriceFrom").val();
var to = $("#skuPriceTo").val();
var query = from + "_" + to;
location.href = replaceAndAddParamVal(location.href, "skuPrice", query);
});
效果:
![1669800659396](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669800659396.png)
- 仅显示有货
![1669811235260](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669811235260.png)
<li>
<a href="#" th:with="check = ${param.hasStock}">
<input id="showHasStock" type="checkbox" th:checked="${#strings.equals(check,'1')}">
仅显示有货
</a>
</li>
$("#showHasStock").change(function (){
if ($(this).prop('checked')){
location.href = replaceAndAddParamVal(location.href,"hasStock",1);
}else {
//没选中
var re = eval('/(hasStock=)([^&]*)/gi');
location.href = (location.href+"").replace(re,'');
}
});
效果展示:
![1669811380067](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669811380067.png)
bug解决:之前搜索过的关键词在URL地址栏不会被替换,而是一直叠加。
function searchProducts(name, value) {
//原来的页面
location.href = replaceAndAddParamVal(location.href,name,value);
}
![1669811476426](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669811476426.png)
2.3.12 面包屑导航
修改 后台代码。
search微服务引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
让spring-cloud版本一致:
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.4.2</elasticsearch.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>
引入依赖管理:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
我们需要调用商品的远程服务,获取属性的名字。
gulimall-search下新建feign包:
ProductFeignService
@FeignClient("gulimall-product")
public interface ProductFeignService {
@GetMapping ("/product/attr/info/{attrId}")
public R attrInfo(@PathVariable("attrId") Long attrId);
}
主启动类添加调用远程服务注解
@EnableFeignClients //开启远程调用
新建 AttrResponseVo封装结果:这里我们暂时不用 gulimall-product下的 AttrRespVo了,可以将其放到公共服务中去。但是如果我们每个人只能修改自己多负责的微服务,我们就新建然后进行封装就行。
@Data
public class AttrResponseVo {
/**
* 属性id
*/
private Long attrId;
/**
* 属性名
*/
private String attrName;
/**
* 是否需要检索[0-不需要,1-需要]
*/
private Integer searchType;
/**
* 值类型[0-为单个值,1-可以选择多个值]
*/
private Integer valueType;
/**
* 属性图标
*/
private String icon;
/**
* 可选值列表[用逗号分隔]
*/
private String valueSelect;
/**
* 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
*/
private Integer attrType;
/**
* 启用状态[0 - 禁用,1 - 启用]
*/
private Long enable;
/**
* 所属分类
*/
private Long catelogId;
/**
* 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
*/
private Integer showDesc;
private Long attrGroupId;
private String catelogName;
private String groupName;
private Long[] catelogPath;
}
SearchResult
//面包屑导航数据
private List<NavVo> navs;
@Data
public static class NavVo{
private String navName;
private String navValue;
private String link;
}
SearchParam
private String _queryString;//原生的所有查询条件
SearchController
@GetMapping("/list.html")
public String listPage(SearchParam param, Model model, HttpServletRequest request) {
param.set_queryString(request.getQueryString());
//1、根据传递过来的页面的查询参数,去es中检索商品
SearchResult result = mallSearchService.search(param);
//放到 model 中,方便页面取值
model.addAttribute("result",result);
return "list";
}
MallSearchServiceImpl
@Autowired
ProductFeignService productFeignService;
/**
* 构建结果数据
*
* @return
*/
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
.....
//6、构建面包屑导航功能
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
//1、分析每个attrs传过来的查询参数值。
SearchResult.NavVo navVo = new SearchResult.NavVo();
// attrs=2_5存:6寸
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
R r = productFeignService.attrInfo(Long.parseLong(s[0]));
if (r.getCode() == 0) {
AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(data.getAttrName());
} else {
navVo.setNavName(s[0]);
}
//2、取消了这个面包屑之后,我们要跳转到那个地方,将请求地址的url里面的当前置空
//拿到所有的查询条件,去掉当前。
//attrs = 15_海思(Hisilicon)
String encode = null;
try {
encode = URLEncoder.encode(attr, "UTF-8");
encode = encode.replace("+", "%20");//浏览器对空格编码和java不一样
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String replace = param.get_queryString().replace("&attrs=" + encode, "");
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
return navVo;
}).collect(Collectors.toList());
result.setNavs(collect);
}
}
list.html页面修改
![1669817051401](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669817051401.png)
<div class="JD_ipone_one c">
<!-- 遍历面包屑功能 -->
<a th:href="${nav.link}" th:each="nav:${result.navs}"><span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span> x</a>
</div>
下面两个JS有修改:
function searchByKeyword() {
searchProducts("keyword", $("#keyword_input").val());
}
function replaceAndAddParamVal(url, paramName, replaceVal, forceAdd) {
var oUrl = url.toString();
//1.如果没有就添加,有就替换;
if (oUrl.indexOf(paramName) != -1) {
if (forceAdd) {
var nUrl = "";
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + '=' + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + '=' + replaceVal;
}
return nUrl;
} else {
var re = eval('/(' + paramName + '=)([^&]*)/gi');
var nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
return nUrl;
}
} else {
var nUrl = "";
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + '=' + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + '=' + replaceVal;
}
return nUrl;
}
};
测试:
地址加上了属性。
![1669816515865](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669816515865.png)
点 “ x” 地址栏消失属性。
![1669816602449](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669816602449.png)
- 条件筛选联动
商品服务的 BrandController中添加获取品牌id集合的方法:
@GetMapping("/infos")
public R info(@RequestParam("brandIds") List<Long> brandIds) {
List<BrandEntity> brand = brandService.getBrandsByIds(brandIds);
return R.ok().put("brand", brand);
}
BrandServiceImpl
@Override
public List<BrandEntity> getBrandsByIds(List<Long> brandIds) {
return baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id",brandIds));
}
因为 这些查询都比较耗费时间:远程调用,所以可以加上缓存。
AttrServiceImpl
@Cacheable(value = "attr",key = "'attrinfo:'+#root.args[0]")
@Override
public AttrRespVo getAttrInfo(Long attrId) {
}
查询服务的ProductSaveService
@GetMapping("/product/brand/infos")
public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds);
SearchParam
@Data
public class SearchParam {
...
private String _queryString;//原生的所有查询条件
}
SearchResult
![1669821822199](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669821822199.png)
@Data
public class SearchResult {
//面包屑导航数据
private List<NavVo> navs = new ArrayList<>();
private List<Long> attrIds = new ArrayList<>();
}
MallSearchServiceImpl
因为我们经常使用编码的方法,所以提取成一个公共方法。
![1669818473465](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669818473465.png)
略做修改
private String replaceQueryString(SearchParam param, String value, String key) {
String encode = null;
try {
encode = URLEncoder.encode(value, "UTF-8");
encode = encode.replace("+", "%20");//浏览器对空格编码和java不一样
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String replace = param.get_queryString().replace("&" + key + "=" + encode, "");
return replace;
}
上一节的属性面包屑导航增加和修改一些代码:
![1669821465984](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669821465984.png)
//6、构建面包屑导航功能
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
//1、分析每个attrs传过来的查询参数值。
SearchResult.NavVo navVo = new SearchResult.NavVo();
// attrs=2_5存:6寸
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
R r = productFeignService.attrInfo(Long.parseLong(s[0]));
result.getAttrIds().add(Long.parseLong(s[0]));
if (r.getCode() == 0) {
AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(data.getAttrName());
} else {
navVo.setNavName(s[0]);
}
//2、取消了这个面包屑之后,我们要跳转到那个地方,将请求地址的url里面的当前置空
//拿到所有的查询条件,去掉当前。
//attrs = 15_海思(Hisilicon)
String replace = replaceQueryString(param, attr, "attrs");
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
return navVo;
}).collect(Collectors.toList());
result.setNavs(collect);
}
对于品牌,分类的面包屑导航,这里暂时只做 品牌的。
//品牌,分类
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
List<SearchResult.NavVo> navs = result.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setNavName("品牌");
//TODO 远程查询所有品牌
R r = productFeignService.brandsInfo(param.getBrandId());
if (r.getCode() == 0) {
List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
});
StringBuffer buffer = new StringBuffer();
String replace = "";
for (BrandVo brandVo : brand) {
buffer.append(brandVo.getBrandName() + ";");
replace = replaceQueryString(param, brandVo.getBrandId() + "", "brandId");
}
navVo.setNavValue(buffer.toString());
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
}
navs.add(navVo);
}
//TODO 分类:不需要导航取消
list.html
![1669821670237](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669821670237.png)
<div class="JD_nav_logo" th:with="brandid= ${param.brandId}">
<!--品牌-->
<div th:if="${#strings.isEmpty(brandid)}" class="JD_nav_wrap">
![1669821704171](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669821704171.png)
<!--其他所有需要展示的属性-->
<div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds,attr.attrId)}">
测试
![1669821917865](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669821917865.png)
重要!!!!
关于 第5章 异步和线程池单独写在另外一篇文档:谷粒商城之高级篇知识补充。
2.4 商品详情
详情数据:
![1669907504151](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669907504151.png)
2.4.1 环境搭建
- 域名跳转设置
host配置
![1669896964866](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669896964866.png)
Nginx配置
之前已经配置了。
![1669897372758](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669897372758.png)
网关配置
![1669897460224](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669897460224.png)
- 动静资源设置
![1669898128937](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669898128937.png)
遵循动静分离的配置,我们将详情页的静态资源上传到Nginx下。
![1669899546642](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669899546642.png)
这是因为我们设置了对应的配置:
![1669898271316](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669898271316.png)
将详情页的页面复制到 商品服务下。
![1669898357749](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669898357749.png)
改名为 item.html
将页面中的相应静态资源加上对应前缀。
![1669898527632](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669898527632.png)
![1669898555755](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669898555755.png)
- 实现点击商品图片等可以跳转到 商品详情页。
修改 search服务下的 list.html页面
![1669899731732](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669899731732.png)
<a th:href="|http://item.gulimall.com/${product.skuId}.html|">
![1669899775763](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669899775763.png)
2.4.2 模型抽取
商品服务下新建 SkuItemVo(最终结果,以下均是)
@Data
public class SkuItemVo {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info;
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images;
//3、获取的spu的销售属性组合。
List<SkuItemSaleAttrVo> saleAttr;
//4、获取spu的介绍
SpuInfoDescEntity desp;
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> groupAttrs;
}
SkuItemSaleAttrVo
@Data
@ToString
public class SkuItemSaleAttrVo {
private Long attrId;
private String attrName;
private String attrValues;
}
SpuItemAttrGroupVo
@Data
@ToString
public class SpuItemAttrGroupVo {
private String groupName;
private List<Attr> attrs;
}
2.4.3 规格参数代码实现
ItemController(最终代码)
/**
* 因为这个类做跳转,所以不用写 @RestController,只需要是@Controller。
*/
@Controller
public class ItemController {
@Autowired
SkuInfoService skuInfoService;
/**
* 展示当前sku的详情
*
* @param skuId
* @return
*/
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId, Model model) {
System.out.println("准备查询" + skuId + "详情");
SkuItemVo vo = skuInfoService.item(skuId);
model.addAttribute("item",vo);
return "item";
}
}
SkuInfoServiceImpl
@Autowired
SkuImagesService imagesService;
@Autowired
SpuInfoDescService spuInfoDescService;
@Autowired
AttrGroupService attrGroupService;
@Override
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
Long catalogId = info.getCatalogId();
Long spuId = info.getSpuId();
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
//3、获取的spu的销售属性组合。
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(spuId);
skuItemVo.setDesp(spuInfoDescEntity);
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
skuItemVo.setGroupAttrs(attrGroupVos);
return null;
}
SkuImagesServiceImpl
@Override
public List<SkuImagesEntity> getImagesBySkuId(Long skuId) {
SkuImagesDao imagesDao = this.baseMapper;
List<SkuImagesEntity> imagesEntities = imagesDao.selectList(new QueryWrapper<SkuImagesEntity>().eq("sku_id", skuId));
return imagesEntities;
}
AttrGroupServiceImpl
@Override
public List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {
//1、查出当前spu对应的所有属性的分组信息以及当前分组下的所有属性对应的值
AttrGroupDao baseMapper = this.baseMapper;
List<SpuItemAttrGroupVo> vos = baseMapper.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
return vos;
}
AttrGroupDao
List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(@Param("spuId") Long spuId, @Param("catalogId") Long catalogId);
AttrGroupDao.xml
<!-- resultType 返回集合里面元素的类型,只要有嵌套属性就要封装自定义结果集-->
<resultMap id="spuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo">
<result property="groupName" column="attr_group_name"></result>
<collection property="attrs" ofType="com.atguigu.gulimall.product.vo.Attr">
<result column="attr_name" property="attrName"></result>
<result column="attr_value" property="attrValue"></result>
</collection>
</resultMap>
<select id="getAttrGroupWithAttrsBySpuId"
resultMap="spuItemAttrGroupVo">
SELECT
pav.`spu_id`,
ag.`attr_group_name`,
ag.`attr_group_id`,
aar.`attr_id`,
attr.`attr_name`,
pav.`attr_value`
FROM `pms_attr_group` ag
LEFT JOIN `pms_attr_attrgroup_relation` aar ON aar.`attr_group_id` = ag.`attr_group_id`
LEFT JOIN `pms_attr` attr ON attr.`attr_id` = aar.`attr_id`
LEFT JOIN `pms_product_attr_value` pav ON pav.`attr_id` = attr.`attr_id`
WHERE ag.`catelog_id` = 225 AND pav.`spu_id` = 6
</select>
sql语句
SELECT
pav.`spu_id`,
ag.`attr_group_name`,
ag.`attr_group_id`,
aar.`attr_id`,
attr.`attr_name`,
pav.`attr_value`
FROM `pms_attr_group` ag
LEFT JOIN `pms_attr_attrgroup_relation` aar ON aar.`attr_group_id` = ag.`attr_group_id`
LEFT JOIN `pms_attr` attr ON attr.`attr_id` = aar.`attr_id`
LEFT JOIN `pms_product_attr_value` pav ON pav.`attr_id` = attr.`attr_id`
WHERE ag.`catelog_id` = 225 AND pav.`spu_id` = 6
![1669905646810](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669905646810.png)
GulimallProductApplicationTests 测试
@Autowired
AttrGroupDao attrGroupDao;
@Test
public void test() {
List<SpuItemAttrGroupVo> attrGroupWithAttrsBySpuId = attrGroupDao.getAttrGroupWithAttrsBySpuId(6L, 225L);
System.out.println(attrGroupWithAttrsBySpuId);
}
![1669905673080](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669905673080.png)
2.4.4 销售属性组合代码实现
SkuInfoServiceImpl(最终代码)
@Autowired
SkuImagesService imagesService;
@Autowired
SpuInfoDescService spuInfoDescService;
@Autowired
AttrGroupService attrGroupService;
@Autowired
SkuSaleAttrValueService skuSaleAttrValueService;
@Override
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
Long catalogId = info.getCatalogId();
Long spuId = info.getSpuId();
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
//3、获取的spu的销售属性组合。
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
skuItemVo.setSaleAttr(saleAttrVos);
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(spuId);
skuItemVo.setDesp(spuInfoDescEntity);
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
skuItemVo.setGroupAttrs(attrGroupVos);
return skuItemVo;
}
SkuSaleAttrValueServiceImpl
@Override
public List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId) {
SkuSaleAttrValueDao dao = this.baseMapper;
List<SkuItemSaleAttrVo> saleAttrVos = dao.getSaleAttrsBySpuId(spuId);
return saleAttrVos;
}
SkuSaleAttrValueDao
List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(@Param("spuId") Long spuId);
SkuSaleAttrValueDao.xml
<select id="getSaleAttrsBySpuId" resultType="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
SELECT
ssav.`attr_id` attr_id,
ssav.`attr_name` attr_name,
GROUP_CONCAT(DISTINCT ssav.`attr_value`) attr_values
FROM `pms_sku_info` info
LEFT JOIN `pms_sku_sale_attr_value` ssav ON ssav.`sku_id` = info.`sku_id`
WHERE info.`spu_id`=#{spuId}
GROUP BY ssav.`attr_id`,ssav.`attr_name`
</select>
sql语句
SELECT
ssav.`attr_id` attr_id,
ssav.`attr_name` attr_name,
GROUP_CONCAT(DISTINCT ssav.`attr_value`) attr_values
FROM `pms_sku_info` info
LEFT JOIN `pms_sku_sale_attr_value` ssav ON ssav.`sku_id` = info.`sku_id`
WHERE info.`spu_id`=#{spuId}
GROUP BY ssav.`attr_id`,ssav.`attr_name`
![1669908003591](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669908003591.png)
GulimallProductApplicationTests 测试:
@Autowired
SkuSaleAttrValueDao skuSaleAttrValueDao;
@Test
public void test() {
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueDao.getSaleAttrsBySpuId(7L);
System.out.println(saleAttrVos);
}
![1669908066622](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669908066622.png)
2.4.5 详情页渲染
![1669952027231](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669952027231.png)
<div class="box-name" th:text="${item.info.skuTitle}">
华为 HUAWEI Mate 10 6GB+128GB 亮黑色 移动联通电信4G手机 双卡双待
</div>
<div class="box-hide" th:text="${item.info.skuSubtitle}">预订用户预计11月30日左右陆续发货!麒麟970芯片!AI智能拍照!
<a href="/static/item/"><u></u></a>
</div>
![1669952190363](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669952190363.png)
<div class="probox">
<img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
<div class="hoverbox"></div>
</div>
<div class="showbox">
<img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
</div>
![1669952717987](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669952717987.png)
<span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>
![1669955432132](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669955432132.png)
<li th:each="img : ${item.images}" th:if="${!#strings.isEmpty(img.imgUrl)}"><img th:src="${img.imgUrl}" /></li>
![1669955909924](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669955909924.png)
<div class="box-attr clear" th:each="attr:${item.saleAttr}">
<dl>
<dt>选择[[${attr.attrName}]]</dt>
<dd th:each="val:${#strings.listSplit(attr.attrValues,',')}">
<a href="/static/item/#">
[[${val}]]
<!--<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> 摩卡金-->
</a>
</dd>
</dl>
</div>
![1669956284354](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669956284354.png)
<img class="xiaoguo" th:src="${descp}" th:each="descp:${#strings.listSplit(item.desp.decript,',')}"/>
![1669958765969](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669958765969.png)
![1669958712530](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669958712530.png)
<div class="guiGe" th:each="group:${item.groupAttrs}">
<h3 th:text="${group.groupName}">主体</h3>
<dl>
<div th:each="attr:${group.attrs}">
<dt th:text="${attr.attrName}">品牌</dt>
<dd th:text="${attr.attrValue}">华为(HUAWEI)</dd>
</div>
</div>
效果:
![1669962136842](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669962136842.png)
2.4.6 销售属性渲染
修改后台代码
![1669974238774](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669974238774.png)
SkuItemSaleAttrVo
@Data
@ToString
public class SkuItemSaleAttrVo {
private Long attrId;
private String attrName;
private List<AttrValueWithSkuIdVo> attrValues;
}
AttrValueWithSkuIdVo
@Data
public class AttrValueWithSkuIdVo {
private String attrValue;
private String skuIds;
}
SkuSaleAttrValueDao.xml
<resultMap id="SkuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
<result column="attr_id" property="attrId"></result>
<result column="attr_name" property="attrName"></result>
<collection property="attrValues" ofType="com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo">
<result column="attr_value" property="attrValue"></result>
<result column="sku_ids" property="skuIds"></result>
</collection>
</resultMap>
<select id="getSaleAttrsBySpuId" resultMap="SkuItemSaleAttrVo">
SELECT
ssav.`attr_id` attr_id,
ssav.`attr_name` attr_name,
ssav.`attr_value`,
GROUP_CONCAT(DISTINCT info.`sku_id`) sku_ids
FROM `pms_sku_info` info
LEFT JOIN `pms_sku_sale_attr_value` ssav ON ssav.`sku_id` = info.`sku_id`
WHERE info.`spu_id`=#{spuId}
GROUP BY ssav.`attr_id`,ssav.`attr_name`,ssav.`attr_value`
</select>
sql代码:
SELECT
ssav.`attr_id` attr_id,
ssav.`attr_name` attr_name,
ssav.`attr_value`,
GROUP_CONCAT(DISTINCT info.`sku_id`) sku_ids
FROM `pms_sku_info` info
LEFT JOIN `pms_sku_sale_attr_value` ssav ON ssav.`sku_id` = info.`sku_id`
WHERE info.`spu_id`=7
GROUP BY ssav.`attr_id`,ssav.`attr_name`,ssav.`attr_value`
![1669974377166](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669974377166.png)
item.html
![1669974520249](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669974520249.png)
<div class="box-attr clear" th:each="attr:${item.saleAttr}">
<dl>
<!--strings.listSplit 切分
#list.contains(A,B) 判断A数组中间 是否包含B
skuId 同时设置一个class 在列表中包含的设置为选中 否则不选中
-->
<dt>选择[[${attr.attrName}]]</dt>
<dd th:each="vals:${attr.attrValues}">
<a class="sku_attr_value" th:attr="skus=${vals.skuIds},class=${#lists.contains(#strings.listSplit(vals.skuIds,','),
item.info.skuId.toString())? 'sku_attr_value checked':'sku_attr_value'}">
[[${vals.attrValue}]]
<!--<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> 摩卡金-->
</a>
</dd>
</dl>
</div>
$(function () {
//页面初始化 给父类id设置样式
$(".sku_attr_value").parent().css({"border": "solid 1px #CCC"});
//class里面有对应样式的父类设置样式 checked 表示选中
$("a[class = 'sku_attr_value checked']").parent().css({"border": "1px solid red"});
})
效果:
![1669974540897](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669974540897.png)
实现点击 sku 能够动态切换。
$(".sku_attr_value").click(function () {
//1、点击的元素先添加上自定义的属性。为了识别我们是刚才被点击的。
var skus = new Array();
$(this).addClass("checked");
//属性skus以逗号拆分
var curr = $(this).attr("skus").split(",");
//当前被点击的所有sku组合数组放进去
skus.push(curr);
//去掉同一行的所有checked
/**
* parent 父类 中查询 拥有class的 然后删除调 checked
*/
$(this).parent().parent().find(".sku_attr_value").removeClass("checked");
$("a[class='sku_attr_value checked']").each(function () {
skus.push($(this).attr("skus").split(","));
});
console.log(skus);
//2、取出他们的交集,得到skuId
var filterEle = skus[0];
for (var i = 1; i < skus.length; i++) {
filterEle = $(filterEle).filter(skus[i]);
}
console.log(filterEle[0]);
//3、跳转
location.href = "http://item.gulimall.com/" + filterEle[0] + ".html";
})
效果:点击颜色和版本都可以自动切换。
![1669981285893](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669981285893.png)
2.4.7 异步编排优化代码
①引入依赖:配置类可以有提示,这个可以配也可以不配。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
② 商品服务下新建 ThreadPoolConfigProperties
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
③ application.properties中添加线程池的相应配置
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
④商品服务下新建 MyThreadConfig
@EnableConfigurationProperties(ThreadPoolConfigProperties.class) //因为 ThreadPoolConfigProperties添加了注解@Component,可以不用写这个配置了,直接从容器中拿
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(pool.getCoreSize(),
pool.getMaxSize(),pool.getKeepAliveTime(),
TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
⑤ SkuInfoServiceImpl
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
// 第一步获得的数据,第3步、4步、5步也要使用
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3、获取的spu的销售属性组合。
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
//等待所有任务都完成
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();
return skuItemVo;
}
⑥ItemController
/**
* 展示当前sku的详情
*
* @param skuId
* @return
*/
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId, Model model) throws ExecutionException, InterruptedException {
System.out.println("准备查询" + skuId + "详情");
SkuItemVo vo = skuInfoService.item(skuId);
model.addAttribute("item",vo);
return "item";
}
⑦测试:一切正常。
![1669990189776](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669990189776.png)
2.5 认证服务
2.5.1 环境搭建
①创建 gulimall-auth-server服务
![1669991569359](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669991569359.png)
![1669991677797](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669991677797.png)
②引入依赖
<dependencies>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
这里如果我们修改 spring-boot-starter-parent 版本依然爆红,处理结果:清除缓存:
![1669993351283](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669993351283.png)
③ 添加域名
![1669992683902](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669992683902.png)
④动静分离。
将登录页面(改名为login.html)和认证页面(改名为 reg.html)放到认证服务下。
![1669993498656](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669993498656.png)
![1669993645487](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669993645487.png)
![1669993680335](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669993680335.png)
修改页面静态资源的前缀。
login.html
![1669993771455](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669993771455.png)
![1669993808463](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669993808463.png)
reg.html
![1669993841959](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669993841959.png)
⑤将认证服务注册进nacos
application.properties
spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=20000
GulimallAuthServerApplication(主启动类)
@EnableFeignClients //开启远程调用功能
@EnableDiscoveryClient //开启服务注册发现功能
![1669994323585](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669994323585.png)
⑥配置网关
- id: gulimall_auth_route
uri: lb://gulimall-auth-server
predicates:
- Host=auth.gulimall.com
⑦测试访问登录页面
注意:模板下面的页面除了 index页面,springmvc能够自动访问到,所有要去的页面我门都需要设置controller类来处理相应的请求。
为了测试,我们暂时将 login.html改名为 index.html。
![1669994363346](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669994363346.png)
![1669994560514](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669994560514.png)
⑧ 实现各个页面之间的相互跳转
1、实现登录页面点击”谷粒商城“图标能跳转到首页:
![1669995151526](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669995151526.png)
login.html
![1669994603488](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669994603488.png)
2、实现首页点击登录和注册能跳转到登录和注册页面:
![1669995167254](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669995167254.png)
修改商品服务下的首页index.html
![1669995275064](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669995275064.png)
认证服务编写 controller 实现跳转
@Controller
public class LoginController {
@GetMapping("/login.html")
public String loginPage(){
return "login";
}
@GetMapping("/reg.html")
public String regPage(){
return "reg";
}
}
![1669995498525](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669995498525.png)
![1669995518913](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669995518913.png)
登录页面点击“立即注册”能够跳转到注册页面。
![1669997262187](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669997262187.png)
![1669997287655](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669997287655.png)
注册页面点击“请登录”能够跳转到登录页面。
![1669997360037](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669997360037.png)
![1669997384646](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1669997384646.png)
ps:这里可以稍微修改一下 登录页面的宽度,让页面更好看一点。
![1670050548930](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670050548930.png)
2.5.2 短信验证码
①把 reg.html页面中这一处修改为 “发送验证码”
![1670051075825](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670051075825.png)
![1670051102425](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670051102425.png)
发送验证码,有60秒倒计时:
![1670053604720](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670053604720.png)
$(function (){
$("#sendCode").click(function () {
//2、倒计时
if ($(this).hasClass("disabled")){
//正在倒计时。
}else{
//1、给指定手机号码发送验证码
timeoutChangeStyle();
}
});
})
var num = 60;
function timeoutChangeStyle(){
$("#sendCode").attr("class","disabled");
if (num == 0){
$("#sendCode").text("发送验证码");
num = 60;
$("#sendCode").attr("class","");
}else{
var str = num +"s 后再次发送";
$("#sendCode").text(str);
//每隔1s调用timeoutChangeStyle()
setTimeout("timeoutChangeStyle()",1000);
}
num --;
}
效果:
![1670053650503](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670053650503.png)
②修改后台代码
如果编写一个接口仅仅是为了跳转页面,没有数据的处理,如果这样的跳转接口多了则可以使用SpringMVC的view Controller(视图控制器)将请求与页面进行绑定
新建 GulimallWebConfig
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
/**
* 视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
/**
* * @GetMapping("/login.html")
* * public String loginPage(){
* *
* * return "login";
* * }
* * @param registry
*/
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
ps:idea快捷键:实现接口方法
alt + shift + p
以前的 LoginController 里面的 方法就可以注释掉了。
@Controller
public class LoginController {
/**
* 发送一个请求直接跳转到一个页面。
* springMVC viewcontroller:将请求和页面映射过来。
*/
// @GetMapping("/login.html")
// public String loginPage(){
//
// return "login";
// }
//
// @GetMapping("/reg.html")
// public String regPage(){
//
// return "reg";
// }
}
此外,设置 认证服务的最大内存:
![1670054335513](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670054335513.png)
③利用第三方服务进行短信验证码的发送。
这个是老师视频里面购买的短信服务:
![1670055851324](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670055851324.png)
但是现在这个只有企业用户才能购买。
我们就使用这个来进行测试。
![1670056219683](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670056219683.png)
初步测试一下。注意,这里需要开通API网关才可以进行调试。
购买成功后,从网关控制台这里点进去,如果直接从之前购买页面点进去调试,可能出现无法填写 AppCode等情况,继而测试不成功。
![1670057366898](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670057366898.png)
下面是调试内容展示:
![1670057336563](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670057336563.png)
测试:手机成功收到短信,且内容为code所写号码。
我们使用 postman进行测试。
![1670057900903](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670057900903.png)
![1670057922184](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670057922184.png)
![1670057863198](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670057863198.png)
测试成功。
ps:当我们在页面上点击“发送验证码”,我们不能通过js代码带上我们的APPCODE ,这样就直接将APPCODE 暴露给别人了,然后别人使用它发送大量短信,这样就有危机了。我们通过后台来发送验证码,这样比较保险。
短信验证码属于第三方服务,我们就放在 gulimall-third-party 服务下。
④ 后台代码调试
复制 相应java调试代码进行测试
@Test
public void sendSms() {
String host = "http://dingxin.market.alicloudapi.com";
String path = "/dx/sendSms";
String method = "POST";
String appcode = "8c7b3796b27f44eb9569bfd090e74225";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile", "15884430987");
querys.put("param", "code:123456");
querys.put("tpl_id", "TP1711063");
Map<String, String> bodys = new HashMap<String, String>();
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
将 HttpUtils 从页面复制过来:https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
然后在gulimall-third-party服务下新建 utils包存放相应的工具类。
测试:手机能获取到验证码。
![1670059465958](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670059465958.png)
我们将 获取验证码的方法 抽取成一个类:SmsComponent
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data
@Component
public class SmsComponent {
private String host;
private String path;
private String appcode;
private String tpl_id;
public void sendSmsCode(String mobile,String code) {
String host = "http://dingxin.market.alicloudapi.com";
String path = "/dx/sendSms";
String method = "POST";
String appcode = "8c7b3796b27f44eb9569bfd090e74225";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile", mobile);
// querys.put("param", "code:123456");
querys.put("param", "code:"+code);
querys.put("tpl_id", "TP1711063");
Map<String, String> bodys = new HashMap<String, String>();
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
在配置文件 application.yml 中配置相应参数:
sms:
host: http://dingxin.market.alicloudapi.com
path: /dx/sendSms
appcode: xxxxxxxxxxxxxxx #这里写自己的
tpl_id: TP1711063
在测试类中进行测试:
@Autowired
SmsComponent smsComponent;
@Test
public void testSendCode() {
smsComponent.sendSmsCode("15884430987","123456");
}
测试成功,手机收到验证码。
2.5.3 验证码之防刷校验
1.编写短信验证controller,方便其它服务调用
在 第三方服务下编写:
@RestController
@RequestMapping("/sms")
public class SmsSendController {
@Autowired
SmsComponent smsComponent;
/**
* 提供给别的服务进行调用
* @param phone
* @param code
* @return
*/
@GetMapping("/sendcode")
public R sendCode(@RequestParam("phone") String phone,@RequestParam("code")String code){
smsComponent.sendSmsCode(phone,code);
return R.ok();
}
}
2. 认证服务远程调用发送短信验证码功能
①依赖已导入,开启远程服务调用功能
![1670080535151](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670080535151.png)
② 远程调用接口编写
在认证服务下新建 feign包:
@FeignClient("gulimall-third-party")
public interface ThirdPartyFeignService {
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code")String code);
}
3.认证服务中编写获取短信验证码的controller
@Controller
public class LoginController {
/**
* 发送一个请求直接跳转到一个页面。
* springMVC viewcontroller:将请求和页面映射过来。
*/
@Autowired
ThirdPartyFeignService thirdPartyFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone){
String code = UUID.randomUUID().toString().substring(0, 5);
thirdPartyFeignService.sendCode(phone,code);
return R.ok();
}
}
4. 注册页面编写请求发送验证码功能
①为手机号码input框设置id,方便获取
![1670076546938](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670076546938.png)
②发送请求,请求后台发送短信验证码
![1670081135651](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670081135651.png)
$.get("/sms/sendcode?phone=" + $("#phoneNum").val());
测试:成功发送。
![1670077481902](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670077481902.png)
5.防止一个手机号码60s内多次获取短信验证码
解决思路:将短信验证码存储在redis中,key为phoneNum,value为验证码和存储时系统的当前时间。从redis中查询为null则调用发送短信验证码,若查询不为空则判断是否超过60s,是则再次调用发送短信验证码,否则返回提示信息。
①导入redis的依赖,并配置好redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties
spring.redis.host=192.168.56.10
spring.redis.port=6379
②common的constant中编写存储在redis中的验证码前置
public class AuthServerConstant {
public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
}
③编写触发错误时的代码
![1670081406924](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670081406924.png)
SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
④LoginController修改
@Controller
public class LoginController {
/**
* 发送一个请求直接跳转到一个页面。
* springMVC viewcontroller:将请求和页面映射过来。
*/
@Autowired
ThirdPartyFeignService thirdPartyFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone){
//TODO 1、接口防刷。
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(redisCode)){
long l = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() - l < 60000){
//60秒内不能再发
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
//2、验证码的再次校验。redis。存 key-phone,value-code sms:code:15884430987 -> 45678
String code = UUID.randomUUID().toString().substring(0, 5)+"_"+System.currentTimeMillis();
// redis缓存验证码,防止同一个phone在60秒内再次发送验证码
//set(K var1, V var2, long var3, TimeUnit var5)
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10, TimeUnit.MINUTES);
thirdPartyFeignService.sendCode(phone,code);
return R.ok();
}
}
⑤ 注册页面的请求发送验证码的回调函数编写
![1670081585164](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670081585164.png)
//1、给指定手机号码发送验证码
$.get("/sms/sendcode?phone=" + $("#phoneNum").val(),function (data) {
if (data.code != 0){
alert(data.msg);
}
});
⑥测试,60秒内发送多次(刷新页面即可)
![1670080078059](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670080078059.png)
redis中存储:
![1670081729708](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670081729708.png)
2.5.4 一步一坑的注册页环境
①编写 vo封装注册页内容
这里使用后端进行验证: JSR303校验
@Data
public class UserRegistVo {
//添加 JR303校验注解
@NotEmpty(message = "用户名必须提交")
@Length(min = 6, max = 18,message = "用户名必须是6-18位字符")
private String userName;
@NotEmpty(message = "密码必须提交")
@Length(min = 6, max = 18,message = "密码必须是6-18位字符")
private String password;
@NotEmpty(message = "手机号必须填写")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")//第一位是1,第二位是3-9,后面9位都是 0-9
private String phone;
@NotEmpty(message = "验证码必须填写")
private String code;
}
回顾前面的JSR303校验怎么用:
JSR303校验的结果,被封装到BindingResult
,再结合BindingResult.getFieldErrors()
方法获取错误信息,有错误就重定向至注册页面。
②编写 controller接口
使用@Valid注解开启数据校验功能,将校验后的结果封装到BindingResult中
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result) {
if (result.hasErrors()) {
//校验出错,转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
//注册成功回到首页,回到登录页
return "redirect:/login.html";
}
③ 编写注册页面
为每个input框设置name属性,值需要与Vo的属性名一一对应
![1670133742383](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670133742383.png)
![1670133759341](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670133759341.png)
![1670133780511](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670133780511.png)
![1670133806711](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670133806711.png)
点击注册按钮没有发送请求,说明:为注册按钮绑定了单击事件,禁止了默认行为。将绑定的单击事件注释掉
![1670133850075](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670133850075.png)
④为Model绑定校验错误信息
方法一:
![1670133955608](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670133955608.png)
方法二:
![1670133981264](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670133981264.png)
⑤编写前端页面获取错误信息
1. 导入thymeleaf的名称空间
![1670134048254](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134048254.png)
2. 封装错误信息
![1670134112771](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134112771.png)
![1670134175222](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134175222.png)
<form action="/regist" method="post" class="one">0
<div class="register-box">
<label class="username_label">用 户 名
<input name="userName" maxlength="20" type="text" placeholder="您的用户名和登录名">
</label>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors, 'userName')?errors.userName:''):''}">
</div>
</div>
<div class="register-box">
<label class="other_label">设 置 密 码
<input name="password" maxlength="20" type="password" placeholder="建议至少使用两种字符组合">
</label>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors, 'password')?errors.password:''):''}">
</div>
</div>
<div class="register-box">
<label class="other_label">确 认 密 码
<input maxlength="20" type="password" placeholder="请再次输入密码">
</label>
<div class="tips">
</div>
</div>
<div class="register-box">
<label class="other_label">
<span>中国 0086∨</span>
<input name="phone" class="phone" id="phoneNum" maxlength="20" type="text" placeholder="建议使用常用手机">
</label>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors, 'phone')?errors.phone:''):''}">
</div>
</div>
<div class="register-box">
<label class="other_label">验 证 码
<input name="code" maxlength="20" type="text" placeholder="请输入验证码" class="caa">
</label>
<a id="sendCode">发送验证码</a>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors, 'code')?errors.code:''):''}">
</div>
⑥测试--坑集合
1.出现问题: Request method 'POST' not supported
出现问题的原因:表单的提交使用的是post请求,会原封不动的转发给reg.html,但是/reg.html(路径映射默认都是get方式访问)
解决方案:如下图所示
![1670134295061](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134295061.png)
2.出现问题:刷新页面,会重复提交表单
![1670134321688](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134321688.png)
出现问题的原因:转发,原封不动转发过去
解决方案:使用重定向
![1670134361781](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134361781.png)
3.出现问题:转发,数据都封装在Model中,而重定向获取不到
解决方案:使用 RedirectAttributes
RedirectAttributes的方法讲解:Spring MVC ---- RedirectAttributes 使用,请求转发携带参数总结
![1670134442677](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134442677.png)
4.出现问题:重定向到服务端口地址
![1670134494006](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134494006.png)
解决方案: 写完整的域名路径
![1670134515206](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134515206.png)
说明: RedirectAttributes的 addFlashAttribute()方法是将errors保存在session中,刷新一次就没了
![1670134561639](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670134561639.png)
5.出现问题:分布式下重定向使用session存储数据会出现一些问题
解决方案:后续会说明
6.至此,暂时比较完整的controller接口代码如下:
/**
* //TODO 重定向携带数据,利用session原理。将数据放在session中,只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
*
*
*
* // TODO 1、分布式下的session问题。
* RedirectAttributes redirectAttributes : 模拟重定向携带数据
* @param vo
* @param result
* @param redirectAttributes
* @return
*/
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes, HttpSession session) {
if (result.hasErrors()) {
/**
* .map(fieldError ->{
* String field = fieldError.getField();
* String defaultMessage = fieldError.getDefaultMessage();
* errors.put(field,defaultMessage);
* return
* })
*
*
*/
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
// model.addAttribute("errors", errors);
redirectAttributes.addFlashAttribute("errors",errors);
// Request method 'POST' not supported
//用户注册 -》/regist[post] ---->转发/reg.html(路径映射默认都是get方法访问的。)
//真正注册。调用远程服务进行注册
//校验出错,转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
//注册成功回到首页,回到登录页
return "redirect:/login.html";
}
测试:先全写错的,验证码不写。
![1670123370638](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670123370638.png)
后端校验提示出现。
![1670123388007](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670123388007.png)
ps: 以上内容是注册用户
在 gulimall-auth-server服务中编写注册的主体逻辑
- 从redis中确认手机验证码是否正确,一致则删除验证码,(令牌机制)
- 会员服务调用成功后,重定向至登录页(防止表单重复提交),否则封装远程服务返回的错误信息返回至注册页面
- 重定向的请求数据,可以利用RedirectAttributes参数转发
- 但是他是利用的session原理,所以后期我们需要解决分布式的session问题
- 重定向取一次后,session数据就消失了,因为使用的是.addFlashAttribute(
- 重定向时,如果不指定host,就直接显示了注册服务的ip,所以我们重定义写http://…
注: RedirectAttributes可以通过session保存信息并在重定向的时候携带过去
2.5.5 异常机制
1.校验验证码
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes, HttpSession session) {
if (result.hasErrors()) {
/**
* .map(fieldError ->{
* String field = fieldError.getField();
* String defaultMessage = fieldError.getDefaultMessage();
* errors.put(field,defaultMessage);
* return
* })
*/
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
// model.addAttribute("errors", errors);
redirectAttributes.addFlashAttribute("errors",errors);
// Request method 'POST' not supported
//用户注册 -》/regist[post] ---->转发/reg.html(路径映射默认都是get方法访问的。)
//校验出错,重定向到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
//1、校验验证码
String code = vo.getCode();
String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)){
if (code.equals(s.split("_")[0])){
//删除验证码;令牌机制
redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//验证通过。//真正注册。调用远程服务进行注册。
}else{
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
}else{
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
//注册成功回到首页,回到登录页
return "redirect:/login.html";
}
验证短信验证码通过,下面开始去数据库保存。
member远程服务
通过gulimall-member
会员服务注册逻辑
- 通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
- 如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间
2. 会员服务中编写Vo接受数据
@Data
public class MemberRegistVo {
/**
* 能调用远程服务,说明是auth服务过来的,这个时候就不需要进行校验了。
*/
private String userName;
private String password;
private String phone;
}
3. 编写会员服务的用户注册接口
MemberController
//因为我们注册会提交很多的东西,所以是 post方式提交
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
memberService.regist(vo);
return R.ok();
}
MemberServiceImpl
@Override
public void regist(MemberRegistVo vo) {
MemberDao memberDao = this.baseMapper;
MemberEntity entity = new MemberEntity();
//设置默认等级
MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
entity.setLevelId(levelEntity.getId());
//检查用户名和手机号是否唯一。为了让controller能够感知异常,使用异常机制:一直往上抛
checkPhoneUnique(vo.getPhone());
checkUsernameUnique(vo.getUserName());
entity.setMobile(vo.getPhone());
entity.setUsername(vo.getUserName());
//密码要进行加密存储。
memberDao.insert(entity);
}
MemberLevelDao.xml -> :查询会员的默认等级
<select id="getDefaultLevel" resultType="com.atguigu.gulimall.member.entity.MemberLevelEntity">
SELECT * FROM `ums_member_level` WHERE default_status = 1
</select>
这里:检查用户名和手机号是否唯一
这里采用异常机制处理,如果查出用户名或密码不唯一则向上抛出异常
异常类的编写
会员服务下创建 exception包
PhoneExistException
public class PhoneExistException extends RuntimeException{
public PhoneExistException() {
super("手机号存在");
}
}
UsernameExistException
public class UsernameExistException extends RuntimeException{
public UsernameExistException() {
super("用户名存在");
}
}
检查方法编写->MemberServiceI
void checkPhoneUnique(String phone) throws PhoneExistException;
void checkUsernameUnique(String username) throws UsernameExistException;
MemberServiceImpl
@Override
public void checkPhoneUnique(String phone) throws PhoneExistException{
MemberDao memberDao = this.baseMapper;
Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (mobile > 0){
throw new PhoneExistException();
}
}
@Override
public void checkUsernameUnique(String username) throws UsernameExistException {
MemberDao memberDao = this.baseMapper;
Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
if (count > 0){
throw new UsernameExistException();
}
}
![1670140858325](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670140858325.png)
如果抛出异常,则进行捕获
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
try{
memberService.regist(vo);
}catch (Exception e){
//不同的异常,有不同的处理
}
return R.ok();
}
密码的设置,前端传来的密码是明文,存储到数据库中需要进行加密。
2.5.6 MD5&盐值&BCrypt
首先,加密分为可逆加密和不可逆加密。密码的加密为不可逆加密
MD5
- Message Digest algorithm 5,信息摘要算法
- 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
- 容易计算:从原数据计算出MD5值很容易。
- 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
- 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
- 不可逆
加盐:
-
通过生成随机数与MD5生成字符串进行组合
-
数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可
@Test
public void contextLoads() {
//e10adc3949ba59abbe56e057f20f883e
//抗修改性:彩虹表。 123456 -> xxxx
String s = DigestUtils.md5Hex("123456");
//MD5不能直接进行密码的加密存储:可以被直接暴力破解
// System.out.println(s);
}
Apache.common下DigestUtils工具类的md5Hex()方法,将MD5加密后的数据转化为16进制
MD5并安全,很多在线网站都可以破解MD5,通过使用彩虹表,暴力破解。
![1670143619736](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670143619736.png)
因此,可以通过使用MD5+盐值进行加密
盐值:随机生成的数
方法1是加默认盐值:$1$xxxxxxxx
方法2是加自定义盐值
//盐值加密:随机值 加盐:$1$+8位字符
//$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1
//验证: 123456进行盐值(去数据库查)加密
// String s1 = Md5Crypt.md5Crypt("123456".getBytes(), "$1$qqqqqqqq");
// System.out.println(s1);
这种方法需要在数据库添加一个专门来记录注册时系统时间的字段,此外还需额外在数据库中存储盐值。
可以使用Spring家的BCryptPasswordEncoder,它的encode()方法使用的就是MD5+盐值进行加密,盐值是随机产生的,通过matches()方法进行密码是否一致。
//使用 spring家的
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//$2a$10$R/VBymW1UA.VzeBedBcspe.iypJIyQiWkka/Ds5SDG7h6r0wQsF6G
String encode = passwordEncoder.encode("123456");
boolean matches = passwordEncoder.matches("123456", "$2a$10$R/VBymW1UA.VzeBedBcspe.iypJIyQiWkka/Ds5SDG7h6r0wQsF6G");
// $2a$10$jLJp4edbLb9pnCg9quGk0u2uvsm4E/6TD5zi1wqHY4jz/f1ydS.LS=>true
System.out.println(encode+"=>"+matches);
用户注册业务中的密码加密
![1670143940986](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670143940986.png)
//密码要进行加密存储。
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());
entity.setPassword(encode);
2.5.7 注册完成
1.在common的exception包下,编写异常枚举
![1670165234576](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670165234576.png)
USER_EXIST_EXCEPTION(15001,"用户存在"),
PHONE_EXIST_EXCEPTION(15002,"手机号存在"),
2. 进行异常的捕获
MemberController
//因为我们注册会提交很多的东西,所以是 post方式提交
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
try{
memberService.regist(vo);
}catch (PhoneExistException e){
return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
}catch (UsernameExistException e){
return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
3. 远程服务接口编写
在 auth 服务下新建 MemberFeignService
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
public R regist(@RequestBody UserRegistVo vo);
}
4. 远程服务调用
![1670165444033](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670165444033.png)
@Autowired
MemberFeignService memberFeignService;
/**
* //TODO 重定向携带数据,利用session原理。将数据放在session中,只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
* <p>
* <p>
* <p>
* // TODO 1、分布式下的session问题。
* RedirectAttributes redirectAttributes : 模拟重定向携带数据
*
* @param vo
* @param result
* @param redirectAttributes
* @return
*/
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes, HttpSession session) {
if (result.hasErrors()) {
/**
* .map(fieldError ->{
* String field = fieldError.getField();
* String defaultMessage = fieldError.getDefaultMessage();
* errors.put(field,defaultMessage);
* return
* })
*/
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
// model.addAttribute("errors", errors);
redirectAttributes.addFlashAttribute("errors", errors);
// Request method 'POST' not supported
//用户注册 -》/regist[post] ---->转发/reg.html(路径映射默认都是get方法访问的。)
//校验出错,重定向到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
//1、校验验证码
String code = vo.getCode();
String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)) {
if (code.equals(s.split("_")[0])) {
//删除验证码;令牌机制
redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//验证通过。//真正注册。调用远程服务进行注册。
R r = memberFeignService.regist(vo);
if (r.getCode() == 0) {
//成功
return "redirect:http://auth.gulimall.com/login.html";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData("msg",new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
}
5.注册页错误消息提示
![1670165495445](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670165495445.png)
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors, 'msg')?errors.msg:''):''}">
</div>
6.一些测试中发现需要修改的地方
![1670163928941](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670163928941.png)
/**
* 获取短信验证码
*
* @param phone
* @return
*/
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone) {
//TODO 1、接口防刷。
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(redisCode)) {
long l = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() - l < 60000) {
//60秒内不能再发
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
//2、验证码的再次校验。redis。存 key-phone,value-code sms:code:15884430987 -> 45678
String code = UUID.randomUUID().toString().substring(0, 5);
String substring = code + "_" + System.currentTimeMillis();
// redis缓存验证码,防止同一个phone在60秒内再次发送验证码
//set(K var1, V var2, long var3, TimeUnit var5)
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, substring, 10, TimeUnit.MINUTES);
thirdPartyFeignService.sendCode(phone, code);
return R.ok();
}
![1670165757529](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670165757529.png)
需要使用重定向,前面代码已经给了。
测试:
![1670165026176](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670165026176.png)
成功重定向到 登录页面。
![1670165715304](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670165715304.png)
数据库中也有相应记录的保存。
![1670165072298](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670165072298.png)
2.5.8 账户密码登录完成
1.编写Vo-> 认证服务下新建 UserLoginVo
@Data
public class UserLoginVo {
private String loginacct;
private String password;
}
2.数据绑定
将ul包在表单里面
![1670166876778](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670166876778.png)
3. 编写登录接口
说明:不能加@RequestBody注解,这里是页面直接提交数据,数据类型是map并非json
@PostMapping("/login")
public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){//因为请求第一次过来是页面传过来的是kv键值对(表单传过来的),不是JSON,所以不加@RequestBody,调用远程服务时将其又转换为了JSON
//远程登录
R login = memberFeignService.login(vo);
if (login.getCode() == 0){
//成功
return "redirect:http://gulimall.com";
}else{
//失败,回到登录页
Map<String, String> errors = new HashMap<>();
errors.put("msg", login.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
接下来我们需要调用 会员服务进行 登录验证。
4.member服务的Vo编写
MemberLoginVo
@Data
public class MemberLoginVo {
private String loginacct;
private String password;
}
5. member服务用户校验接口编写
MemberController
@PostMapping("/login")
public R login(@RequestBody MemberLoginVo vo){
MemberEntity entity = memberService.login(vo);
if (entity != null){
return R.ok();
}else{
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getCode(), BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getMsg());
}
}
MemberServiceImpl
@Override
public MemberEntity login(MemberLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword();
//1、去数据库查询 SELECT * FROM `ums_member` WHERE username = ? OR mobile = ?
MemberDao memberDao = this.baseMapper;
MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct)
.or().eq("mobile", loginacct));
if (entity == null){
//登录失败
return null;
}else{
//1、获取到数据库的password $2a$10$OuDQdPAHqJRdzbQvJWeJwu8UQ.mVSw/i0MP8E4CWu2bjQmM3Xvt4m
String passwordDb = entity.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//2、密码匹配
boolean matches = passwordEncoder.matches(password, passwordDb);
if (matches){
return entity;
}else{
return null;
}
}
}
MemberLevelDao.xml
<select id="getDefaultLevel" resultType="com.atguigu.gulimall.member.entity.MemberLevelEntity">
SELECT * FROM `ums_member_level` WHERE default_status = 1
</select>
编写异常枚举
BizCodeEnume
LOGINACCT_PASSWORD_INVALID_EXCEPTION(15003,"账号密码错误");
6.远程服务接口编写
认证服务调用会员服务接口进行登录验证。
MemberFeignService.java
@PostMapping("/member/member/login")
R login(@RequestBody UserLoginVo vo);
LoginController.java
@PostMapping("/login")
public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){//因为请求第一次过来是页面传过来的是kv键值对(表单传过来的),不是JSON,所以不加@RequestBody,调用远程服务时将其又转换为了JSON
//远程登录
R login = memberFeignService.login(vo);
if (login.getCode() == 0){
//成功
return "redirect:http://gulimall.com";
}else{
//失败,回到登录页
Map<String, String> errors = new HashMap<>();
errors.put("msg", login.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
7.页面错误消息提示
![1670222208092](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670222208092.png)
在 form 表单下面新增一个 div存放错误消息提示
<div class="tips" style="color:red"
th:text="${errors!=null?(#maps.containsKey(errors, 'msg')?errors.msg:''):''}"></div>
8.测试
先随便写一些错误的账号密码,看提示是否出来。
![1670169147813](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670169147813.png)
ps:注意这里将之前的 注册时 错误的存放修改一下。
LoginController的 regist方法
![1670167979849](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670167979849.png)
输入正确的账号密码,成功跳转到首页,至于显示登录用户个人信息,后面在进行补充。
![1670169157569](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670169157569.png)
2.5.9 社交登录
1、OAuth 2.0
下面是老师课件内容:
![1670244151569](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244151569.png)
![1670244229269](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244229269.png)
![1670244251221](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244251221.png)
![1670244302051](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244302051.png)
![1670244336826](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244336826.png)
![1670244358570](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244358570.png)
![1670244376216](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244376216.png)
![1670244394288](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244394288.png)
2、下面进入实际操作。
首先我们先明白 OAuth2.0的原理图:
![1670244217441](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244217441.png)
①因为现在微博需要开发者身份认证且耗时较久,所以此次社交登录选择利用能够快速开始项目实践的Gitee进行测试。
注册地址:https://gitee.com/oauth/applications/
对应文档:Gitee OAuth 文档
注册内容(可以根据文档来进行):
![1670245013828](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670245013828.png)
下面是随机找的一个图标
![1670244678786](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670244678786.png)
本次Gitee OAuth2登录原理:
![1670245192903](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670245192903.png)
②修改 login.html页面
对应的是 登录页面第三方登录图标,以及跳转地址。
![1670226422558](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670226422558.png)
<a href="https://gitee.com/oauth/authorize?client_id=821c228b5c8a2a597419ee2ac8b98d05d387d4a9a8a9f37480d7213f24c64582&redirect_uri=http://gulimall.com/success&response_type=code">
<img style="width: 50px;height: 18px" src="/static/login/JD_img/gitee.png"/>
</a>
根据图片地址将图片上传到 Nginx下。
![1670226277600](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670226277600.png)
![1670245095728](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670245095728.png)
③测试获取 code 码
点击 Gitee登录
![1670226798572](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670226798572.png)
引导到指定页面进行登录。
![1670227097025](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670227097025.png)
![1670240427530](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670240427530.png)
登录完成获得:code码
![1670240450428](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670240450428.png)
④利用 postman进行获取 access_token测试
1.获取 access_token
![1670245443018](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670245443018.png)
2.有了access_token之后,我们根据 API文档即可获取到我们想要的所有信息。
![1670245584350](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670245584350.png)
以上测试都是按照 API文档进行测试的。
![1670245634919](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670245634919.png)
![1670245655455](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670245655455.png)
3、完成社交登录功能。
像 client_id、client_secret、access_token等应该保密,不应爱直接放在路径地址后,所以我们应该编写后端代码进行获取。
①首先,修改之前设置的回调地址:
![1670308695925](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670308695925.png)
页面也需要修改:
<a href="https://gitee.com/oauth/authorize?client_id=821c228b5c8a2a597419ee2ac8b98d05d387d4a9a8a9f37480d7213f24c64582&redirect_uri=http://auth.gulimall.com/oauth2.0/gitee/success&response_type=code">
<img style="width: 50px;height: 18px" src="/static/login/JD_img/gitee.png"/>
</a>
②编写后端代码
- 首先为了方便编写请求,我们导入之前 短信验证码使用过的 HttpUtils类,将其放在 common包。
依赖地址: https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
<!--导入 org.apache.http.client.HttpClient等依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.15</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.3.7.v20160115</version>
</dependency>
<!--<dependency>-->
<!-- <groupId>junit</groupId>-->
<!-- <artifactId>junit</artifactId>-->
<!-- <version>4.5</version>-->
<!-- <scope>test</scope>-->
<!--</dependency>-->
test依赖暂时不需要。
-
编写封装社交账户用户信息的实体类(注意,这里老师课件上是将 获取Access token的得到的JSON数据封装成的实体类)。这里我们使用的 Gitee 获取 access_token得到的数据中没有一个能够唯一标识 该社交用户的 字段,因为我们如果使用社交账户登录成功,如果是第一次登录,我们需要将其注册进 会员表中,此时应该有一个能够唯一识别 该用户的字段,通过 微博 登录测试会直接获得 一个 字段“uid”,我们通过Gitee来进行测试,只能再通过一个接口进行获取用户id的信息。
![1670307231238](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670307231238.png)
在 认证服务的vo包下新建 SocialUser(之后我们通过接口文档提供的url地址来将相应信息设置进去)
@Data
public class SocialUser {
private String access_token;
private long expires_in;
private String uid;
}
老师课件上的:
![1670307054279](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670307054279.png)
利用在线JSON转换工具获得的一个Javabean实体类。
![1670249399800](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670249399800.png)
- 为member表新增三个字段
![1670307598018](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670307598018.png)
-
认证服务下编写 处理社交登录请求的类
OAuth2Controller(最终)
/**处理社交登录请求 * @author wystart * @create 2022-12-05 21:42 */ @Slf4j @Controller public class OAuth2Controller { @Autowired MemberFeignService memberFeignService; /** * 社交登录成功回调 * @param code * @return * @throws Exception */ @GetMapping("/oauth2.0/gitee/success") public String gitee(@RequestParam("code") String code) throws Exception { Map<String, String> map = new HashMap<>(); map.put("grant_type", "authorization_code"); map.put("code", code); map.put("client_id", "821c228b5c8a2a597419ee2ac8b98d05d387d4a9a8a9f37480d7213f24c64582"); map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/gitee/success"); map.put("client_secret", "4fba1e08692dcc06909f213d05d3e5cfa531458815dd494629a9fea92fc25ccc"); Map<String,String> headers = new HashMap<>(); Map<String,String> querys = new HashMap<>(); //1、根据code 换取 access_token HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", headers, querys, map); //2、处理 if (response.getStatusLine().getStatusCode() == 200) { //获取到了 accessToken String json = EntityUtils.toString(response.getEntity()); JSONObject jsonObject = JSON.parseObject(json); String accessToken = jsonObject.getString("access_token"); Long expiresIn = Long.valueOf(jsonObject.getString("expires_in")); //通过 access_token 获取用户 id Map<String, String> map1 = new HashMap<>(); map1.put("access_token", accessToken); HttpResponse response1 = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<>(), map1); String json1 = EntityUtils.toString(response1.getEntity()); JSONObject jsonObject1 = JSON.parseObject(json1); String id = jsonObject1.getString("id"); //将 access_token expires_in uid封装到 SocialUser SocialUser socialUser = new SocialUser(); socialUser.setUid(id); socialUser.setAccess_token(accessToken); socialUser.setExpires_in(expiresIn); //知道当前是哪个社交用户 //1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员账号 //登录或者注册这个社交用户 R oauthlogin = memberFeignService.oauthlogin(socialUser); if (oauthlogin.getCode() == 0) { MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() { }); log.info("登录成功:用户信息:{}",data.toString()); //2、登录成功就跳回首页 return "redirect:http://gulimall.com"; } else { return "redirect:http://auth.gulimall.com/log.html"; } } else { return "redirect:http://auth.gulimall.com/log.html"; } } }
-
会员服务下编写 处理社交登录的类
MemberEntity实体类下新增3个字段
private String socialUid; private String accessToken; private Long expiresIn;
MemberController
//社交登录 @PostMapping("/oauth2/login") public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception { MemberEntity entity = memberService.login(socialUser); if (entity != null){ //TODO 1、登录成功处理 return R.ok().setData(entity); }else{ return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getCode(), BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getMsg()); } }
MemberServiceImpl
@Override public MemberEntity login(SocialUser socialUser) throws Exception { //登录和注册合并逻辑 String uid = socialUser.getUid(); //1、判断当前社交用户是否已经登录过系统; MemberDao memberDao = this.baseMapper; MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid)); if (memberEntity != null) { //这个用户已经注册 MemberEntity update = new MemberEntity(); update.setId(memberEntity.getId()); update.setAccessToken(socialUser.getAccess_token()); update.setExpiresIn(socialUser.getExpires_in()); memberDao.updateById(update); memberEntity.setAccessToken(socialUser.getAccess_token()); memberEntity.setExpiresIn(socialUser.getExpires_in()); return memberEntity; } else { //2、没有查询到当前社交用户对应的记录我们就需要注册一个 MemberEntity regiset = new MemberEntity(); try { //3、查询当前社交用户的社交账户信息(昵称,性别等) Map<String, String> query = new HashMap<>(); query.put("access_token", socialUser.getAccess_token()); HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<>(), query); if (response.getStatusLine().getStatusCode() == 200) { //查询成功 String json = EntityUtils.toString(response.getEntity()); JSONObject jsonObject = JSON.parseObject(json); //昵称 String name = jsonObject.getString("name"); regiset.setNickname(name); } } catch (Exception e) { } regiset.setSocialUid(socialUser.getUid()); regiset.setAccessToken(socialUser.getAccess_token()); regiset.setExpiresIn(socialUser.getExpires_in()); memberDao.insert(regiset); return regiset; } }
为了方便调用 ,我们将 HttpUtils放进 common服务中。
此外,我们将 SocialUser类也复制到member服务的vo包下。
-
远程调用:认证服务调用会员服务
认证服务下的MemberFeignService
@PostMapping("/member/member/oauth2/login") R oauthlogin(@RequestBody SocialUser socialUser) throws Exception;
新增 MemberRespVo封装数据
直接将 MemberEntity 复制过来
@ToString @Data public class MemberRespVo { /** * id */ private Long id; /** * 会员等级id */ private Long levelId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 昵称 */ private String nickname; /** * 手机号码 */ private String mobile; /** * 邮箱 */ private String email; /** * 头像 */ private String header; /** * 性别 */ private Integer gender; /** * 生日 */ private Date birth; /** * 所在城市 */ private String city; /** * 职业 */ private String job; /** * 个性签名 */ private String sign; /** * 用户来源 */ private Integer sourceType; /** * 积分 */ private Integer integration; /** * 成长值 */ private Integer growth; /** * 启用状态 */ private Integer status; /** * 注册时间 */ private Date createTime; private String socialUid; private String accessToken; private Long expiresIn; }
-
测试
登录成功返回首页(这里很容易出现 超时现象,多试几次即可。)
![1670308759420](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670308759420.png)
数据库多出一条记录
![1670308809350](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670308809350.png)
![1670308820786](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670308820786.png)
控制台成功打印:
![1670308853340](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670308853340.png)
- 总体步骤总结:
![1670292726501](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670292726501.png)
ps:
注意点:
登录成功得到了code,这不应该提供给用户;
拿着code还有其他信息APP-KEY去获取token,更不应该给用户看到
应该回调的是后台的controller,在后台处理完token逻辑后返回
把成功后回调改为:http://auth.gulimall.com/oauth2.0/gitee/success
通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录
进行账号保存,主要有uid、token、expires_in
若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页
token保存
登录包含两种流程,实际上包括了注册和登录
如果之前未使用该社交账号登录,则使用
token
调用开放api获取社交账号相关信息(头像等),注册并将结果返回如果之前已经使用该社交账号登录,则更新
token
并将结果返回![1670309377030](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670309377030.png)
2.5.10 分布式session
登录成功后,首页NickName的显示
![1670316077618](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670316077618.png)
在之前的单体应用中,会将登录成功后的属性保存到session中
![1670316119321](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670316119321.png)
Thymeleaf取出session
![1670316262924](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670316262924.png)
index.html
![1670316293920](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670316293920.png)
出现问题:NickName未显示
出现问题的原因:Session不能跨域使用
auth.gulimall域下的session作用域只限于auth.gulimall域,gulimall域是获取不到的,不共享的
![1670316567816](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670316567816.png)
![1670316587507](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670316587507.png)
session原理:
![1670329942463](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670329942463.png)
![1670329999406](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670329999406.png)
session共享问题
1.同域名,同一个服务下,session复制多份:第一个服务器下存的session,在第二个服务器下没有
2.不同域名,不同服务下,session如何共享。
解决方案:
方案一:sessio复制,不采用
![1670330049141](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670330049141.png)
方案二:客户端存储,不采用
![1670330068118](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670330068118.png)
方案三: 利用hash一致性,进行负载均衡,可以采用但是这里不采用
![1670330088486](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670330088486.png)
方案四: 统一存储,这里采用这套方案
![1670330107432](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670330107432.png)
每个服务:把session放到redis中存储;发卡(JSESSIONID)并放大域名;------->利用SpringSession解决 :
![1670330131244](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670330131244.png)
后端统一存储,前端一个卡去往任何服务都通用。
2.5.11 SpringSession 整合 redis 完成 session域问题
![1670335752169](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670335752169.png)
![1670335771152](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670335771152.png)
相关文档:
https://spring.io/projects/spring-session-data-redis
https://docs.spring.io/spring-session/docs/2.4.2/reference/html5/#modules
通过SpringSession修改session的作用域
会员服务、订单服务、商品服务,都是去redis里存储session
①整合依赖
认证服务和商品服务均导入此依赖(如果遵循微服务自制原则,我们就不应该将依赖导入到公共包,而是编写我们自己的微服务)
<!--1、整合SpringSession完成session共享问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
② 配置文件配置
认证服务和商品服务都可以这样配置。当然,最简单的就可以直接配置会话存储类型即可。因为redis相应配置已经配置,可以不配了。
# 会话存储类型
spring.session.store-type=redis
# 会话超时。如果未指定持续时间后缀,则使用秒。
server.servlet.session.timeout=30m
③使用@EnableRedisHttpSession注解开启Spring Session with Redis功能
@EnableRedisHttpSession //整合redis作为session存储
@EnableFeignClients //开启远程调用功能
@EnableDiscoveryClient //开启服务注册发现功能
@SpringBootApplication
public class GulimallAuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallAuthServerApplication.class, args);
}
}
④测试:看是否将session存入redis中
出现问题:DefaultSerializer requires a Serializable payload but received an object of type [com.atguigu.gulimall.auth.vo.MemberRespVo]
出现的原因: MemberRespVo未实现序列化解接口
解决方案:
public class MemberRespVo implements Serializable //实现序列化
这里,我们可以将MemberRespVo复制到commom中,因为,product服务还需要将Session中存储的loginUser反序列化为MemberRespVo对象。
保存成功,数据库有相应数据。
![1670336350639](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670336350639.png)
但是,首页依然不显示,这是因为域名范围限制,下面我们就将自定义SpringSession完成子域session共享。
⑤解决子域session共享问题:
认证服务和商品服务下均配置该类:
@Configuration
public class GulimallSessionConfig {
//子域共享问题解决
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");// 扩大session作用域,也就是cookie的有效域
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
// 使用json序列化方式来序列化对象数据到redis中
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
前端页面修改,需要进行非空判断
<a href="http://auth.gulimall.com/login.html">你好,请登录:[[${session.loginUser==null?'':session.loginUser.nickname}]]</a>
测试:成功显示nickname
![1670336645955](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670336645955.png)
数据库存入相应的实体类。
![1670335296751](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670335296751.png)
2.5.12 SpringSession原理--装饰者模式
@EnableRedisHttpSession注解导入了RedisHttpSessionConfiguration.class这个配置类
![1670338824779](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670338824779.png)
在 RedisHttpSessionConfiguration.class这个配置类,为容器中注入了一个组件
sessionRepository -> sessionRedisOperations : redis操作session,实现session的增删改查
![1670338897402](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670338897402.png)
调用SpringHttpSessionConfiguration中的springSessionRepositoryFilter()方法,获取一个
SessionRepositoryFilter对象,调用doFilterInternal()对原生的request和response对象进行封装即装饰者模式,request对象调用getSession()方法就会调用wrapperRequest对象的getSession()方法
![1670339101755](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670339101755.png)
![1670339138789](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670339138789.png)
/** * 核心原理 * 1)、@EnableRedisHttpSession导入RedisHttpSessionConfiguration配置 * 1、给容器中添加了一个组件 * sessionRepository -> 【RedisOperationsSessionRepository】 : redis操作session,实现session的增删改查 * 2、SessionRepositoryFilter --Filter:session存储过滤器;每个请求过来都必须经过filter * 1、创建的时候,就自动从容器中获取到了 SessionRepository * 2、原始的request,response都被包装,SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper * 3、以后获取session。 request.getSession(); * 4、wrappedRequest.getSession() --》 SessionRepository中获取到的。 * * * 装饰者模式 * * 自动延期;redis中的数据也是有过期时间。 */
网上的相关内容,可以参考下:https://blog.csdn.net/m0_46539364/article/details/110533408
2.5.13 页面效果完善
1、完善社交登录的页面效果
index.html
![1670405798497](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670405798497.png)
<li>
<a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser==null?'':session.loginUser.nickname}]]</a>
<a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser ==null}">欢迎登录</a>
</li>
<li>
<a href="http://auth.gulimall.com/reg.html" class="li_2" th:if="${session.loginUser==null}">免费注册</a>
</li>
2、通过账号密码登录(账户登录)的用户信息也保存到session中
①编写一个可修改的属性key
公共服务下
![1670404236454](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670404236454.png)
② 用户信息也保存到session中
会员服务 MemberController下
![1670404372950](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670404372950.png)
认证服务LoginController下
![1670404547598](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670404547598.png)
③设置默认的昵称
![1670340657595](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670340657595.png)
④ 登录后,首页页面细化
![1670339966493](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670339966493.png)
<li>
<a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser==null?'':session.loginUser.nickname}]]</a>
<a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser ==null}">欢迎登录</a>
</li>
<li>
<a href="http://auth.gulimall.com/reg.html" class="li_2" th:if="${session.loginUser==null}">免费注册</a>
</li>
已经登录的话,在进入登录页要实现跳转首页的效果
①自己编写业务逻辑,将自动页面映射注释
![1670405026207](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670405026207.png)
②编写接口
LoginController类下
//利用session判断如果登陆过,访问登录页面就直接跳转到首页
@GetMapping("/login.html")
public String loginPage(HttpSession session){
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute == null){
//没登录
return "login";
}else{
return "redirect:http://gulimall.com";
}
}
商品详情页,用户昵称显示
详情页显示昵称
![1670341583728](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670341583728.png)
点击京东图标返回首页
![1670341669687](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670341669687.png)
效果展示:
![1670341501148](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670341501148.png)
搜索页,用户昵称显示
完善检索服务
①导入依赖
![1670341840911](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670341840911.png)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
②配置
application.properties
![1670405373718](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670405373718.png)
③ 开启共享session功能
GulimallSearchApplication 添加注解
@EnableRedisHttpSession
④ 复制配置类
将 GulimallSessionConfig 配置类复制到 检索服务下
![1670405522379](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670405522379.png)
⑤ 前端代码
![1670405562159](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670405562159.png)
<li>
<a th:if="${session.loginUser == null }" href="http://auth.gulimall.com/login.html" class="li_2">你好,请登录</a>
<a th:else style="width: 100px">[[${session.loginUser.nickname}]]</a>
</li>
<li>
<a th:if="${session.loginUser == null }" href="http://auth.gulimall.com/reg.html">免费注册</a>
</li>
效果展示:
![1670405644368](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670405644368.png)
前面使用的是Gitee进行登录,现在我们也可以修改成和老师一样的使用 微博登录。下面是需要修改的代码。
OAuth2回调地址:
![1670407946814](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670407946814.png)
相应文档:
postman测试
![1670407830875](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670407830875.png)
login页面修改:
<li>
<!--<a href="https://gitee.com/oauth/authorize?client_id=821c228b5c8a2a597419ee2ac8b98d05d387d4a9a8a9f37480d7213f24c64582&redirect_uri=http://auth.gulimall.com/oauth2.0/gitee/success&response_type=code">-->
<a href="https://api.weibo.com/oauth2/authorize?client_id=1579098500&response_type=code&redirect_uri=http%3A%2F%2Fauth.gulimall.com%2Foauth2.0%2Fweibo%2Fsuccess">
<!--<img style="width: 50px;height: 18px" src="/static/login/JD_img/gitee.png"/>-->
<img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png"/>
</a>
</li>
SocialUser
@ToString
@Data
public class SocialUser {
//gitee
// private String access_token;
// private Long expires_in;
// private String uid;
//weibo
private String access_token;
private String remind_in;
private long expires_in;
private String uid;
private String isRealName;
}
OAuth2Controller
/**
* 社交登录成功回调
* @param code
* @return
* @throws Exception
*/
// @GetMapping("/oauth2.0/gitee/success")
@GetMapping("/oauth2.0/weibo/success")
public String gitee(@RequestParam("code") String code, HttpSession session, HttpServletResponse servletResponse) throws Exception {
Map<String, String> map = new HashMap<>();
// gitee测试
// map.put("grant_type", "authorization_code");
// map.put("code", code);
// map.put("client_id", "821c228b5c8a2a597419ee2ac8b98d05d387d4a9a8a9f37480d7213f24c64582");
// map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/gitee/success");
// map.put("client_secret", "4fba1e08692dcc06909f213d05d3e5cfa531458815dd494629a9fea92fc25ccc");
//微博测试
map.put("client_id", "1579098500");
map.put("client_secret", "7f19b49cbd0803e6fd875b4f96412f2f");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
Map<String,String> headers = new HashMap<>();
Map<String,String> querys = new HashMap<>();
//1、根据code 换取 access_token
// HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", headers, querys, map);
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, querys, map);
//2、处理
if (response.getStatusLine().getStatusCode() == 200) {
//获取到了 accessToken
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
//gitee
// JSONObject jsonObject = JSON.parseObject(json);
// String accessToken = jsonObject.getString("access_token");
// Long expiresIn = Long.valueOf(jsonObject.getString("expires_in"));
// //通过 access_token 获取用户 id
// Map<String, String> map1 = new HashMap<>();
// map1.put("access_token", accessToken);
// HttpResponse response1 = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<>(), map1);
// String json1 = EntityUtils.toString(response1.getEntity());
// JSONObject jsonObject1 = JSON.parseObject(json1);
// String id = jsonObject1.getString("id");
// //将 access_token expires_in uid封装到 SocialUser
// SocialUser socialUser = new SocialUser();
// socialUser.setUid(id);
// socialUser.setAccess_token(accessToken);
// socialUser.setExpires_in(expiresIn);
//知道当前是哪个社交用户
//1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员账号
//登录或者注册这个社交用户
R oauthlogin = memberFeignService.oauthlogin(socialUser);
if (oauthlogin.getCode() == 0) {
MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
});
log.info("登录成功:用户信息:{}",data.toString());
//1、第一次使用session:命令浏览器保存卡号。JSESSIONID这个cookie;
//以后浏览器访问哪个网站就会带上这个网站的cookie;
//子域之间:gulimall.com auth.gulimall.com order.gulimall.com
//发卡的时候(指定域名为父域名),即使是子域系统发的卡,也能让父域直接使用。
//TODO 1、默认发的令牌。session=xxxxxxx。作用域:当前域;(解决子域session共享问题)
//TODO 2、使用JSON的序列化方式来序列化对象数据到redis中
session.setAttribute("loginUser",data);
// new Cookie("JSESSIONID","dadaa").setDomain("");
// servletResponse.addCookie();
//2、登录成功就跳回首页
return "redirect:http://gulimall.com";
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
}
}
MemberServiceImpl
@Override
public MemberEntity login(SocialUser socialUser) throws Exception {
//登录和注册合并逻辑
String uid = socialUser.getUid();
//1、判断当前社交用户是否已经登录过系统;
MemberDao memberDao = this.baseMapper;
MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity != null) {
//这个用户已经注册
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
memberDao.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else {
//2、没有查询到当前社交用户对应的记录我们就需要注册一个
MemberEntity regiset = new MemberEntity();
try {
//3、查询当前社交用户的社交账户信息(昵称,性别等)
Map<String, String> query = new HashMap<>();
//gitee
// query.put("access_token", socialUser.getAccess_token());
//weibo
query.put("access_token", socialUser.getAccess_token());
query.put("uid", socialUser.getUid());
// HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<>(), query);
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), query);
if (response.getStatusLine().getStatusCode() == 200) {
//查询成功
String json = EntityUtils.toString(response.getEntity());
// 这个JSON对象什么样的数据都可以直接获取
JSONObject jsonObject = JSON.parseObject(json);
//昵称
//gitee
// String name = jsonObject.getString("name");
// regiset.setNickname(name);
//weibo
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
regiset.setGender("m".equals(gender)?1:0);
regiset.setNickname(name);
}
} catch (Exception e) {
}
regiset.setSocialUid(socialUser.getUid());
regiset.setAccessToken(socialUser.getAccess_token());
regiset.setExpiresIn(socialUser.getExpires_in());
memberDao.insert(regiset);
return regiset;
}
}
ps:总结:使用springsession解决分布式session不共享问题
①所有登录后的状态信息都存进了session中,每个服务又都整合了springsession,将session存入到了redis中;
②session存数据的时候,会给浏览器发卡(JsessionId)-> 标识了我们存session的id是什么,并且把卡的作用域放大了:比如某一个服务发的卡,让其保存进session中后,可以全服务通用(跨父子域,父域下的所有服务都可以使用Jsessionid)。
ps:分布式登录总结
登录url:http://auth.gulimall.com/login.html
(注意是url,不是页面。)
判断session中是否有user对象
没有user对象,渲染login.html页面
用户输入账号密码后发送给 url:auth.gulimall.com/login
根据表单传过来的VO对象,远程调用memberFeignService验证密码
- 如果验证失败,取出远程调用返回的错误信息,放到新的请求域,重定向到登录url
- 如果验证成功,远程服务就返回了对应的MemberRespVo对象,
然后放到分布式redis-session中,key为"loginUser",重定向到首页gulimall.com,
同时也会带着的GULISESSIONID
- 重定向到非auth项目后,先经过拦截器看session里有没有loginUser对象
- 有,放到静态threadLocal中,这样就可以操作本地内存,无需远程调用sessio
- 没有,重定向到登录页
有user对象,代表登录过了,重定向到首页,session数据还依靠sessionID持有着
额外说明:
问题1:我们有sessionId不就可以了吗?为什么还要在session中放到User对象?
为了其他服务可以根据这个user查数据库,只有session的话不能再次找到登录session的用户问题2:threadlocal的作用?
他是为了放到当前session的线程里,threadlocal就是这个作用,随着线程创建和消亡。把threadlocal定义为static的,这样当前会话的线程中任何代码地方都可以获取到。如果只是在session中的话,一是每次还得去redis查询,二是去调用service还得传入session参数,多麻烦啊
问题3:cookie怎么回事?不是在config中定义了cookie的key和序列化器?
序列化器没什么好讲的,就是为了易读和来回转换。而cookie的key其实是无所谓的,只要两个项目里的key相同,然后访问同一个域名都带着该cookie即可。
2.5.14 单点登录
![1670467094907](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670467094907.png)
spring session已经解决不了不同域名的问题了。无法扩大域名
sso思路:
记住一个核心思想:建议一个公共的登陆点server,他登录了代表这个集团的产品就登录过了
比如,我登录过尚硅谷电商系统了,我希望登录其他谷粒系统就不用登录了,也即是:只要注册了登录某一个服务就可以自动登录其它所有服务,例如:注册登录了谷粒商城,则可以自动登录在线教育、众筹系统等
这里可以参考 码云上的 xxl的单点登录的开源框架。
1、开源项目:https://gitee.com/xuxueli0323/xxl-sso/repository/archive/master.zip
修改配置:
![1670469382522](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670469382522.png)
①配置本机域名
![1670468589876](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670468589876.png)
②修改服务器redis地址:
![1670468653028](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670468653028.png)
③修改客户端配置:
![1670468699845](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670468699845.png)
![1670468722742](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670468722742.png)
④打包,在根目录下。
![1670468802600](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670468802600.png)
![1670468780725](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670468780725.png)
mvn clean package -Dmaven.skip.test=true
⑤启动服务端和客户端。
![1670469119661](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670469119661.png)
![1670469147507](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670469147507.png)
![1670469169582](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670469169582.png)
当访问客户端1,会自动重定向到服务器认证中心,认证中心登录后,客户端2刷新之后就登录上了。
认证中心访问路径:http://ssoserver.com:8080/xxl-sso-server
客户端1访问路径:http://client1.com:8081/xxl-sso-web-sample-springboot
客户端2访问路径:http://client2.com:8082/xxl-sso-web-sample-springboot
![1670469290591](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670469290591.png)
![1670469278378](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670469278378.png)
![1670469305220](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670469305220.png)
2、下面进行实际编写代码演示:
配置域名:
![1670420612192](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670420612192.png)
idea中创建两个模块,一个客户端,一个服务器端。
![1670418291615](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670418291615.png)
![1670418335346](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670418335346.png)
客户端和服务器端的操作一样,最终效果:
![1670418526409](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670418526409.png)
①单点登录流程1:
![1670420443097](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670420443097.png)
客户端:
HelloController
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
/**
* 无需登录即可访问
* @return
*/
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/employees")
public String employees(Model model, HttpSession session){
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null){
//没登录,跳转到登录服务器进行登录
//跳转过去以后,使用url上的查询参数标识我们自己是那个页面
//redirect:http://client1.com:8081/employees
return "redirect:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
}else{
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps",emps);
return "list";
}
}
}
list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工列表</title>
</head>
<body>
<h1>欢迎:[]</h1>
<ul>
<li th:each="emp:${emps}">姓名:[[${emp}]]</li>
</ul>
</body>
</html>
application.properties
server.port=8081
sso.server.url=http://sso.com:8080/login.html
服务器端
LoginController
@Controller
public class LoginController {
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url")String url){
return "login";
}
@PostMapping("/doLogin")
public String doLogin(){
//登录成功跳转,跳回到之前的页面
return "";
}
}
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input name="username"/><br/>
密码:<input name="password" type="password"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
application.properties
server.port=8080
演示:访问客户端直接重定向到 服务端。
![1670422503471](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670422503471.png)
②单点登录流程2:
![1670423992820](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670423992820.png)
![1670423982617](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670423982617.png)
服务器端:
加入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置:
spring.redis.host=192.168.56.10
login页面带一个隐藏输入框:用于存储调回的url
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input name="username"/><br/>
密码:<input name="password" type="password"/><br/>
<input type="hidden" name="url" th:value="${url}"/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
LoginController
登录成功保存用户信息并传递token
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url")String url, Model model){
model.addAttribute("url",url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,@RequestParam("password") String password,
@RequestParam("url") String url){
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
//登录成功,跳回之前页面
//把登录成功的用户保存起来
//登录成功保存用户信息并传递token
String uuid = UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set(uuid,username);
return "redirect:"+url+"?token="+uuid;
}
//登录失败,展示登录页
return "login";
}
客户端:
拿到令牌需要去认证中心查询用户的信息,这里只是简单保存了以下并没有模拟
/**
* 能够感知这次是在 ssoserver登录成功跳回来的。
* @param model
* @param session
* @param token 只要去ssoserver登录成功跳回来就会带上
* @return
*/
@GetMapping("/employees")
public String employees(Model model, HttpSession session,
@RequestParam(value = "token",required = false) String token){
if (!StringUtils.isEmpty(token)){
//去ssoserver登录成功跳回来就会带上
//TODO 1、去ssoserver获取当前token真正对应的用户信息
session.setAttribute("loginUser","zhangsan");
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null){
//没登录,跳转到登录服务器进行登录
//跳转过去以后,使用url上的查询参数标识我们自己是那个页面
//redirect:http://client1.com:8081/employees
return "redirect:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
}else{
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps",emps);
return "list";
}
}
测试:客户端1加上了token
![1670424885711](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670424885711.png)
③单点登录流程3:
这里流程没有截取完全。
![1670426038780](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670426038780.png)
复制客户端,改为客户端2
![1670470344702](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670470344702.png)
![1670470359101](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670470359101.png)
![1670470380453](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670470380453.png)
添加进项目
![1670425403338](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670425403338.png)
服务器端:
实现一次登录,处处登录的核心就是认证通过之后给浏览器留下一个痕迹,凡是访ssoserver.com这个域名的都会带上这个痕迹,通过使用cookie实现
详解:
- 子系统都先去 login.html这个请求,
- 这个请求会告诉登录过的系统的令牌,
- 如果没登录过就带着url重新去server端,server给一个登录页,如下
当点击登录之后,server端返回一个cookie,子系统重新返回去重新请去业务。于是又来server端验证,这回server端有cookie了,该cookie里有用户在redis中的key,重定向时把key带到url后面,子系统就知道怎么找用户信息了
![1670470464093](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670470464093.png)
![1670470571351](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670470571351.png)
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate redisTemplate;
//登录之后保存用户token
@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token){
String s = redisTemplate.opsForValue().get(token);
return s;
}
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url")String url, Model model,
@CookieValue(value = "sso_token",required = false) String sso_token){
if (!StringUtils.isEmpty(sso_token)){
//说明之前有人登录过,浏览器留下了痕迹
return "redirect:"+url+"?token="+sso_token;
}
model.addAttribute("url",url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("url") String url,
HttpServletResponse response){
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
//登录成功,跳回之前页面
//把登录成功的用户保存起来
// 登录成功保存用户信息并传递token
String uuid = UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set(uuid,username);
Cookie sso_token = new Cookie("sso_token", uuid);
response.addCookie(sso_token);
return "redirect:"+url+"?token="+uuid;
}
//登录失败,展示登录页
return "login";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input name="username"/><br/>
密码:<input name="password" type="password"/><br/>
<input type="hidden" name="url" th:value="${url}"/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
客户端:
sso解决
client1.com 8081 和 client2.com 8082 都跳转到ssoserver 8080
- 给登录服务器留下痕迹
- 登录服务器要将token信息重定向的时候,带到url地址上
- 其他系统要处理url地址上的token,只要有,将token对应的用户保存到自己的session
- 自己系统将用户保存在自己的session中
![1670470686958](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670470686958.png)
![1670470775092](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670470775092.png)
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
/**
* 无需登录即可访问
* @return
*/
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
/**
* 能够感知这次是在 ssoserver登录成功跳回来的。
* @param model
* @param session
* @param token 只要去ssoserver登录成功跳回来就会带上
* @return
*/
@GetMapping("/employees")
public String employees(Model model, HttpSession session,
@RequestParam(value = "token",required = false) String token){
if (!StringUtils.isEmpty(token)){
//去ssoserver登录成功跳回来就会带上
//TODO 1、去ssoserver获取当前token真正对应的用户信息
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://sso.com:8080/userInfo?token=" + token, String.class);
String body = forEntity.getBody();
session.setAttribute("loginUser",body);
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null){
//没登录,跳转到登录服务器进行登录
//跳转过去以后,使用url上的查询参数标识我们自己是那个页面
//redirect:http://client1.com:8081/employees
return "redirect:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
}else{
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps",emps);
return "list";
}
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工列表</title>
</head>
<body>
<h1>欢迎:[[${session.loginUser}]]</h1>
<ul>
<li th:each="emp:${emps}">姓名:[[${emp}]]</li>
</ul>
</body>
</html>
测试:
当客户端1在服务器登录之后,服务器就设置一个sso_token
![1670428672519](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670428672519.png)
客户端1正常显示之前访问的页面
![1670428723697](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670428723697.png)
客户端2无需登录,即可直接登录页面。
![1670428741663](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670428741663.png)
最终流程总结:![1671090832890](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671090832890.png)
- 发送8081/employees请求,判断没登录就跳转到server.com:8080/login.html登录页,并带上现url
- server登录页的时候,有之前带过来的url信息,发送登录请求的时候也把url继续带着
- doLogin登录成功后返回一个token(保存到server域名下)然后重定向
- 登录完后重定向到带的url参数的地址。
- 跳转回业务层的时候,业务层要能感知是登录过的,调回去的时候带个uuid,用uuid去redis里(课上说的是去server里再访问一遍,为了安全性?)看user信息,保存到它系统里自己的session
- 以后无论哪个系统访问,如果session里没有指定的内容的话,就去server登录,登录过的话已经有了server的cookie,所以不用再登录了。回来的时候就告诉了子系统应该去redis里怎么查你的用户内容
ps: 可以借鉴:单点登录(SSO)看这一篇就够了
![img](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\12540413-041b3228c5e865e8.webp)
上图是CAS官网上的标准流程,具体流程如下:有两个子系统app1、app2
用户访问app1系统,app1系统是需要登录的,但用户现在没有登录。
跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app1系统,同时将ST作为参数传递给app1系统。
app1系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。
用户访问app2系统,app2系统没有登录,跳转到SSO。
由于SSO已经登录了,不需要重新登录认证。
SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
app2拿到ST,后台访问SSO,验证ST是否有效。
验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的。
SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。如果想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?
其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。
2.6 购物车服务
2.6.1 环境搭建
①域名配置
![1670484012821](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670484012821.png)
②创建 微服务
![1670483850563](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670483850563.png)
暂时需要的插件
![1670483921031](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670483921031.png)
- 此外,导入 公共包的依赖
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- application.properties配置
server.port=40000
spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
- 主启动类加上nacos服务发现注册注解,以及后面需要用到的远程服务注解,并且因为导入了common包,需要暂时排除数据库自动配置
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
③动静分离
- 静态资源放到 Nginx中
![1670484089189](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670484089189.png)
- 动态页面放到 templates下。
![1670486214726](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670486214726.png)
- 页面前缀替换:
![1670484259663](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670484259663.png)
![1670484292158](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670484292158.png)
![1670484319702](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670484319702.png)
- 网关配置
- id: gulimall_cart_route
uri: lb://gulimall-cart
predicates:
- Host=cart.gulimall.com
⑦测试,为了方便,将success页面重命名为index页面
出现问题,替换:将 th:替换成空字符串
![1670486459504](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670486459504.png)
![1670486537234](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670486537234.png)
前端页面跳转
点击 图标等能返回首页。
![1670486575527](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670486575527.png)
![1670486595060](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670486595060.png)
![1670486616360](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670486616360.png)
2.6.2 数据模型分析
1、购物车需求
1)、需求描述:
- 用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
- 放入数据库
- mongodb
- 放入redis(采用)
登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车;
- 用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
- 放入localstorage(客户端存储,后台不存)
- cookie
- WebSQL
- 放入redis(采用)
浏览器即使关闭,下次进入,临时购物车数据都在
- 用户可以使用购物车一起结算下单
- 给购物车添加商品
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量。
- 用户可以在购物车中删除商品。
- 选中不选中商品
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化
2)、数据结构
![1670492965283](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670492965283.png)
因此每一个购物项信息,都是一个对象,基本字段包括:
{
skuId: 2131241,
check: true,
title: "Apple iphone.....",
defaultImage: "...",
price: 4999,
count: 1,
totalPrice: 4999,
skuSaleVO: {...}
}
另外,购物车中不止一条数据,因此最终会是对象的数组。即:
[
{...},{...},{...}
]
Redis 有5 种不同数据结构,这里选择哪一种比较合适呢?Map<String, List
- 首先不同用户应该有独立的购物车,因此购物车应该以用户的作为key 来存储,Value 是
用户的所有购物车信息。这样看来基本的k-v
结构就可以了。 - 但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品id 进行判断,
为了方便后期处理,我们的购物车也应该是k-v
结构,key 是商品id,value 才是这个商品的
购物车信息。
综上所述,我们的购物车结构是一个双层Map:Map<String,Map<String,String>>
- 第一层Map,Key 是用户id
- 第二层Map,Key 是购物车中商品id,值是购物项数据
将购物车中的购物项存为list类型的话,修改起来太麻烦要从头到尾遍历。可以使用hash来存储购物车中的购物项
![1670493136121](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670493136121.png)
2.6.3 vo编写
购物项的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;//商品数量
private BigDecimal totalPrice;//总价,总价需要计算
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
public Boolean getCheck() {
return check;
}
public void setCheck(Boolean check) {
this.check = check;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public List<String> getSkuAttr() {
return skuAttr;
}
public void setSkuAttr(List<String> skuAttr) {
this.skuAttr = skuAttr;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
/**
* 计算当前项的总价
* @return
*/
public BigDecimal getTotalPrice() {
return this.price.multiply(new BigDecimal("" + this.count));
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
}
编写购车Vo
/**整个购物车
* 需要计算的属性,必须重写它的get方法,保证每次获取属性都会进行计算
*/
public class Cart {
List<CartItem> items;
private Integer countNum;//商品数量
private Integer countType;//商品类型数量
private BigDecimal totalAmount;//商品总价
private BigDecimal reduce = new BigDecimal("0.00");//减免价格
public List<CartItem> getItems() {
return items;
}
public void setItems(List<CartItem> items) {
this.items = items;
}
public Integer getCountNum() {
int count = 0;
if (items !=null && items.size()>0){
for (CartItem item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
int count = 0;
if (items !=null && items.size()>0){
for (CartItem item : items) {
count += 1;
}
}
return count;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
//1、计算购物项总价
if (items !=null && items.size()>0){
for (CartItem item : items) {
BigDecimal totalPrice = item.getTotalPrice();
amount = amount.add(totalPrice);
}
}
//2、减去优惠总价
BigDecimal subtract = amount.subtract(getReduce());
return subtract;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
2.6.4 ThreadLocal用户身份鉴别
1.将购物车数据存储至Redis中,因此,需要导入Spring整合Redis的依赖以及Redis的配置。项目上线之后,应该有一个专门的Redis负责存储购物车的数据不应该使用缓存的Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis地址
spring.redis.host=192.168.56.10
2.编写服务层
@Slf4j
@Service
public class CartServiceImpl implements CartService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
}
3.判断用户是否登录则通过判断Session中是否有用户的数据,因此,导入SpringSession的依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
配置Session
/**自定义SpringSession完成子域session共享
* @author wystart
* @create 2022-12-06 21:48
*/
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {
//子域共享问题解决
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");// 扩大session作用域,也就是cookie的有效域
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
// 默认使用jdk进行序列化机制,这里我们使用json序列化方式来序列化对象数据到redis中
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
4. cookie中的user-key说明
第一次访问京东,会给你的cookie中设置user-key标识你的身份,有效期为一个月,浏览器会保存你的user-key,以后访问都会带上
*浏览器有一个cookie;user-key;标识用户身份,一个月后个过期;
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
* 浏览器以后保存,每次访问都会带上这个cookie;
*
*
* 登录:session有
* 没登录:按照cookie里面带来user-key来做。
* 第一次:如果没有临时用户,帮忙创建一个临时用户。
*
5.编写To与常量
购物车服务下
@ToString
@Data
public class UserInfoTo {
private Long userId;
private String userKey;//一定要封装
private boolean tempUser = false;//标识位
}
公共服务下:新建 CartConstant
public class CartConstant {
public static final String TEMP_USER_COOKIE_NAME = "user-key";
//过期时间为1一个月
public static final int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;//临时用户的过期时间
}
6.编写拦截器
拦截器逻辑:业务执行之前,判断是否登录,若登录则封装用户信息,将标识位设置为true,postHandler就不再设置作用域和有效时间,否则为其创建一个user-key
注意细节:整合SpringSession之后,Session获取数据都是从Redis中获取的
使用ThreadLocal,解决线程共享数据问题,方便同一线程共享UserInfoTo
![1670509051220](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670509051220.png)
![1670509028337](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670509028337.png)
编写拦截器实现类
/**在执行目标方法之前,判断用户的登录状态,并封装传递给controller目标请求
* @author wystart
* @create 2022-12-08 20:58
*/
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
UserInfoTo userInfoTo = new UserInfoTo();
HttpSession session = request.getSession();
MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (member != null){
//用户登录
userInfoTo.setUserId(member.getId());
}
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0){
for (Cookie cookie : cookies) {
//user-key
String name = cookie.getName();
if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
}
}
}
//如果没有临时用户一定分配一个临时用户
if (StringUtils.isEmpty(userInfoTo.getUserKey())){
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
//目标方法执行之前
threadLocal.set(userInfoTo);
return true;
}
/**
* 业务执行之后;分配临时用户,让浏览器保存
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
//如果没有临时用户一定保存一个临时用户
if (!userInfoTo.isTempUser()){
//持续的延长临时用户的过期时间
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
}
配置拦截器,否则拦截器不生效
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置CartInterceptor拦截器拦截所有请求
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}
@Controller
public class CartController {
/**
*浏览器有一个cookie;user-key;标识用户身份,一个月后个过期;
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
* 浏览器以后保存,每次访问都会带上这个cookie;
*
*
* 登录:session有
* 没登录:按照cookie里面带来user-key来做。
* 第一次:如果没有临时用户,帮忙创建一个临时用户。
*
* @return
*/
@GetMapping("/cart.html")
public String cartListPage(){
//1、快速得到用户信息,id,user-key
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
System.out.println(userInfoTo);
return "cartList";
}
}
Debug测试UserInfoTo中是否有数据
![1670509402698](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670509402698.png)
user-key也有
![1670509457763](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670509457763.png)
2.6.5 页面环境搭建
- 首页点击购物车去购物车页面
![1670511281802](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511281802.png)
index.html
![1670511304093](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511304093.png)
- 检索页面点击 我的购物车去购物车页面
![1670511420137](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511420137.png)
list.html
![1670511448888](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511448888.png)
- 商品详情页修改
![1670511502767](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511502767.png)
item.html
![1670511522997](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511522997.png)
![1670511557904](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511557904.png)
CartController
/**
* 添加商品到购物车
* @return
*/
@GetMapping("/addToCart")
public String addToCart(){
return "success";
}
- 加入商品成功后,跳转到购物车列表页面
success.html
这里“查看商品详情暂时写死了”!!
![1670511671840](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511671840.png)
![1670511683481](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511683481.png)
- 购物车详情页面
cartList.html
![1670511805204](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511805204.png)
![1670511831933](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511831933.png)
![1670511872855](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670511872855.png)
2.6.6 添加购物车
编写添加商品进入购物车的请求方法,需要知道商品的SkuId和数量
![1670512328186](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670512328186.png)
为加入购物车绑定单击事件,url改为#避免跳转并且设置id;为超链接自定义属性,用于存储skuId
![1670513076392](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670513076392.png)
<a href="#" id="addToCartA" th:attr="skuId=${item.info.skuId}">
加入购物车
</a>
为文本框设置id
![1670513111712](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670513111712.png)
编写单击事件 ,$(this)指当前实例,return false : 禁止默认行为
$("#addToCartA").click(function (){
var num = $("#numInput").val();//获取数量
var skuId = $(this).attr("skuId");//获取商品的skuId
location.href = "http://cart.gulimall.com/addToCart?skuId="+skuId+"&num="+num;//拼接路径
return false;//取消默认行为
})
修改加入购物车的成功页面的显示
①默认图片的显示、商品详情页跳转以及标题显示、商品数量显示
![1670575289110](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670575289110.png)
购物车前缀
![1670553041321](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670553041321.png)
boundHashOps()方法:所有的增删改查操作只针对这个key
将其抽取成方法
选中->右击->Refactor->Extract Method
/**
* 获取到我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey = "";
if (userInfoTo.getUserId() != null){
//gulimall:cart:1 ----登录用户
cartKey = CART_PREFIX + userInfoTo.getUserId();
}else{
//gulimall:cart:xxxxx ----临时用户
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
//redisTemplate.boundHashOps(cartKey):以后关于这个cartKey就都绑定到redis中操作了
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
远程调用product查询sku详情
![1670553144088](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670553144088.png)
记得主启动类要加上@EnableFeignClients:开启远程调用注解
远程调用product服务查询销售属性
①编写product服务中的查询销售属性AsString的接口
SkuSaleAttrValueController
@GetMapping("/stringlist/{skuId}")
public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId){
return skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
}
SkuSaleAttrValueServiceImpl
@Override
public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {
SkuSaleAttrValueDao dao = this.baseMapper;
return dao.getSkuSaleAttrValuesAsStringList(skuId);
}
数据库查询SQL语句
select concat(attr_name,":",attr_value)
from `pms_sku_sale_attr_value`
where sku_id = 21
![1670516761878](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670516761878.png)
SkuSaleAttrValueDao.xml
<select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
select concat(attr_name,":",attr_value)
from `pms_sku_sale_attr_value`
where sku_id = #{skuId}
</select>
购物车服务远程调用接口
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
//@RequiresPermissions("product:skuinfo:info")
R getSkuInfo(@PathVariable("skuId") Long skuId);
@GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
}
配置线程池提高查询效率:因为多次调用远程服务
MyThreadConfig(直接复制之前商品服务的)
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(pool.getCoreSize(),
pool.getMaxSize(),pool.getKeepAliveTime(),
TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
ThreadPoolConfigProperties
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
application.properties配置
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
使用异步编排
①编写vo,属性从SkuInfoEntity中copy
@Data
public class SkuInfoVo {
private Long skuId;
/**
* spuId
*/
private Long spuId;
/**
* sku名称
*/
private String skuName;
/**
* sku介绍描述
*/
private String skuDesc;
/**
* 所属分类id
*/
private Long catalogId;
/**
* 品牌id
*/
private Long brandId;
/**
* 默认图片
*/
private String skuDefaultImg;
/**
* 标题
*/
private String skuTitle;
/**
* 副标题
*/
private String skuSubtitle;
/**
* 价格
*/
private BigDecimal price;
/**
* 销量
*/
private Long saleCount;
}
②异步编排
@Autowired
ThreadPoolExecutor executor;
@Override
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
CartItem cartItem = new CartItem();
//异步编排
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
//1、远程调用商品服务查询商品详情
R skuInfo = productFeignService.getSkuInfo(skuId);
SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
//2、商品添加 到购物车
cartItem.setCheck(true);
cartItem.setCount(num);
cartItem.setImage(data.getSkuDefaultImg());
cartItem.setTitle(data.getSkuTitle());
cartItem.setSkuId(skuId);
cartItem.setPrice(data.getPrice());
}, executor);
//3、远程查询sku的组合信息
//同时调用多个远程服务,为了不影响最终的查询速度,我们可以使用多线程的方式,使用自定义的线程池提高效率
CompletableFuture<Void> getSkuoSaleAttrValues = CompletableFuture.runAsync(() -> {
List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
cartItem.setSkuAttr(values);
}, executor);
CompletableFuture.allOf(getSkuInfoTask,getSkuoSaleAttrValues).get();
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
return cartItem;
}
测试:
出现问题:
![1670553817566](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670553817566.png)
不能使用th:else,改为 th:if
item.html
![1670553905398](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670553905398.png)
list.html
![1670553937661](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670553937661.png)
未登录状态
![1670518045017](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670518045017.png)
已登录状态
![1670518111464](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670518111464.png)
redis中存储:cart:3代表已登录状态;cart:xxxxx代表临时用户,没有登录状态。
![1670518159029](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670518159029.png)
完善购物车添加细节①:之前是都认为购物车中没有要添加的此商品存在,现在要判断购物车中是否有我们要添加的此商品。也即是:上面的操作是针对添加新商品进购物车,若购物车里已存在此商品则是一个数量的叠加
@Override
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String res = (String) cartOps.get(skuId.toString());
if (StringUtils.isEmpty(res)){//购物车此时没有此商品
CartItem cartItem = new CartItem();
//异步编排
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
//1、远程调用商品服务查询商品详情
R skuInfo = productFeignService.getSkuInfo(skuId);
SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
//2、新商品添加到购物车
cartItem.setCheck(true);
cartItem.setCount(num);
cartItem.setImage(data.getSkuDefaultImg());
cartItem.setTitle(data.getSkuTitle());
cartItem.setSkuId(skuId);
cartItem.setPrice(data.getPrice());
}, executor);
//3、远程查询sku的组合信息
//同时调用多个远程服务,为了不影响最终的查询速度,我们可以使用多线程的方式,使用自定义的线程池提高效率
CompletableFuture<Void> getSkuoSaleAttrValues = CompletableFuture.runAsync(() -> {
List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
cartItem.setSkuAttr(values);
}, executor);
CompletableFuture.allOf(getSkuInfoTask,getSkuoSaleAttrValues).get();
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
return cartItem;
}else{
//购物车有此商品,修改数量
CartItem cartItem = JSON.parseObject(res, CartItem.class);
cartItem.setCount(cartItem.getCount() + num);
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
return cartItem;
}
}
测试:
![1670555297379](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670555297379.png)
![1670555317826](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670555317826.png)
![1670555335245](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670555335245.png)
完善购物车添加细节②:为了避免用户一直刷新页面,重复提交数据,通过重定向的方式获取购物车内容
RedirectAttributes的addFlashAttribut()方法:将对象存储在Session中且只能使用一次,再次刷新就没有了
RedirectAttributes的addAttribut()方法:将对象拼接在url中
CartController
/**
* 添加商品到购物车
*
* RedirectAttributes ra
* ra.addFlashAttribute():将数据放在session里面可以在页面取出,但是只能取一次
* ra.addAttribute("skuId", skuId);将数据放在url后面
* @return
*/
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes ra) throws ExecutionException, InterruptedException {
cartService.addToCart(skuId, num);
// model.addAttribute("item",cartItem);
ra.addAttribute("skuId", skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
/**
* 跳转到成功页
* @param skuId
* @param model
* @return
*/
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccess(@RequestParam("skuId") Long skuId, Model model) {
//重定向到成功页面,再次查询购物车数据即可
CartItem item = cartService.getCartItem(skuId);
model.addAttribute("item",item);
return "success";
}
CartServiceImpl
/**
* 获取购物车中的商品
* @param skuId
* @return
*/
@Override
public CartItem getCartItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String item = (String) cartOps.get(skuId.toString());
CartItem cartItem = JSON.parseObject(item, CartItem.class);
return cartItem;
}
前端页面修改,进行简单的逻辑判断
success.html
![1670557476005](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670557476005.png)
测试:查询没有的商品
![1670557233572](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670557233572.png)
不停刷新页面数量不增加
![1670557553164](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670557553164.png)
2.6.7 获取&合并购物车
购物车列表展示逻辑:首先判断是否登录,没有登录则展示临时购物车,若登录则展示合并后的购物车,将临时购物车合并后并清空
1.编写获取购物车的方法
CartServiceImpl
/**
* 获取购物车
* @param cartKey
* @return
*/
private List<CartItem> getCartItems(String cartKey){
BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(cartKey);
//获取所有商品数据
List<Object> values = hashOps.values();
if (values != null && values.size() > 0){
List<CartItem> collect = values.stream().map((obj) -> {
String str = (String) obj;
CartItem cartItem = JSON.parseObject(str, CartItem.class);
return cartItem;
}).collect(Collectors.toList());
return collect;
}
return null;
}
2. 编写删除购物车的方法
CartService
/**
* 清空购物车数据
* @param cartKey
*/
void clearCart(String cartKey);
CartServiceImpl
@Override
public void clearCart(String cartKey) {
redisTemplate.delete(cartKey);
}
3. 合并购物车,合并完之后要删除临时购物车
/**
* 购物车展示
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo.getUserKey() != null){
//1、登录
String cartKey = CART_PREFIX + userInfoTo.getUserId();
//2、如果临时购物车的数据还没有,进行合并【合并购物车】
String temCartKey = CART_PREFIX + userInfoTo.getUserKey();
List<CartItem> temCartItems = getCartItems(temCartKey);
if (temCartItems != null){
//临时购物车有数据,需要合并
for (CartItem item : temCartItems) {
addToCart(item.getSkuId(), item.getCount());
}
//清除临时购物车的数据
clearCart(temCartKey);
}
//3、获取登录后的购物车的数据【包含合并过来的临时购物车的数据,和登录后的购物车的数据】
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}else {
//4、没登录
String cartKey = CART_PREFIX + userInfoTo.getUserKey();
//获取临时购物车的所有购物项
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}
return cart;
}
前端页面编写
1.登录回显
![1670575947513](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670575947513.png)
<li>
<a th:if="${session.loginUser == null }" href="http://auth.gulimall.com/login.html"
class="li_2">你好,请登录</a>
<a th:if="${session.loginUser != null }" style="width: 100px">[[${session.loginUser.nickname}]]</a>
</li>
<li>
<a th:if="${session.loginUser == null }" href="http://auth.gulimall.com/reg.html">免费注册</a>
</li>
2. 逻辑判断
![1670576081599](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576081599.png)
3. 购物车中商品遍历
![1670576118553](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576118553.png)
4.是否被选中、图片展示、标题展示、销售属性展示、格式化商品单价展示、商品数量展示、商品总价显示
![1670576378293](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576378293.png)
<div class="One_ShopCon">
<h1 th:if="${cart.items == null}">
购物车还没有商品,<a href="http://gulimall.com">去购物</a>
</h1>
<ul th:if="${cart.items != null}">
<li th:each="item : ${cart.items}">
<div>
</div>
<div>
<ol>
<li><input type="checkbox" class="check" th:checked="${item.check}"></li>
<li>
<dt><img th:src="${item.image}" alt=""></dt>
<dd style="width: 300px">
<p>
<span th:text="${item.title}">TCL 55A950C 55英寸32核</span>
<br/>
<span th:each="attr:${item.skuAttr}" th:text="${attr}">尺码: 55时 超薄曲面 人工智能</span>
</p>
</dd>
</li>
<li>
<p class="dj" th:text="'¥'+${#numbers.formatDecimal(item.price,3,2)}">¥4599.00</p>
</li>
<li>
<p>
<span>-</span>
<span th:text="${item.count}">5</span>
<span>+</span>
</p>
</li>
<li style="font-weight:bold"><p class="zj">¥[[${#numbers.formatDecimal(item.totalPrice,3,2)}]]</p></li>
<li>
<p>删除</p>
</li>
</ol>
</div>
</li>
</ul>
</div>
5.购物车总价显示、优惠价格显示
![1670576540245](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576540245.png)
<li>总价:<span style="color:#e64346;font-weight:bold;font-size:16px;" class="fnt">¥[[${#numbers.formatDecimal(cart.totalAmount,3,2)}]]</span>
</li>
<li>优惠:[[${#numbers.formatDecimal(cart.reduce,1,2)}]]</li>
6.登录之后不显示“你还没有登录。。。。。” ,没有登录则显示,并且可以跳转去登录页面。
![1670576635714](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576635714.png)
![1670576601802](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576601802.png)
效果:
![1670576734473](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576734473.png)
临时购物车删除,并合并到登录用户购物车中。
![1670576758376](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670576758376.png)
2.6.8 选中购物项
为input框设置class方便后续绑定单击事件修改选中状态,自定义属性保存skuId
![1670579196358](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670579196358.png)
单击事件编写 ,prop会返回true或false
$(".itemCheck").click(function (){
var skuId = $(this).attr("skuId");
var check = $(this).prop("checked");//使用prop获取到的是 true、false;使用 attr获取到的就是checked:这里需要使用prop
location.href="http://cart.gulimall.com/checkItem?skuId="+skuId+"&check="+(check?1:0);// check = 1:true;check = 0:false
})
编写Controller处理请求
CartController
/**
* 勾选购物项
* @param skuId
* @param check
* @return
*/
@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId,
@RequestParam("check") Integer check){
cartService.checkItem(skuId,check);
return "redirect:http://cart.gulimall.com/cart.html";
}
CartServiceImpl
@Override
public void checkItem(Long skuId, Integer check) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
//获取单个商品
CartItem cartItem = getCartItem(skuId);
cartItem.setCheck((check==1?true:false));
String s = JSON.toJSONString(cartItem);//序列化存进Redis中
cartOps.put(skuId.toString(),s);
}
测试:不勾选,变false
![1670579371751](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670579371751.png)
![1670579066272](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670579066272.png)
勾选,变 true
![1670579392219](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670579392219.png)
![1670579099048](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670579099048.png)
2.6.9 改变购物项数量
为父标签自定义属性存储skuId,为加减操作设置相同的class,为数量设置class
![1670585516525](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670585516525.png)
<p th:attr="skuId=${item.skuId}">
<span class="countOpsBtn">-</span>
<span class="countOpsNum" th:text="${item.count}">5</span>
<span class="countOpsBtn">+</span>
</p>
编写加减的单击事件
$(".countOpsBtn").click(function () {
//1、skuId
var skuId = $(this).parent().attr("skuId");
var num = $(this).parent().find(".countOpsNum").text();
location.href="http://cart.gulimall.com/countItem?skuId="+skuId+"&num="+num;
})
编写Controller
/**
* 修改购物项数量
* @param skuId
* @param num
* @return
*/
@GetMapping("/countItem")
public String countItem(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num){
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.gulimall.com/cart.html";
}
CartServiceImpl
@Override
public void changeItemCount(Long skuId, Integer num) {
CartItem cartItem = getCartItem(skuId);
cartItem.setCount(num);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();//根据当前登录状态获取购物车
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));//保存进redis中(需要序列化保存):skuId转为string作为key,商品序列化后的文本作为值
}
2.6.10 删除购物项
为图中的删除按钮设置class,绑定单击事件临时保存skuId
![1670594025517](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670594025517.png)
![1670593377438](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670593377438.png)
![1670594084668](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670594084668.png)
![1670593430418](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670593430418.png)
//全局变量
var deleteId = 0;
//删除购物项
function deleteItem(){
location.href = "http://cart.gulimall.com/deleteItem?skuId="+deleteId;
}
$(".deleteItemBtn").click(function () {
deleteId = $(this).attr("skuId");
})
编写Controller
CartController
/**
* 删除购物项
* @param skuId
* @return
*/
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId){
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";//删除完了之后重新跳转到此页面,相当于刷新获得最新的内容
}
CartServiceImpl
@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();//根据当前状态获取购物车
cartOps.delete(skuId.toString());//利用skuId进行删除
}
测试:删除所有商品
![1670593882029](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670593882029.png)
重要!!!!
关于 第6章 分布式事务 和 第7章RabbitMQ 单独写在另外一篇文档:谷粒商城之高级篇知识补充。
2.7 订单服务
2.7.1 环境搭建
1、实现动静分离:
课件中等待付款是订单详情页;订单页是用户订单列表;结算页是订单确认页;收银页是支付页
在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
![1670837938964](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670837938964.png)
![1670851419138](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670851419138.png)
-
修改HOSTS, 192.168.56.10 order.gulimall.com
![1670837983546](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670837983546.png)
-
nginx中已经配置过转发
-
在gateway中新增order路由
- id: gulimall_order_route
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.com
- 修改html中的路径/static前缀。比如/static/order/confirm
![1670838525208](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670838525208.png)
![1670838739265](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670838739265.png)
此外每个页面都需要加入 thymeleaf名称空间。
其他三个页面都需要进行上述操作。
pay.html页面还需要:
![1670838757641](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670838757641.png)
2、相应配置
①将订单服务注册到注册中心去,并且给微服务起一个名字
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-order
②导入thymeleaf依赖并在开发期间禁用缓存
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring.thymeleaf.cache=false
③主启动类加上注解 @EnableDiscoveryClient
3、编写Controller访问订单页面
@Controller
public class HelloController {
@GetMapping("/{page}.html")
public String listPage(@PathVariable("page") String page){
return page;
}
}
测试访问各个页面:
出现错误:confirm.html 报 Unfinished block structure
![1670852085899](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670852085899.png)
解决方案: 将/*删除即可
![1670852113757](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670852113757.png)
最后能够成功访问各个页面。
2.7.2 整合SpringSession
1.Redis默认使用lettuce作为客户端可能导致内存泄漏,因此需要排除lettuce依赖,使用jedis作为客户端或者使用高版本的Redis依赖即可解决内存泄漏问题
引入依赖
<!--导入SpringBoot整合Redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--导入Jedis的依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置 Redis
spring.redis.host=192.168.56.10
spring.redis.port=6379
2. 导入Session依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
配置 Session的存储类型
spring.session.store-type=redis
编写Session配置类
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
编写线程池配置
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(pool.getCoreSize(),
pool.getMaxSize(),pool.getKeepAliveTime(),
TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
配置
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
主启动类使用@EnableRedisHttpSession让Session启作用
![1670854273773](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670854273773.png)
3. 登录回显
首页:我的订单路径跳转
![1670854315675](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670854315675.png)
首页:
![1670854392076](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670854392076.png)
订单详情页面(detail.html):显示用户名
![1670853567809](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670853567809.png)
![1670853590020](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670853590020.png)
订单确认页(confirm.html):用户名显示
![1670853688707](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670853688707.png)
![1670853673513](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670853673513.png)
订单支付页面(pay.html):用户名回显
![1670853816618](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670853816618.png)
![1670853800860](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670853800860.png)
2.7.3 订单基本概念
1、订单中心:
电商系统涉及到3流,分别是信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。
订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。
- 订单构成
![1670855398196](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670855398196.png)
1、用户信息
用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等。
2、订单基础信息
订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。
(1)订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。
(2)同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
(3)订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
(4)订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
(5)订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等
3、商品信息
商品信息从商品库中获取商品的SKU 信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。
4、优惠信息
优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。
为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。
5、支付信息
(1)支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
(2)支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。
(3)商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等之和;实付金额,用户实际需要付款的金额。
用户实付金额=商品总金额+运费-优惠总金额
6、物流信息
物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。
- 订单状态
1、待付款
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。
2、已付款/待发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调拨,配货,分拣,出库等操作。
3、待收货/已发货
仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态
4、已完成
用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态
5、已取消
付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
6、售后中
用户在付款后申请退款,或商家发货后用户申请退换货。
售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。
2、订单流程
订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与O2O 订单等,所以需要根据不同的类型进行构建订单流程。
不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。
而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图
![1670855856075](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670855856075.png)
1、订单创建与支付
- (1) 、订单创建前需要预览订单,选择收货信息等
- (2) 、订单创建需要锁定库存,库存有才可创建,否则不能创建
- (3) 、订单创建后超时未支付需要解锁库存
- (4) 、支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
- (5) 、支付的每笔流水都需要记录,以待查账
- (6) 、订单创建,支付成功等状态都需要给MQ 发送消息,方便其他系统感知订阅
2、逆向流程
- (1) 、修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息,优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
- (2) 、订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的限时处理,尤其是在前面说的下单减库存的情形下面,可以保证快速的释放库存。
另外需要需要处理的是促销优惠中使用的优惠券,权益等视平台规则,进行相应补回给用户。 - (3) 、退款,在待发货订单状态下取消订单时,分为缺货退款和用户申请退款。如果是全部退款则订单更新为关闭状态,若只是做部分退款则订单仍需进行进行,同时生成一条退款的售后订单,走退款流程。退款金额需原路返回用户的账户。
- (4) 、发货后的退款,发生在仓储货物配送,在配送过程中商品遗失,用户拒收,用户收货后对商品不满意,这样情况下用户发起退款的售后诉求后,需要商户进行退款的审核,双方达成一致后,系统更新退款状态,对订单进行退款操作,金额原路返回用户的账户,同时关闭原订单数据。仅退款情况下暂不考虑仓库系统变化。如果发生双方协调不一致情况下,可以申请平台客服介入。在退款订单商户不处理的情况下,系统需要做限期判断,比如5 天商户不处理,退款单自动变更同意退款。
3、幂等性处理
参照幂等性文档。
2.7.4 订单登录拦截
点击 去结算 跳转到 订单详情页面:
![1670858023510](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670858023510.png)
购物车页面(cartList.html):
![1670858060259](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670858060259.png)
编写controller接口,实现跳转。
@Controller
public class OrderWebController {
@GetMapping("/toTrade")
public String toTrade(){
return "confirm";
}
}
这里我们需要有判断,如果是未登录用户,需要拦截,让其去登录才可以点击结算:即只要能结算就是登录状态,因此,需要编写一个拦截器。
因为订单系统必然涉及到用户信息,因此进入订单系统的请求必须是已经登录的,所以我们需要通过拦截器对未登录订单请求进行拦截
拦截器
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取登录用户
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
//登录成功后,将用户信息存储至ThreadLocal中方便其他服务获取用户信息:
//加上ThreadLocal共享数据,是为了登录后把用户放到本地内存,而不是每次都去远程session里查
loginUser.set(attribute);
return true;
} else {
//没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
需要配置以下配置类拦截器才会生效:
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置 LoginUserInterceptor拦截器拦截所有请求
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
未登录消息提醒 回显
login.html
![1670858531843](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670858531843.png)
测试没有登录点击 去结算:
![1670858563341](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670858563341.png)
跳转到登录页面登录并提示错误消息。
2.7.5 订单确认页模型抽取
1.购物车计算价格存在小bug,未选中的商品不应该加入总价的计算
![1670859954615](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670859954615.png)
修改 购物车服务下的 Cart
![1670860008790](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670860008790.png)
2. 订单确认页的数据编写
①用户地址信息,数据来源:ums_member_receive_address
![1670860086212](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670860086212.png)
![1670860170831](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670860170831.png)
② 商品项信息,之前编写CartItem
![1670860209365](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670860209365.png)
获取的是最新的价格,而不是加入购物车时的价格。
③ 优惠券信息,使用京豆的形式增加用户的积分
![1670860245709](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670860245709.png)
![1670860277461](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670860277461.png)
④ 订单总额和应付总额信息
![1670860309245](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670860309245.png)
动态计算出来。
3. 编写Vo
OrderConfirmVo
//订单确认页需要用的数据
@Data
public class OrderConfirmVo {
// 收货地址,ums_member_receive_address 表
List<MemberAddressVo> address;
//所有选中的购物项
List<OrderItemVo> items;
//发票记录...
//优惠券信息...
Integer integration;
BigDecimal total;//订单总额
BigDecimal payPrice;//应付价格
}
MemberAddressVo
@Data
public class MemberAddressVo {
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;
}
OrderItemVo
@Data
public class OrderItemVo {
private Long skuId;
private Boolean check ;//是否被选中
private String title;//标题
private String image;//图片
private List<String> skuAttr;//销售属性组合描述
private BigDecimal price;//商品单价
private Integer count;//商品数量
private BigDecimal totalPrice;//总价,总价需要计算
}
4. 编写业务代码
OrderWebController
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("OrderConfirmData",confirmVo);
//展示订单确认的数据
return "confirm";
}
}
OrderService
/**
* 订单确认页返回需要用的数据
* @return
*/
OrderConfirmVo confirmOrder();
2.7.6 订单确认页数据获取
1.创建出返回会员地址列表的方法,方便后续的远程服务调用
会员服务下:MemberReceiveAddressController
/**
* 查询用户地址信息
* @param memberId
* @return
*/
@GetMapping("/{memberId}/addresses")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId){
return memberReceiveAddressService.getAddress(memberId);
}
实现:MemberReceiveAddressServiceImpl
/**
*
* 查出对应会员id的地址
* @param memberId
* @return
*/
@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id",memberId));
}
2.远程调用会员服务查询收货地址
订单服务下新建 MemberFeignService
@FeignClient("gulimall-member")
public interface MemberFeignService {
@GetMapping("/member/memberreceiveaddress/{memberId}/addresses")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
开启远程服务调用功能
![1670901511476](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670901511476.png)
3. 查询购物车时,需要查询实时的商品价格,因此,编写通过skuId查询商品价格的接口
商品服务下:SkuInfoController
/**
* 获取商品的最新价格
*/
@GetMapping("/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId){
SkuInfoEntity byId = skuInfoService.getById(skuId);
return byId.getPrice();
}
4. 远程调用商品服务,查询商品的实时价格
购物车服务下 ProductFeignService
@GetMapping("/product/skuinfo/{skuId}/price")
BigDecimal getPrice(@PathVariable("skuId") Long skuId);
5. 查询购物车接口编写
注意细节:①需要过滤选中的商品②Redis中的购物车商品的价格可能是很久之前的需要实时查询商品的价格
CartController
@GetMapping("/currentUserCartItems")
public List<CartItem> getCurrentUserCartItems(){
return cartService.getCurrentUserCartItems();
}
实现:CartServiceImpl
@Override
public List<CartItem> getCurrentUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo.getUserId() == null){
return null;
}else{
String cartKey = CART_PREFIX + userInfoTo.getUserId();
List<CartItem> cartItems = getCartItems(cartKey);
//获取所有被选中的购物项
List<CartItem> collect = cartItems.stream().filter(item -> item.getCheck()).map(item -> {
BigDecimal price = productFeignService.getPrice(item.getSkuId());
// TODO 1、更新为最新价格
item.setPrice(price);
return item;
}).collect(Collectors.toList());
return collect;
}
}
6. 远程调用购物车服务,查询购物车中的商品列表
CartFeignService
@FeignClient("gulimall-cart")
public interface CartFeignService {
@GetMapping("/currentUserCartItems")
List<OrderItemVo> getCurrentUserCartItems();
}
7. 价格获取方法编写(动态计算),此外为了防止用户重复提交提订单,需要编写一个令牌(Token)
//订单确认页需要用的数据
// @Data
public class OrderConfirmVo {
// 收货地址,ums_member_receive_address 表
@Setter @Getter
List<MemberAddressVo> address;
//所有选中的购物项
@Setter @Getter
List<OrderItemVo> items;
//发票记录...
//优惠券信息...
@Setter @Getter
Integer integration;
//防重令牌(防重复提交令牌):防止因为网络原因,用户多次点击提交订单,造成多次提交
@Setter @Getter
String orderToken;
// BigDecimal total;//订单总额
public BigDecimal getTotal() {
BigDecimal sum = new BigDecimal("0");
if (items != null){
for (OrderItemVo item : items) {
BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
sum = sum.add(multiply);
}
}
return sum;
}
// BigDecimal payPrice;//应付价格
public BigDecimal getPayPrice() {
return getTotal();
}
}
8.订单确认页返回需要用的数据
OrderWebController
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("OrderConfirmData",confirmVo);
//展示订单确认的数据
return "confirm";
}
实现:
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌(幂等性章节)
return confirmVo;
}
2.7.7 Feign远程调用丢失请求头问题
出现问题:远程调用购物车服务,购物车认为未登录
出现问题的原因:feign构建的新请求未把老请求头给带过来
![1670916001613](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916001613.png)
解决方案:feign在创建RequestTemplate之前会调用很多RequestInterceptor,可以利用RequestInterceptor将老请求头给加上
![1670916079666](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916079666.png)
配置类:
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、RequestContextHolder拿到刚进来的这个请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();//老请求
//同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie
template.header("Cookie",cookie);
}
};
}
}
1、新线程没有用户数据的问题RequestContextHolder
RequestContextHolder可以解决的问题:
- 正常来说在service层是没有request和response的,然而直接从controlller传过来的话解决方法太粗暴。解决方法是SpringMVC提供的RequestContextHolder
- 用线程池执行任务时非主线程是没有请求数据的,可以通过该方法设置线程中的request数据,原理还是用的threadlocal
RequestContextHolder推荐阅读:https://blog.csdn.net/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。
注意:上面在封装cookie的时候要拿到原来请求的cookie,设置到新的请求中
RequestContextHolder为SpingMVC中共享request数据的上下文,底层由ThreadLocal实现,也就是说该请求只对当前访问线程有效,如果new了新线程就找不到原来request了。
2.7.8 Feign异步调用丢失上下文的问题
1.注入线程池
![1670916581816](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916581816.png)
2.使用异步编排,各个任务彼此之间互不相关,但是需要等待各个任务处理完成
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多的拦截器
//RequestInterceptor interceptor :requestInterceptors
}, executor);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌(幂等性章节)
CompletableFuture.allOf(getAddressFuture,cartFuture).get();//等待所有结果完成
return confirmVo;
}
出现问题: 异步任务执行远程调用时会丢失请求上下文,oldRequest会为null
![1670916687666](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916687666.png)
出现问题的原因: 当我们不使用异步编排的时候也就是单线程执行的时候,请求上下文持有器即:RequestContextHolder采用的是ThreadLocal存储请求对象。当我们采用异步编排时,而是多个线程去执行,新建的线程会丢失请求对象。
![1670916743944](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916743944.png)
![1670916769421](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916769421.png)
解决方案: 每个新建的线程都去添加之前请求的数据
![1670916815594](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916815594.png)
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
System.out.println("主线程..."+Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多的拦截器
//RequestInterceptor interceptor :requestInterceptors
}, executor);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌(幂等性章节)
CompletableFuture.allOf(getAddressFuture,cartFuture);
return confirmVo;
}
ps:
因为异步编排的原因,他会丢掉
ThreadLocal
中原来线程的数据,从而获取不到loginUser
,这种情况下我们可以在方法内的局部变量中先保存原来线程的信息,在异步编排的新线程中拿着局部变量的值重新设置到新线程中即可。
- 由于
RequestContextHolder
使用ThreadLocal
共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie
了在这种情况下,我们需要在开启异步的时候将老请求的
RequestContextHolder
的数据设置进去。
bug修改:
出现问题:
- org.thymeleaf.exceptions.TemplateInputException: Error resolving template [user/cart], template might not exist or might not be accessible by any of the configured Template Resolvers
解决方案:
![1670916941248](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670916941248.png)
此外:远程获取价格的时候应该用R。
购物车服务修改
CartServiceImpl
![1670917623993](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670917623993.png)
ProductFeignService
![1670917667903](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670917667903.png)
商品服务修改:
SkuInfoController
![1670917720495](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670917720495.png)
2.7.9 订单确认页渲染
1.收货人信息回显
![1670926142368](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670926142368.png)
<div class="top-3" th:each="addr:${orderConfirmData.address}">
<p>[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
</div>
2. 商品信息回显
添加两项新属性
![1670926208073](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670926208073.png)
![1670926248890](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670926248890.png)
<div class="yun1" th:each="item:${orderConfirmData.items}">
<img th: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>[[${item.hasStock?"有货":"无货"}]]</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>
3. 商品总件数、总金额、应付金额回显
总件数计算:OrderConfirmVo
//总件数
public Integer getCount(){
Integer i = 0;
if (items != null){
for (OrderItemVo item : items) {
i += item.getCount();
}
}
return i;
}
![1670926400730](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670926400730.png)
2.7.10 订单确认页库存查询
1.库存服务中查询库存的方法之前已经编写好了
WareSkuController
//查询sku 是否有库存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List<Long> skuIds){
//sku_id,stock
List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
return R.ok().setData(vos);
}
2. 远程服务接口调用编写
订单服务下
@FeignClient("gulimall-ware")
public interface WmsFeignService {
//查询sku 是否有库存
@PostMapping("/ware/waresku/hasstock")
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
3. Vo编写
@Data
public class SkuStockVo {
private Long skuId;
private Boolean hasStock;
}
4. 编写异步任务查询库存信息
编写Map用于封装库存信息
![1670946774348](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670946774348.png)
![1670946809372](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670946809372.png)
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
// 封装订单
OrderConfirmVo confirmVo = new OrderConfirmVo();
// 获取用户,用用户信息获取购物车
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// System.out.println("主线程..."+Thread.currentThread().getId());
/**
* 我们要从request里获取用户数据,但是其他线程是没有这个信息的,
*所以可以手动设置新线程里也能共享当前的request数据
*/
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
// System.out.println("member线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
//因为异步线程需要新的线程,而新的线程里没有request数据,所以我们自己设置进去
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多的拦截器
//RequestInterceptor interceptor :requestInterceptors
}, executor).thenRunAsync(()->{
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
// TODO 一定要启动库存服务,否则库存查不出
R hasStock = wmsFeignService.getSkuHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null){
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
},executor);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌(幂等性章节)
CompletableFuture.allOf(getAddressFuture,cartFuture).get();//等待所有结果完成
return confirmVo;
}
库存信息回显
![1670946873010](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670946873010.png)
<span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
出现空指针异常:无需共享数据就不用做以下操作了
![1670946957550](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670946957550.png)
测试:
![1670948473155](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670948473155.png)
注意,想要显示有货,数据库 wms_ware_sku表中这两个字段必须有值才行。
![1670948512787](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1670948512787.png)
2.7.11 订单确认页模拟运费效果
1.远程服务调用查询地址接口编写
库存服务下编写
@FeignClient("gulimall-member")
public interface MemberFeignService {
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R addrInfo(@PathVariable("id") Long id);
}
这个远程获取地址的方法在 会员服务 MemberReceiveAddressController下已经写过了,所以可以直接使用。
![1671000318523](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000318523.png)
2.编写获取邮费的接口
WareInfoController
/**
* 根据用户的收货地址计算运费
* @param addrId
* @return
*/
@GetMapping("/fare")
public R getFare(@RequestParam("addrId") Long addrId){
BigDecimal fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
WareInfoServiceImpl
@Override
public BigDecimal getFare(Long addrId) {
R r = memberFeignService.addrInfo(addrId);
MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
});
if (data != null){
String phone = data.getPhone();
// 12345678918 8:截取手机号的最后一位当做运费,实际上应该是接入第三方物流作为接口。这里只是简单模拟一下。
String substring = phone.substring(phone.length() - 1, phone.length());
return new BigDecimal(substring);
}
return null;
}
3. 地址高亮显示
为div绑定class方便找到,自定义def属性存储默认地址值,默认地址为1,否则为0;自定义属性存储地址Id
![1671000445580](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000445580.png)
空格代表子元素
![1671000512478](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000512478.png)
函数调用
![1671000626321](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000626321.png)
为运费定义一个id,用于运费的回显
![1671000680247](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000680247.png)
为应付总额定义一个id,用于计算应付总额的回显
![1671000707568](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000707568.png)
为p标签绑定单击事件
![1671000768842](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000768842.png)
默认地址的邮费查询
![1671000815274](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000815274.png)
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);
});
//发送Ajax获取运费信息
function getFare(addrId) {
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId="+addrId,function (data) {
console.log(data);
//fareEle
$("#fareEle").text(data.data);
var total = [[${orderConfirmData.total}]];
$("#payPriceEle").text(total*1 + data.data*1);
})
}
效果展示:
![1671000856216](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671000856216.png)
2.7.12 订单确认页细节显示
查询运费时连同地址信息一起返回,也就是选中地址的地址信息回显
1.编写vo
@Data
public class FareVo {
private MemberAddressVo address;
private BigDecimal fare;
}
2.改写实现类
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();
// 12345678918 8:截取手机号的最后一位当做运费,实际上应该是接入第三方物流作为接口。这里只是简单模拟一下。
String substring = phone.substring(phone.length() - 1, phone.length());
BigDecimal bigDecimal = new BigDecimal(substring);
fareVo.setAddress(data);
fareVo.setFare(bigDecimal);
return fareVo;
}
return null;
}
WareInfoService
/**
* 根据用户的收货地址计算运费
* @param addrId
* @return
*/
FareVo getFare(Long addrId);
WareInfoController
/**
* 根据用户的收货地址计算运费
* @param addrId
* @return
*/
@GetMapping("/fare")
public R getFare(@RequestParam("addrId") Long addrId){
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
3. 信息回显
![1671002085204](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671002085204.png)
![1671002112035](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671002112035.png)
//发送Ajax获取运费信息
function getFare(addrId) {
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
console.log(resp);
//fareEle
$("#fareEle").text(resp.data.fare);
var total = [[${orderConfirmData.total}]];
//设置运费等
$("#payPriceEle").text(total*1 + resp.data.fare*1);
//设置收货人信息
$("#receiveAddressEle").text(resp.data.address.province+" "+resp.data.address.detailAddress);
$("#receiverEle").text(resp.data.address.name);
})
}
效果展示
![1671002150091](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671002150091.png)
2.7.13 接口幂等性讨论
一、什么是幂等性
假设网络很慢,用户多次点击提交订单,有可能会导致数据库中插入了多条订单记录,为了避免订单的重复提交,用专业的术语就称之为接口幂等性,通俗点讲就是用户提交一次和用户提交一百次的结果是一样的,数据库中只会有一条订单记录。
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。
二、哪些情况需要防止
用户多次点击按钮
用户页面回退再次提交
微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
其他业务情况
三、什么情况下需要幂等
以SQL 为例,有些操作是天然幂等的。
SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=1,多次操作,结果一样,具备幂等性
insert into user(userid,name) values(1,'a') 如userid 为唯一主键,即重复操作上面的业务,只
会插入一条用户数据,具备幂等性。
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
insert into user(userid,name) values(1,'a') 如userid 不是主键,可以重复,那上面业务多次操
作,数据都会新增多条,不具备幂等性。
四、幂等解决方案
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 脚本完成这个操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
2、各种锁机制
1、数据库悲观锁
select * from xxxx where id = 1 for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会
非常麻烦。
数据库悲观锁的使用场景:当我们查询库存信息时可以使用悲观锁锁住这条记录确保别人拿不到。
2、数据库乐观锁
这种方法适合在更新的场景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据version 版本,也就是在操作库存前先获取当前商品的version 版本号,然后操作的时候
带上此version 号。我们梳理下,我们第一次操作库存时,得到version 为1,调用库存服务
version 变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订
单服务传如的version 还是1,再执行上面的sql 语句时,就不会执行;因为version 已经变
为2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题。
数据库乐观锁的使用场景:当我们减库存操作时,带上version=1执行成功此时version=2,但是由于网络原因没有返回执行成功标识,下一次请求过来还是带上的是version=1就无法对库存进行操作。
3、业务层分布式锁
如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数
据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断
这个数据是否被处理过。
3、各种唯一约束
1、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。
对订单号设置唯一约束
![1671022240284](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671022240284.png)
我们在数据库层面防止重复。
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert 场景时幂等问题。但主键
的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要
不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
2、redis set 防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5 将其放入redis 的set,
每次处理数据,先看这个MD5 是否已经存在,存在就不处理。
Redis set的防重场景:每个数据的MD5加密后的值唯一,网盘就可以根据上传的数据进行MD5加密,将加密后的数据存储至Redis的set里,下次你上传同样的东西时先会去set进行判断是否存在,存在就不处理。
4、防重表
使用订单号orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且
他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避
免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个
事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
之前说的redis 防重也算
防重表的应用场景:当我们去解库存的时候,先去防重表里插入一条数据,当请求再次过来的时候,先去防重表里插入数据,只有当插入成功才能进行下一步操作。
5、全局请求唯一id
调用接口时,生成一个唯一id,redis 将数据保存到集合中(去重),存在即处理过。
可以使用nginx 设置每一个请求的唯一id;
proxy_set_header X-Request-Id $request_id;
Nginx为每一个请求设置唯一id可以用作链路追踪,看这个请求请求了那些服务
2.7.14 订单确认页完成
我们这里用token令牌机制解决幂等性。
1.订单服务的执行流程如下图所示
![1671003979864](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671003979864.png)
![1671004052696](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671004052696.png)
2. 防重令牌的编写
①注入StringRedisTemplate
OrderServiceImpl中
![1671022529224](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671022529224.png)
② 编写订单服务常量即防重令牌前缀,格式:order:token:userId
public class OrderConstant {
//订单放入redis中的防重令牌
public static final String USER_ORDER_TOKEN_PREFIX = "order:token:";
}
③ 防重令牌存储
![1671022635095](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671022635095.png)
3. 提交页面数据Vo的编写
仿照京东:京东的结算页中的商品信息是实时获取的,结算的时候会去购物车中再去获取一遍,因此,提交页面的数据Vo没必要提交商品信息
/**封装订单提交的数据
*/
@Data
public class OrderSubmitVo {
private Long addrId;//收货地址id
private Integer payType;//支付方式
//无需提交需要购买的商品,去购物车再获取一遍
//优惠,发票
private String orderToken;//防重令牌
private BigDecimal payPrice;//应付价格,验价
private String note;//订单备注
//用户相关信息,直接去session中取出登录的用户
}
4. 前端页面提交表单编写
![1671022808313](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671022808313.png)
<form action="http://order.gulimall.com/submitOrder" method="post">
<input type="hidden" id="addrIdInput" name="addrId">
<input type="hidden" id="payPriceInput" name="payPrice">
<input type="hidden" name="orderToken" th:value="${orderConfirmData.orderToken}">
<button class="tijiao">提交订单</button>
</form>
5. 为input框绑定数据
![1671022861435](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671022861435.png)
//发送Ajax获取运费信息
function getFare(addrId) {
//给表单回填选择的地址
$("#addrIdInput").val(addrId);
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
console.log(resp);
//fareEle
$("#fareEle").text(resp.data.fare);
var total = [[${orderConfirmData.total}]];
//设置运费等
var payPrice = total*1 + resp.data.fare*1;
$("#payPriceEle").text(payPrice);
$("#payPriceInput").val(payPrice);
//设置收货人信息
$("#receiveAddressEle").text(resp.data.address.province+" "+resp.data.address.detailAddress);
$("#receiverEle").text(resp.data.address.name);
})
}
6.编写提交订单数据接口
OrderWebController
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo){
//下单:去创建订单,验令牌,验价格,锁库存...
//下单成功来到支付选择页
//下单失败回到订单确认页重新确认订单信息
return null;
}
2.7.15 原子验证令牌
1.提交订单返回结果Vo编写
@Data
public class SubmitOrderResponseVo {
private OrderEntity order;//当前订单内容
private Integer code;// 0成功 错误状态码
}
2. 接口编写
OrderWebController
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo){
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
if (responseVo.getCode() == 0){
//下单成功来到支付选择页
return "pay";
}else{
//下单失败回到订单确认页重新确认订单信息
return "redirect:http://order.gulimall.com/toTrade";
}
}
验证令牌的核心:保证令牌的比较和删除的原子性
解决方案:使用脚本
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
脚本执行的返回结果:
0:代表令牌校验失败
1:代表令牌成功删除即成功
execute(arg1,arg2,arg3)参数解释:
arg1:用DefaultRedisScript的构造器封装脚本和返回值类型
arg2:数组,用于存放Redis中token的key
arg3:用于比较的token即浏览器存储的token
T:返回值的类型
![1671025030730](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671025030730.png)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();//得到当前用户
//1、验证令牌【令牌的对比和删除必须保证原子性】
//0 令牌失败 - 1删除成功
// lua脚本实现原子性
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){
//令牌验证失败
return response;
}else {
//令牌验证成功
//下单:去创建订单,验令牌,验价格,锁库存...
}
return response;
}
2.7.16 构造订单数据
1.订单创建To的编写
@Data
public class OrderCreateTo {
private OrderEntity order;//当前订单内容
private List<OrderItemEntity> orderItems;//订单包含的所有订单项
private BigDecimal payPrice;//订单计算的应付价格
private BigDecimal fare;//运费
}
2. 创建订单方法编写
①订单状态枚举类的编写
直接从课件中复制过来即可。
public enum OrderStatusEnum {
CREATE_NEW(0,"待付款"),
PAYED(1,"已付款"),
SENDED(2,"已发货"),
RECIEVED(3,"已完成"),
CANCLED(4,"已取消"),
SERVICING(5,"售后中"),
SERVICED(6,"售后完成");
private Integer code;
private String msg;
OrderStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
② IDWorker中的getTimeId()生成时间id,不重复,用于充当订单号
![1671027904597](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671027904597.png)
Vo的编写
@Data
public class FareVo {
//地址信息
private MemberAddressVo address;
//运费信息
private BigDecimal fare;
}
④ 远程服务调用获取地址和运费信息
WmsFeignService
//获取运费
@GetMapping("/ware/wareinfo/fare")
R getFare(@RequestParam("addrId") Long addrId);
⑤ 使用ThreadLocal,实现同一线程共享数据
![1671028069221](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671028069221.png)
⑥ 实现方法编写
OrderServiceImpl
public OrderCreateTo createOrderTo(){
OrderCreateTo createTo = new OrderCreateTo();
//1、生成订单号
String orderSn = IdWorker.getTimeId();
//创建订单号
OrderEntity orderEntity = buildOrder(orderSn);
//2、获取到所有的订单项
List<OrderItemEntity> itemEntities = bulidOrderItems();
//3、验价
return createTo;
}
/**
* 构建订单
* @param orderSn
* @return
*/
private OrderEntity buildOrder(String orderSn) {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(orderSn);
OrderSubmitVo submitVo = submitVoThreadLocal.get();
//获取收货地址信息
R fare = wmsFeignService.getFare(submitVo.getAddrId());
FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
});
//设置运费信息
entity.setFreightAmount(fareResp.getFare());
//设置收货人信息
entity.setReceiverCity(fareResp.getAddress().getCity());
entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
entity.setReceiverName(fareResp.getAddress().getName());
entity.setReceiverPhone(fareResp.getAddress().getPhone());
entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
entity.setReceiverProvince(fareResp.getAddress().getProvince());
entity.setReceiverRegion(fareResp.getAddress().getRegion());
return entity;
}
/**
* 构建所有订单项数据
* @return
*/
private List<OrderItemEntity> bulidOrderItems() {
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size() > 0){
currentUserCartItems.stream().map(cartItem ->{
OrderItemEntity itemEntity = bulidOrderItem(cartItem);
return itemEntity;
}).collect(Collectors.toList());
}
return null;
}
/**
* 构建某一个订单项
* @param cartItem
* @return
*/
private OrderItemEntity bulidOrderItem(OrderItemVo cartItem) {
return null;
}
2.7.17 构造订单项数据
1.远程服务调用,通过skuId获取Spu信息
商品服务下的SpuInfoController
/**
* 通过skuId获取Spu信息
* @param skuId
* @return
*/
@GetMapping("/skuId/{id}")
public R getSpuInfoBySkuId(@PathVariable("id") Long skuId) {
SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
return R.ok().setData(entity);
}
实现:SpuInfoServiceImpl
@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
SkuInfoEntity byId = skuInfoService.getById(skuId);
Long spuId = byId.getSpuId();
SpuInfoEntity spuInfoEntity = getById(spuId);
return spuInfoEntity;
}
订单服务下ProductFeignService
@FeignClient("gulimall-product")
public interface ProductFeignService {
@GetMapping("/product/spuinfo/skuId/{id}")
R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}
2. 设置订单购物项数据
编写vo
@Data
public class SpuInfoVo {
private Long id;
/**
* 商品名称
*/
private String spuName;
/**
* 商品描述
*/
private String spuDescription;
/**
* 所属分类id
*/
private Long catalogId;
/**
* 品牌id
*/
private Long brandId;
/**
*
*/
private BigDecimal weight;
/**
* 上架状态[0 - 下架,1 - 上架]
*/
private Integer publishStatus;
/**
*
*/
private Date createTime;
/**
*
*/
private Date updateTime;
}
实现方法完善
@Autowired
ProductFeignService productFeignService;
public OrderCreateTo createOrderTo(){
OrderCreateTo createTo = new OrderCreateTo();
//1、生成订单号
String orderSn = IdWorker.getTimeId();
//创建订单号
OrderEntity orderEntity = buildOrder(orderSn);
//2、获取到所有的订单项
List<OrderItemEntity> itemEntities = bulidOrderItems(orderSn);
//3、验价
return createTo;
}
/**
* 构建订单
* @param orderSn
* @return
*/
private OrderEntity buildOrder(String orderSn) {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(orderSn);
OrderSubmitVo submitVo = submitVoThreadLocal.get();
//获取收货地址信息
R fare = wmsFeignService.getFare(submitVo.getAddrId());
FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
});
//设置运费信息
entity.setFreightAmount(fareResp.getFare());
//设置收货人信息
entity.setReceiverCity(fareResp.getAddress().getCity());
entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
entity.setReceiverName(fareResp.getAddress().getName());
entity.setReceiverPhone(fareResp.getAddress().getPhone());
entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
entity.setReceiverProvince(fareResp.getAddress().getProvince());
entity.setReceiverRegion(fareResp.getAddress().getRegion());
return entity;
}
/**
* 构建所有订单项数据
* @return
*/
private List<OrderItemEntity> bulidOrderItems(String orderSn) {
//最后确定每个购物项的价格
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size() > 0){
List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = bulidOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return itemEntities;
}
return null;
}
/**
* 构建某一个订单项
* @param cartItem
* @return
*/
private OrderItemEntity bulidOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
//1、订单信息:订单号
//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信息
itemEntity.setSkuId(cartItem.getSkuId());
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
itemEntity.setSkuAttrsVals(skuAttr);
itemEntity.setSkuQuantity(cartItem.getCount());
//4、优惠信息(不做)
//5、积分信息
itemEntity.setGiftGrowth(cartItem.getPrice().intValue());
itemEntity.setGiftIntegration(cartItem.getPrice().intValue());
return itemEntity;
}
2.7.18 订单验价
1.计算单个购物项的真实价格
![1671033476486](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671033476486.png)
2.设置订单的价格
![1671033520914](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671033520914.png)
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");
//订单的总额,叠加每一个订单项的总额信息
for (OrderItemEntity entity : itemEntities) {
coupon = coupon.add(entity.getCouponAmount());
integration = integration.add(entity.getIntegrationAmount());
promotion = promotion.add(entity.getPromotionAmount());
total = total.add(entity.getRealAmount());
gift = gift.add(new BigDecimal(entity.getGiftIntegration().toString()));
growth = growth.add(new BigDecimal(entity.getGiftGrowth().toString()));
}
//1、订单价格相关
orderEntity.setTotalAmount(total);
//应付总额
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(integration);
orderEntity.setCouponAmount(coupon);
//设置积分等信息
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
orderEntity.setDeleteStatus(0);//未删除
}
3.订单其它信息设置
![1671033891238](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671033891238.png)
4. 验价
![1671033983730](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671033983730.png)
2.7.19 保存订单数据
1.保存订单和订单项数据
①保存订单和订单项以及锁库存操作处于事务当中,出现异常需要回滚
![1671093200982](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671093200982.png)
②注入orderItemService
![1671093227795](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671093227795.png)
③保存
![1671093283954](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671093283954.png)
/**
* 保存订单数据
* @param order
*/
private void saveOrder(OrderCreateTo order){
OrderEntity orderEntity = order.getOrder();
orderEntity.setModifyTime(new Date());
this.save(orderEntity);
//保存订单项
List<OrderItemEntity> orderItems = order.getOrderItems();
orderItemService.saveBatch(orderItems);
}
2.7.20 锁定库存
锁库存逻辑
![1671093760985](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671093760985.png)
远程服务调用锁定库存
1.锁库存Vo编写
订单服务下
@Data
public class WareSkuLockVo {
private String orderSn;//根据订单号判断是否存库存成功
private List<OrderItemVo> locks;//需要锁住的所有库存信息:skuId skuName num
}
将订单服务的WareSkuLockVo和OrderItemVo复制到库存服务中
2. 锁库存响应Vo编写
库存服务下
/**商品的库存锁定状态
*/
@Data
public class LockStockResult {
private Long skuId;//那个商品
private Integer num;//锁了几件
private Boolean locked;//锁住了没有,状态
}
3. 锁库存异常类的编写
public class NoStockException extends RuntimeException{
private Long skuId;
public NoStockException(Long skuId){
super("商品id:" + skuId + ";没有足够的库存了");
}
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
}
4. 库存不足异常状态码编写
![1671098135379](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671098135379.png)
5.为库存表的锁库存字段设置默认值:0
![1671096582621](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671096582621.png)
6. 查询库存接口编写
WareSkuController
/**
* 锁定库存
* @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());
}
}
实现:
![1671098283330](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671098283330.png)
指定抛出此异常时一定要回滚,不指定也会回滚默认运行时异常都会回滚
WareSkuServiceImpl
![1671098323099](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671098323099.png)
内部类保存商品在那些仓库有库存以及锁库存数量
@Data
class SkuWareHasStock{
private Long skuId;
private Integer num;
private List<Long> wareId;
}
锁库存实现
/**
* 为某个订单锁定库存
*
*
* (rollbackFor = NoStockException.class)
* 默认只要是运行时异常就会回滚
* @param vo
* @return
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
//1、按照下单的收货地址,找到一个就近仓库,锁定库存。//暂时不这样做
//1、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
Boolean skuStocked = false;//标识位
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0){
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
for (Long wareId : wareIds) {
//成功就返回的是1行受影响,否则就是0行受影响
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if (count == 1){
skuStocked = true;
break;
}else {
//当前仓库锁定失败,重试下一个仓库
}
}
if (skuStocked == false){
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
return true;
}
WareSkuDao
@Mapper
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {
void addStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("skuNum") Integer skuNum);
Long getSkuStock(Long skuId);//一个参数的话,可以不用写@Param,多个参数一定要写,方便区分
List<Long> listWareIdHasSkuStock(@Param("skuId") Long skuId);
Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);
}
![1671098538251](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671098538251.png)
相应SQL语句的由来:
SELECT ware_id FROM `wms_ware_sku` WHERE sku_id = 1 AND stock - stock_locked > 0
UPDATE `wms_ware_sku` SET stock_locked = stock_locked + 1
WHERE sku_id = 1 AND ware_id = 1 AND stock-stock_locked >= 1
<update 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}
</update>
<select id="listWareIdHasSkuStock" resultType="java.lang.Long">
SELECT ware_id FROM `wms_ware_sku` WHERE sku_id = #{skuId} AND stock - stock_locked > 0
</select>
7. 远程服务调用
订单服务下
![1671098787391](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671098787391.png)
8、接口完善
OrderWebController
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo,Model model){
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
if (responseVo.getCode() == 0){
//下单成功来到支付选择页
model.addAttribute("submitOrderResp",responseVo);
return "pay";
}else{
//下单失败回到订单确认页重新确认订单信息
return "redirect:http://order.gulimall.com/toTrade";
}
}
OrderServiceImpl
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
submitVoThreadLocal.set(vo);//放到线程中共享数据
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();//得到当前用户
response.setCode(0);
//1、验证令牌【令牌的对比和删除必须保证原子性】
//0 令牌失败 - 1删除成功
// lua脚本实现原子性
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 {
//令牌验证成功
//下单:去创建订单,验令牌,验价格,锁库存...
//1、创建订单、订单项等信息
OrderCreateTo order = createOrderTo();
//2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
//页面提交价格与计算的价格相差小于0.01则验价成功
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//.....
//3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
// TODO 远程锁库存
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0){
//锁成功了
response.setOrder(order.getOrder());
return response;
}else{
//锁定失败
response.setCode(3);
return response;
}
} else {
response.setCode(2);
return response;
}
}
}
2.7.21 提交订单的问题
1.订单号显示、应付金额回显
![1671110447704](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671110447704.png)
ps:之前代码忘记创建订单之后相应数据,这个方法应该补充以下两项。
![1671110575018](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671110575018.png)
出现问题:orderSn长度过长
解决方案:数据库的表中的对应字段长度增大
![1671110644515](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671110644515.png)
![1671110664428](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671110664428.png)
2.提交订单消息回显
![1671110708718](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671110708718.png)
confirm.html
![1671110737692](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671110737692.png)
3. 为了确保锁库存失败后,订单和订单项也能回滚,需要抛出异常
修改 NoStockException,将库存下的这个异常类直接放到公共服务下。并添加一个构造器。
![1671111185321](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671111185321.png)
orderServiceImpl
![1671111215746](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671111215746.png)
OrderWebController
![1671204687908](F:\JAVA listen\尚硅谷Java学科全套教程(总207.77GB)\谷粒商城\课件(最新)\自己\谷粒商城之高级篇\1671204687908.png)