谷粒商城项目之高级篇笔记(一)

本文详细介绍了谷粒商城项目中的商品上架流程,包括商品Mapping、建立商品索引、上架细节、数据一致性及代码实现。同时,讲解了商城系统首页的Thymeleaf整合、DevTools使用,以及通过Nginx实现域名访问环境。此外,还涉及了缓存管理,包括Redis的使用、分布式锁和缓存一致性问题。最后,讨论了商品检索的业务分析、DSL语句、服务构建和页面渲染等关键步骤。
摘要由CSDN通过智能技术生成

目录

1 商城业务

1.1 商品上架

需求:

  • 上架的商品才可以在网站展示。
  • 上架的商品需要可以被检索。

1.1.1 商品Mapping

商品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,以空间换时间

1.1.2 建立product索引

  • { “type”: “keyword” }, # 保持数据精度问题,可以检索,但不分词
  • “analyzer”: “ik_smart” # 中文分词器
  • “index”: false, # 不可被检索,不生成index
  • “doc_values”: false # 默认为true,设置为false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。还可以通过设定doc_values为true,index为false来让字段不能被搜索但可以用于排序、聚合以及脚本操作
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" }
                }
            }
        }
    }
}

1669197427060

nested嵌入式对象

属性是"type": “nested”,因为是内部的属性进行检索

数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)

user.name=["aaa","bbb"]
user.addr=["ccc","ddd"]

这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的

数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)

nested阅读:https://blog.csdn.net/weixin_40341116/article/details/80778599

使用聚合:https://blog.csdn.net/kabike/article/details/101460578

1669204852792

1.1.3 上架细节

上架是将后台的商品放在es 中可以提供检索和查询功能。
1)、hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要
更新一下es
2)、库存补上以后,也需要重新更新一下es
3)、hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新
热度值。
4)、下架就是从es 中移除检索项,以及修改mysql 状态

商品上架步骤:
1)、先在es 中按照之前的mapping 信息,建立product 索引。
2)、点击上架,查询出所有sku 的信息,保存到es 中
3)、es 保存成功返回,更新数据库的上架状态信息。

1.1.4 数据一致性

1)、商品无库存的时候需要更新es 的库存信息
2)、商品有库存也要更新es 的信息

1.1.5 代码实现

  • 接口文档

image-20221123222707831

  • 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 SkuEsMode

商品上架需要在es中保存spu信息并更新spu的状态信息,由于SpuInfoEntity与索引的数据模型并不对应,所以我们要建立专门的vo进行数据传输。

package com.atguigu.common.to.es;

//商品在 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
    • spu下的sku的规格参数相同,因此我们要将查询规格参数提前,只查询一次
    • spu和sku:spu是款,sku是件。spu > sku.
1)先查库存(商品微服务远程调用库存微服务)
  1. 在ware库存微服务里添加"查询sku是否有库存"的controller
  • WareSkuController
package com.atguigu.gulimall.ware.controller;

/**
     * 查询sku是否有库存
     */
    @PostMapping("/hasstock")
    public R getSkuHasStock(@RequestBody List<Long> skuIds){
        //sku_id,stock --- 引出创建SkuHasStockVo
        List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
        return R.ok().setData(vos);
    }
  • 在ware微服务中的vo包下面新建 SkuHasStockVo (方便查询库存) (后面product微服务也会用到,所以在后面会将其复制到common微服务中)
package com.atguigu.gulimall.ware.vo;

@Data
public class SkuHasStockVo {

    private Long skuId;

    private Boolean hasStock;

}
  • WareSkuServiceImpl (查出当前商城下面的所有以sku_id和库存量为集合的数据)
package com.atguigu.gulimall.ware.service.impl;

@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;
    }
  • SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id = 1查询华为这个sku下的总库存量

image-20221126145854208

image-20221126145925505

  • WareSkuDao (查库存)
package com.atguigu.gulimall.ware.dao;


@Mapper
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {


    Long getSkuStock(Long skuId);//一个参数的话,可以不用写@Param,多个参数一定要写,方便区分
}
  • WareSkuDao.xml
 <select id="getSkuStock" resultType="java.lang.Long">
        SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE  sku_id=#{skuId}     #动态获取
    </select>
  • 接着我们在商品微服务调用库存微服务的查询库存总量的方法来查询商品的库存总量

在 package com.atguigu.gulimall.product.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);
}
  1. 将 R 工具类进行改装,查完库存后直接返回封装好的数据(debug检查错误后最终选择自己封装解析结果) — 解决 return R.ok().setData(vos)
package com.atguigu.common.utils;

public class R extends HashMap<String, Object> {
	private static final long serialVersionUID = 1L;


    /**
    		加入以下代码
    */
    
	//利用 阿里巴巴提供的fastjson 进行逆转
	public <T> T getData(TypeReference<T> typeReference){
        /**
        public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    	}
    	
    	//默认是HashMap.Node<k,v> e
        */
		Object data = get("data");
        
		String s = JSON.toJSONString(data);
        
        /**
        
           String jsonStr = "[{\"id\":1001,\"name\":\"Jobs\"}]";
       		List<Model> models = JSON.parseObject(jsonStr, new TypeReference<List<Model>>() {});
       
            Params:
            text – json string
            type – type refernce
            features –
            Returns:

        */
		T t = JSON.parseObject(s, typeReference);
		return t;
	}

	public R setData(Object data){
        //将数据data和data进行映射  
		put("data",data);
		return this;
	}
...

image-20221126153824044

2)商品上架,保存到es中(商品微服务远程调用搜索微服务)
  1. 搜索微服务下创建controller.ElasticSaveController
package com.atguigu.gulimall.search.controller;


@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {

    @Autowired
    ProductSaveService productSaveService;

    //上架商品
    // 添加@RequestBody 将 请求体中的 List<SkuEsModel> 集合转换为json数据,因此请求方式必须为  @PostMapping   
    // GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。
    @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());
        }

    }
}

  • 搜索微服务下创建service和impl
  • ProductSaveService
package com.atguigu.gulimall.search.service;

public interface ProductSaveService {
    boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException;
}

  • ProductSaveServiceImpl
package com.atguigu.gulimall.search.service.impl;

@Slf4j
@Service("productSaveService")
public class ProductSaveServiceImpl implements ProductSaveService {

    @Autowired
    RestHighLevelClient restHighLevelClient;    //eslaticsearch和springboot整合中我们就使用的这个

    @Override
    public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {

        //保存到es
        //1.给 es 中建立索引。product,建立好映射关系。 (提前使用kibana为商品建立索引及映射)

        //2.给 es 中保存这些数据
        //bulkRequest   用来批量处理请求
        BulkRequest bulkRequest = new BulkRequest();
        for (SkuEsModel model : skuEsModels) {
            //1.构造保存请求   index在eslaticsearch中是保存操作
            /**
             public IndexRequest(String index) {
        		super(NO_SHARD_ID);
        		this.index = index;
    		}
            */
            IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
			//设置索引文档的id
            indexRequest.id(model.getSkuId().toString());
            String s = JSON.toJSONString(model);
			/**
			public IndexRequest source(String source, XContentType xContentType) {
        			return source(new BytesArray(source), xContentType);
    		}
			*/
            indexRequest.source(s, XContentType.JSON);

			//批量操作中添加index请求
            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;
    }
}
  • image-20221126155317098

  • **RestHighLevelClient ** eslaticsearch和springboot整合中我们就使用的这个

image-20221126154959263

  • 搜索微服务下创建constant.EsConstant
package com.atguigu.gulimall.search.constant;

public class EsConstant {

    public static final String PRODUCT_INDEX = "product";  //sku数据在 es中的索引

}

  1. fenign 调用: gulimall-product 调用 gulimall-search
  • 商品微服务下SearchFeignService
package com.atguigu.gulimall.product.feign;

@FeignClient("gulimall-search")
public interface SearchFeignService {

    @PostMapping("/search/save/product")
    R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
  1. 上架失败返回R.error(错误码,消息)

此时再定义一个错误码枚举。在接收端获取他返回的状态码

  • BizCodeEnume(common微服务下的exception:专门存放设置错误码)
  • 注意枚举类如果新增一个,必须要将改为
 package com.atguigu.common.exception;

PRODUCT_UP_EXCEPTION(11000,"商品上架异常");
  1. 点击上架后再让数据库中状态变为上架状态
  • 这里在 gulimall-common包下的constant.ProductConstant 类中创建一个新的枚举类(复制里面有的类,稍作修改即可)
package com.atguigu.common.constant;

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;
        }
    }
}
3) 商品微服务中商品上架总代码
  • SpuInfoController
package com.atguigu.gulimall.product.controller;

/**
     * /product/spuinfo/{spuId}/up
     * 商品上架功能
     */
    @PostMapping("/{spuId}/up")
    public R spuUp(@PathVariable("spuId") Long spuId){
        spuInfoService.up(spuId);

        return R.ok();
    }
  • SpuInfoServiceImpl
 package com.atguigu.gulimall.product.service.impl;

@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的所有可以用来被检索的规格属性,
        //SkuInfoEntity    --- pms_product_attr_value   spu属性值表
        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中相比SkuInfoEntity少的数据。
            //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;
             *          }
             *    }
             *
             */

        }

    }
  • AttrService
 package com.atguigu.gulimall.product.service;

List<Long> selectSearchAttrIds(@Param("attrIds") List<Long> attrIds);   //加这个@Param,否则会出现
  • AttrServiceImpl
package com.atguigu.gulimall.product.service;

@Override
    public List<Long> selectSearchAttrIds(List<Long> attrIds) {

        /**
         * SELECT  attr_id FROM `pms_attr` WHERE attr_id IN(?) AND search_type = 1
         */
        return baseMapper.selectSearchAttrIds(attrIds);


    }
  • AttrDao(查询出哪些规格属性可以被检索)
package com.atguigu.gulimall.product.dao;

 List<Long> selectSearchAttrIds(@Param("attrIds") List<Long> attrIds);
  • AttrDao.xml

<select id="selectSearchAttrIds" resultType="java.lang.Long">
        SELECT  attr_id  FROM `pms_attr` WHERE attr_id IN
        <foreach collection="attrIds" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
        AND search_type = 1

    </select>
  • SpuInfoDao (更新库存状态)
package com.atguigu.gulimall.product.dao;

@Mapper
public interface SpuInfoDao extends BaseMapper<SpuInfoEntity> {

void updataSpuStatus(@Param("spuId") Long spuId, @Param("code") int code);
}
  • SpuInfoDao.xml
<update id="updataSpuStatus">
        UPDATE `pms_spu_info` SET publish_status =#{code},update_time=NOW() WHERE id =#{spuId}
    </update>

1669257801032

4)上架中调用的两个远程微服务

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);
}
5)踩坑

报错:

nested exception is org.apache.ibatis.binding.BindingException: Parameter ‘attrIds’ not found. Avail …

  • 解决办法:(来自网络)

image-20221125225717486

image-20221125225745388

我的错误就是没有在dao或者接口中加入@Param这个注解。找不到对应的属性名。 加入注解即可。

image-20221125225845075

6)效果展示

商品成功上架,显示状态 为 已上架

1669277105482


1.2 商城系统首页

image-20221127102330789

前面的分布式基础我们使用的是前后端分离,即前端使用vue进行开发,后端就只做后端代码。但是从分布式高级开始,我们开始使用动静分离的方式。

  • 对于以前的代码,我们可以将分布式基础中关于rest风格的,对接app操作的,前后端分离的包从controller改为app.将对接页面的controller,我们放到web中

image-20221126191400799

1.2.1 整合thymeleaf渲染首页

1)、导入依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

将thymeleaf模板引擎导入商品微服务中

2)、资源存放

将老师给的课件中有关首页的资源放到gulimall-product包下的resources下。index文件夹放到static静态资源文件夹下,index.html放到templates文件夹下。

image-20221127135102126

3)、关闭thymeleaf缓存

我们在application.yml中关闭thymeleaf缓存,方便我们在更新网页资源的时候可以实时更新。

image-20221127135450142

4)、访问首页

localhost:11000

image-20221126194306527

5)、踩坑

这里记录一个坑:

按照老师的设置之后启动访问首页,一直显示访问不了。也不是什么端口问题,后面发现是缓存的问题。

解决办法:删除 target目录,重启商品微服务。

image-20221126194122099

1.2.2 整合dev-tools渲染一级分类

1)、编写IndexController
  1. 我们现在访问的首页的数据都是写死的,我们对于一级分类数据需要从数据库中查出。

    在web包下编写一个IndexController类,实现我们访问localhost:11000/ 和localhost:11000/index.html都可以跳转到首页。

package com.atguigu.gulimall.product.web;
/**
 * @author hxld
 * @create 2022-11-26 20:55
 */
@Controller
public class IndexController {

    @Autowired
    CategoryService categoryService;

    @GetMapping({"/","/index.html"})
    public String indexPage(Model model){
        //TODO 1.查出所有的1级分类
        List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();

        model.addAttribute("categorys",categoryEntities);
        return "index";
    }
}

2)、service和serviceImpl
  • CategoryService

    package com.atguigu.gulimall.product.service;
    
    List<CategoryEntity> getLevel1Categorys();
    
  • CategoryServiceImpl

package com.atguigu.gulimall.product.service.impl;

@Override
    public List<CategoryEntity> getLevel1Categorys() {
        //parent_cid=0 或者 cat_level=1 都表示是一级分类。这里我们选用parent_cid=0
        List<CategoryEntity> entities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return entities;
    }
3) 、引入dev-tools

对于商城首页的编写,我们每次修改之后都希望不重启微服务就可以实时查看是否修改成功。所以我们可以引入热部署工具。

当然对于简单的页面修改我们可以直接ctrl+shift+f9。但是对于一些类或者方法的修改我们还是建议重启微服务,因为可以避免一些不必要的错误。

 <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
           <optional>true</optional>    <!--这个才相当于将工具真正的导入进来了-->
        </dependency>
模板引擎
 1)、thymeleaf-stater:关闭缓存
 2)、静态资源都放在static文件夹下就可以按照路径直接访问
 3)、页面放在templates下,直接访问
 4)、页面修改不重启服务器实时更新
     1)、引入dev-tools
     2)、修改网页面  ctrl+shift+f9重新自动编译下页面,代码配置,推荐重启微服务
4) 、修改index.html
    <!--轮播主体内容-->
    <div class="header_main">
      <div class="header_banner">
        <div class="header_main_left">
          <ul>
            <li th:each="category : ${categorys}">
              <!--th:attr="ctg-data=${category.catId}"  自定义属性写法-->
              <a href="#" class="header_main_left_a"  th:attr="ctg-data=${category.catId}"><b th:text="${category.name}">家用电器 </b></a>
            </li>
          </ul>

将写死的内容给删除掉,使用thymeleaf语法进行编写,动态获取数据。

1.2.3 渲染二、三级分类数据

1)、删除静态资源json数据

前面的二、三级分类的数据都是写在index.json文件夹下的catalog.json中,是写死的。我们将这些json数据进行格式化,如下图。

image-20221127143339918

image-20221126213250929

  • 删除json,我们自己编写

image-20221126214211576

2)、代码实现
  1. 修改js文件夹下的catalogLoader.js,改成如下图所示,到时候我们向index/catalog.json发送请求查数据

image-20221127143642938

  1. 根据上面的json解析的数据新建Catelog2Vo
package com.atguigu.gulimall.product.vo;


/**
 * @author hxld
 * @create 2022-11-26 21:36
 */

//2级分类vo
@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;

    }


}
  1. indexcontroller中编写
//index/catalog.json
    @ResponseBody
    @GetMapping("/index/catalog.json")
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson();

        return catalogJson;
    }

  1. CategoryService
Map<String, List<Catelog2Vo>> getCatalogJson();
  1. 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;
    }
  1. 访问首页

image-20221127144141853

3)、问题

image-20221126232215450

image-20221126232229604

我发现我的在数据库中修改了三级分类的名字,但是首页不实时更新,正常来说数据都是从mysql数据库中取出的,应该会更新的,但是我的不知道为什么不更新。


1.3 nginx 搭建域名访问环境

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Vs8MANc-1673531878263)(null)]

1.3.1 反向代理配置

1)、编写本机hosts

我们使用SwitchHosts文件进行更方便的hosts文件的编写。

192.168.56.10 gulimall.com

  • 当我们在浏览器输入gulimall.com.因为本机hosts中指定了映射,访问gulimall.com.会访问192.168.56.10 虚拟机

image-20221127151304903

2) 、nginx中配置反向代理
  1. Nginx的配置文件详解:

1669306432874

我们查看到配置不仅仅在/nginx.conf,这个是总配置。为了不让单个配置文件过大,nginx还会在/etc/nginx/conf.d/*.conf存放配置文件,然后总配置文件在进行包含。

1669306804896

  1. 我们修改默认配置文件之前先进行备份。

image-20221127152518636

  1. 修改server_name和 配置一个代理

nginx中我们设置了监听80端口,当端口(默认)是80.访问是gulimall.com的时候,会路由访问http://192.168.56.1:11000商品微服务端口,即又跳回到本机11000端口。

image-20221127152714735

image-20221127153234364

  1. 重启nginx

image-20221127153100402

  1. 访问测试

image-20221127153308281

当我们在浏览器输入gulimall.com.因为本机hosts中指定了映射,访问gulimall.com.会访问192.168.56.10 虚拟机。然后我们在虚拟机中的nginx中设置了映射。nginx中我们设置了监听80端口,当端口是80.访问是gulimall.com的时候,会代理访问http://192.168.56.1:11000商品微服务端口,即又跳回到本机11000端口。

1.3.2 负载均衡到网关

我们想一个问题,以后是集群的时候,难道我们没增加一台机器,就要重新去修改nginx吗?

所以我们解决办法是直接让nginx代理到网关,由网关来处理,加上nacos,就能动态上下线。

1)、nginx.conf中配置上游服务器

在http{}块内:

image-20221127154445984

2)、gulimall.conf中配置代理

如下图:

image-20221127154731219

3)、网关微服务中配置路由规则

image-20221127165001668

注意这个配置 一定要放在 最后:因为如果放在前面 ,它会禁用下面其他的网关配置:

比如,http://gulimall.com//product/attrattrgrouprelation/list 这个api 接口访问,它会首先到 gulimall.com,

然后因为没有进行 截串 设置(截取 /api前缀),出现 404 访问不到。

4)、测试及解决办法

我们在浏览器中访问gulimall.com的时候,报404错。

原因:

Nginx 转发给网关的时候,会丢失很多请求头信息,这里就缺失了 host ,这里我们暂时只配置 上 host 地址,以后缺啥补啥。

  • 来到gulimall.conf中修改,添加丢失的host 使用$host动态获取。

nginx – 网关 转给商品服务

image-20221127155846124

  • 测试

    image-20221127165302461

    访问成功。

  • 总结

    • image-20221127165348370

      1. 首先浏览器访问 gulimall.com
        因为我们在Windows配置了host映射:gulimall.com 映射IP 192.168.56.10(虚拟机Ip)
        所以会直接来到虚拟机

      2. 又因为 浏览器访问 默认不带端口,那就是访问80端口,所以会来到 Nginx,我们又配置 了 80端口监听 gulimall.com 这个域名;此外由于 **location/**下的配置:代理转发:

        Nginx 又代理给网关,这里注意一个细节:由于Nginx 转发会丢失 一些请求头信息,所以我们要加上请求头的配置,这里暂时只配置 host地址,之后的其他请求头配置我们用到的时候在进行添加;

      3. 网关发现 域名 是gulimall.com,进而就会找到 对应的配置:路由到商品服务,进而就转给了商品服务,这处网关配置一定要放在最后面,避免放在前面禁用后面的其他截串配置。

  gateway 是前端工程 到 后台服务器之间的一个 对内网关,nginx是用户到 前端工程 的网关,对外网关.

1.4 缓存

1.4.1 缓存

1) 、缓存使用

image-20221129092329435

image-20221129092438384

image-20221129092457923

2)、本地缓存

image-20221129101509238

3)、整合redis 作为缓存

image-20221129101523996

  1. 引入redis-starter(前提是docker中要安装redis)
	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 配置redis
spring:
redis:
    host: 192.168.56.10
    port: 6379     #不写默认也是6379
  1. 使用RedisTemplate 操作redis
 @Autowired
    StringRedisTemplate stringRedisTemplate;

@Test
    public void  test(){
        //hello  world
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();

        //保存
        ops.set("hello","world"+ UUID.randomUUID().toString());

        //查询
        String hello = ops.get("hello");
        System.out.println("之前保存的数据:"+hello);
    }
  1. 优化菜单获取业务getCatalogJson
package com.atguigu.gulimall.product.service.impl;
@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //给缓存中放json字符串,拿出的json字符串,还要逆转为能用的对象类型(序列化与反序列化)


        //1.加入缓存逻辑,注意缓存中存放的数据是json字符串
        //JSON跨语言,跨平台兼容
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if(StringUtils.isEmpty(catalogJSON)){
            //2.如果缓存中没有,就查询数据库
            System.out.println("缓存不命中....查询数据库....");
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();

            return catalogJsonFromDb;
        }
        System.out.println("缓存命中....");

        // 缓存中有的话就直接逆转,转 为我们指定的对象
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return result;
    }
  1. lettuce堆外内存溢出bug
  • 产生堆外内存溢出:OutOfDirectMemoryError
    1.springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信
    2.lettuce的bug导致netty堆外内存溢出 -Xmx300m;netty如果没有指定堆外内存,默认使用-Xmx300m
    可以通过-Dio.netty.maxDirectMemory进行设置

  • 解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存
    1.升级lettuce客户端
    2.切换使用jedis (开发中先选择这个,上线之后在使用lettcue,通过日志来判断问题在哪)

  • redisTemplate:
    lettuce、jedis操作redis的底层客户端。spring再次封装redisTemplate;

<!--引入redis-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <!--排除使用lettuce ,改为使用jedis-->
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

1.4.2 缓存失效问题

先来解决大并发读情况下的缓存失效问题;

1)、缓存穿透
  • 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null 写入缓存,这将导致这个不存在的数据每次
    请求都要到存储层去查询,失去了缓存的意义。

  • 在流量大时,可能DB 就挂掉了,要是有人利用不存在的key 频繁攻击我们的应用,这就是漏洞。

  • 解决:

    • 缓存空结果、并且设置短的过期时间。
    • 布隆过滤器、mvc拦截器
2)、缓存雪崩
  • 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。
  • 解决:
    • 原有的失效时间基础上增加一个随机值,比如1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
    • image-20221129100851150
3)、缓存击穿
  • 对于一些设置了过期时间的key,如果这些key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。

  • 这个时候,需要考虑一个问题:如果这个key 在大量请求同时进来前正好失效,那么所有对这个key 的数据查询都落到db,我们称为缓存击穿。

  • image-20221129100943253

  • 解决:

    • 加锁
    • 加互斥锁:业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db去数据库加载,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
4)、本地锁

对于以上的问题,我们的解决办法:

1.空结果缓存:解决缓存穿透
2.设置过期时间(加随机值):解决缓存雪崩
3.加锁:解决缓存击穿

对于单体应用,使用本地锁的方式来进行解决缓存失效问题。

//从数据库查询并封装数据
    // @Override   //本地锁
    public   Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {

        /**
         * 只要是同一把锁,就能锁住需要这个锁的所有线程
         *
         * 1.synchronized (this) :springboot所有的组件在容器中都是单例的
         * 2. public  synchronized Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {}加到方法上
         *
         * TODO  本地锁:synchronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁。本地锁只能锁住当前服务进程(单个服务器)
         * 想要在分布式情况下,锁住所有,必须使用分布式锁。
         */
        synchronized (this){
            //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
            return getDataFromDb();
        }

    }
  • 我们将原来的查询二、三级分类的方法可以使用idea中的快捷方式抽取为一个方法。

抽取代码为方法:

image-20221129102405333

修改抽取的方法的名字:

image-20221128173016576

测试本地锁在分布式情况下会遇到的问题

  • 快速复制微服务,模拟多个微服务

右键点击服务,copy configuration

program arguments: --server.port=10003

image-20221128170142430

image-20221128170628841

image-20221128170646966

image-20221128170837546

1.4.3 分布式锁

1)、分布式锁与本地锁

image-20221129105738019

2)、分布式锁实现

image-20221129112551760

redis中设置占坑和设置过期时间的操作:

image-20221128184651105

详细可查看redis文档:https://redis.io/commands/set/。

3)、分布式锁演进
  1. 阶段一

image-20221129144621479

  1. 阶段二

image-20221129144640930

  1. 阶段三

image-20221129144700116

  1. 阶段四

image-20221129144722496

  1. 阶段五

image-20221129144745196

image-20221129145128131

  1. 我们在最后解决方案中使用的是redis+lua脚本进行完成,保证删除锁式原子性的。
  • lua脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
  1. 最终代码
//分布式锁
    public   Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
            //1.占分布式锁。去redis占坑
            //Redis Documentation: SETNX (redis中加锁操作)    --- >   setIfAbsent
        String uuid = UUID.randomUUID().toString();  //4
        // Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","1111",300,TimeUnit.SECONDS); //阶段3
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS); //阶段4

        // Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if(lock){
           // System.out.println("获取分布式锁成功。。。。。");
            //加锁成功...执行业务
            //2.先要设置过期时间,注意过期时间和加锁操作必须是同步的,即原子操作。 //
            // redisTemplate.expire("lock",30,TimeUnit.SECONDS);//阶段2
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            try{
                 dataFromDb = getDataFromDb();

            }finally {
                //lua脚本   阶段5   lock =  KEYS[1]   uuid =  ARGV[1] 删除成功返回1,删除失败返回0
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //删除锁
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            // redisTemplate.delete("lock"); //删除锁   //阶段3
            //获取值对比+对比成功删除 也需要是原子操作   使用lua脚本解锁
            /**
            String lockValue = redisTemplate.opsForValue().get("lock");
            if(uuid.equals(lockValue)){
                //只有最开始设置锁的uuid和现在获取到的锁,即要删除的锁,即自己的锁一样了,才删除
                redisTemplate.delete("lock"); //删除锁
            }
             */
            return dataFromDb;

        }else {
            //加锁失败...
            //休眠100ms重试
           // System.out.println("获取分布式锁失败。。。。。等待重试");
            try{
                Thread.sleep(200);
            }catch (Exception e){

            }
            return getCatalogJsonFromDbWithLocalLock();//自旋的方式
        }
    }

上面的lua脚本写法每次用分布式锁时比较麻烦,我们可以采用redisson现有框架。

4)、Redisson 完成分布式锁
1. 简介

image-20221129150236262

官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

2.环境搭建

导入依赖

  • 先用redisson用作练习,上手,了解。后面可以使用redisson-spring-boot-starter
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>
  • 创建配置类MyRedissonConfig 文档:https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95
package com.atguigu.gulimall.product.config;


/**
 * @author hxld
 * @create 2022-11-28 20:43
 */
@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     * @return
     * @throws IOException
     */

   @Bean(destroyMethod = "shutdown")
   public RedissonClient redisson() throws IOException{
        //1.创建配置
        Config config = new Config();
        //Redis url should start with redis:// or rediss:// (for SSL connection)
       // 创建单例模式的配置
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");

        //2.根据config创建出redissonclient示例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

3. 可重入锁(Reentrant Lock)

A调用B。AB都需要同一把锁,此时可重入锁就可以重入,A就可以调用B。不可重入锁时,A调用B将死锁

// 参数为锁名字
RLock lock = redissonClient.getLock("CatalogJson-Lock");//该锁实现了JUC.locks.lock接口
lock.lock();//阻塞等待
// 解锁放到finally // 如果这里宕机:有看门狗,不用担心
lock.unlock();

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

  • 锁的续期:大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout来另行指定。

    //1.获取一把锁,只要锁的名字一样,就是同一把锁
            RLock lock = redissonClient.getLock("my-lock");
    
            //2.加锁
            lock.lock();  //阻塞式等待
            // lock.lock(10, TimeUnit.SECONDS); //10s自动解锁,自动解锁时间一定要大于业务的执行事件
            //问题:lock.lock(10,TimeUnit.SECONDS);在锁时间到了以后,不会自动续期。
            //1.如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
            //2.如果我们未指定锁的超时时间,就使用30*1000【LockWatchdogTimeout看门狗的默认时间】;
                //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动续期,续为30s
                //internalLockLeaseTime【看门狗时间】 /3 ,10s
            /**
             * 默认加的锁都是30s
             * 1.锁的自动续期,如果业务运行时间超长,redisson运行期间会自动给快要失效的锁续上新的30s,不用担心业务时间长,锁自动过期被删掉
             * 2.加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。
             *
             */
    
            //最佳实战:
            //1)、lock.lock(30,TimeUnit.SECONDS);省掉了整个续期操作,手动解锁
            try{
                System.out.println("加锁成功,执行业务..."+Thread.currentThread().getId());
                Thread.sleep(30000);
            } catch (Exception e) {
    
            } finally {
                //3.解锁
                System.out.println("释放锁..."+Thread.currentThread().getId());
                lock.unlock();
            }
            return "hello";
        }
    
    • Redisson同时还为分布式锁提供了异步执行的相关方法:

      RLock lock = redisson.getLock("anyLock");
      lock.lockAsync();
      lock.lockAsync(10, TimeUnit.SECONDS);
      Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
      
  • RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.

public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() {
    Map<String, List<Catalog2Vo>> categoryMap=null;
    RLock lock = redissonClient.getLock("CatalogJson-Lock");
    lock.lock();
    try {
        Thread.sleep(30000);
        categoryMap = getCategoryMap();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        lock.unlock();
        return categoryMap;
    }
}

最佳实战:自己指定锁时间,时间长点即可

lock.lock(30,TimeUnit.SECONDS);省掉了整个续期操作,手动解锁

4. 读写锁(ReadWriteLock)

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

    //保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁
    //写锁没释放就必须等待
    //读+读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功
    //写+读:等待写锁释放
    //写+写:阻塞方式
    //读+写:有读锁,写也需要等待
    //只要有写的存在,都必须等待。
    @GetMapping("/write")
    @ResponseBody
    public String writeValue(){
        RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = lock.writeLock();
        try {
            //1.改数据加写锁,读数据加读锁
            rLock.lock();
            System.out.println("写锁加锁成功:..."+Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            Thread.sleep(30000);
            redisTemplate.opsForValue().set("writeValue",s);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            rLock.unlock();
        }
        return s;
    }

    @GetMapping("/read")
    @ResponseBody
    public String readValue(){
        RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
        String s = "";
        //加读锁
        RLock rLock = lock.readLock();
        rLock.lock();
        System.out.println("读锁加锁成功:..."+Thread.currentThread().getId());
        try {
           s = redisTemplate.opsForValue().get("writeValue");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            rLock.unlock();
        }
        return s;
    }

上锁时在redis的状态:

HashWrite-Lock
key:mode  value:read
key:sasdsdffsdfsdf... value:1
5. 信号量(Semaphore)

信号量为存储在redis中的一个数字,调用acquire()方法获取资源时应该是让数字-1吧,同样release()方法释放资源会让数字+1。

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

/**
     * 车库停车 3车位
     * 信号量也可以用作分布式限流
     */

    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {
        RSemaphore park = redissonClient.getSemaphore("park");
        // park.acquire(); //获取一个信号,获取一个值,占一个车位
        boolean b = park.tryAcquire();
        if(b){
            //执行业务
        }else {
            return "error";
        }
        return "ok=>" +b;
    }


    @GetMapping("/go")
    @ResponseBody
    public String go() throws InterruptedException {
        RSemaphore park = redissonClient.getSemaphore("park");
        park.release(); //释放一个车位
        return "ok" ;
    }
6. 闭锁(CountDownLatch)

基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

以下代码只有offLatch()被调用5次后 setLatch()才能继续执行

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
/**
     *放假,锁门
     * 1班没人了,2
     * 5个班全部走完,我们才可以锁大门
     */
    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws  InterruptedException{
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.trySetCount(5);
        door.await(); //等待闭锁都完成
        return "放假了。。。。。";
    }


    @GetMapping("/gogogo/{id}")
    public  String gogogo(@PathVariable("id") Long id){
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.countDown();  //计数-1

        return id +"班的人都走完了";
    }

1.4.4 缓存数据一致性

1)、保证一致性模式
1.双写模式

image-20221130095200343

2.失效模式

image-20221130095147942

3. 改进方法1-分布式读写锁

分布式读写锁。读数据等待写数据整个操作完成。

4.改进方法2-使用cananl

image-20221130095220603

5. 汇总

image-20221130100036206


1.4.5 Spring Cache

1)、简介

image-20221130100822172

2)、基础概念

image-20221130100848396

3)、注解

image-20221130100910583

4)、代码实现
1.配置
  • 依赖
<dependency>
    <groupId>org.springframework.b oot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

  • 指定缓存类型并在主配置类上加上注解@EnableCaching

application.properties

spring.cache.type=redis
#毫秒为单位
spring.cache.redis.time-to-live=3600000
#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
  • 默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类
    • 创建config.MyCacheConfig
package com.atguigu.gulimall.product.config;


/**
 * @author hxld
 * @create 2022-11-29 21:59
 */
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {

    // @Autowired
    // public CacheProperties cacheProperties;

    /**
     * 配置文件的配置没有用上
     *
     * 1. 原来和配置文件绑定的配置类是这样的:
     * @ConfigurationProperties(prefix = "spring.cache")
     * public class CacheProperties
     *
     * 2. 要让他生效
     * @EnableConfigurationProperties(CacheProperties.class)
     * 
     */

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // config = config.entryTtl();
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //将配置文件中所有的配置都生效
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}
2. 缓存自动配置
// 缓存自动配置源码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
                     HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import({ CacheConfigurationImportSelector.class, // 看导入什么CacheConfiguration
         CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public CacheManagerCustomizers cacheManagerCustomizers(ObjectProvider<CacheManagerCustomizer<?>> customizers) {
        return new CacheManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
    }

    @Bean
    public CacheManagerValidator cacheAutoConfigurationValidator(CacheProperties cacheProperties,
                                                                 ObjectProvider<CacheManager> cacheManager) {
        return new CacheManagerValidator(cacheProperties, cacheManager);
    }

    @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
    @ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
    static class CacheManagerEntityManagerFactoryDependsOnPostProcessor
        extends EntityManagerFactoryDependsOnPostProcessor {

        CacheManagerEntityManagerFactoryDependsOnPostProcessor() {
            super("cacheManager");
        }

    }

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {

    @Bean // 放入缓存管理器
    RedisCacheManager cacheManager(CacheProperties cacheProperties, 
                                   CacheManagerCustomizers cacheManagerCustomizers,
                                   ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
                                   ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
                                   RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
        RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
            determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
        List<String> cacheNames = cacheProperties.getCacheNames();
        if (!cacheNames.isEmpty()) {
            builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
        }
        redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
        return cacheManagerCustomizers.customize(builder.build());
    }

3. @Cacheable
  /**
     *     //1. 每一个需要缓存的数据我们都来指定要放到哪个名字的缓存【缓存的分区(建议按照业务类型分)】 逻辑分区,我们自己设置的分区
     *     2. @Cacheable({"category"})
     *              代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。
     *              如果缓存中没有,会调用方法,最后将方法的结果放入缓存
     *     3. 默认行为
     *          1)、如果缓存中有,方法不用调用
     *          2)、key默认自动生成,缓存的名字::SimpleKey{}(自主生成的key值)
     *          3)、缓存的value的值,默认使用Jdk序列化机制,将序列化后的数据存到redis
     *          4)、默认ttl时间 -1
     *
     *          自定义
     *          1)、指定生成的缓存使用的key   key属性指定,接受一个spel(动态取值)
     *          2)、指定缓存的数据的存活时间    配置文件中设置ttl
     *          3)、将数据保存为json格式
     *
     */

    // @Cacheable(value={"category"},key="'Level1Categorys'")   //代表当前方法的结果需要缓存,如果缓存中有,方法不调用。如果缓存中没有,会调用方法,最后将方法的结果放入缓存
    @Cacheable(value={"category"},key="#root.method.name",sync = true)   //代表当前方法的结果需要缓存,如果缓存中有,方法不调用。如果缓存中没有,会调用方法,最后将方法的结果放入缓存
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("getLevel1Categorys");
        //parent_cid=0 或者 cat_level=1 都表示是一级分类。这里我们选用parent_cid=0
        List<CategoryEntity> entities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return entities;
    }
  • redis中生成的缓存的名称就是我们自己设置的方法名或其他。

image-20221129214128113

image-20221129214459877

image-20221129224916834

4. @CacheEvict
/**
     * 级联更新所有关联的数据
     * 1. @CacheEvict:缓存失效模式
     *  2. @Caching:同时进行多种缓存操作。比如一次性删除两个缓存
     *  3. 指定删除某个分区下的所有数据   @CacheEvict(value="category",allEntries = true)
     *  4.存储同一类型的数据,都可以指定成同一个分区。好处是如果存在多个缓存,修改了数据之后,同一个分区下的所有缓存都可以被清空
     *  分区名默认就是缓存的前缀
     *  category:key (redis中存储的样子)
     * @CachePut:双写模式
     * @param category
     */
    // @CacheEvict(value="category",key="'getLevel1Categorys'")
    // @Caching(evict = {
    //         @CacheEvict(value="category",key="'getLevel1Categorys'"),
    //         @CacheEvict(value="category",key="'getCatalogJson'")
    // })
    @CacheEvict(value="category",allEntries = true)   //默认是false,删除分区里面的所有数据    失效模式
    // @CachePut:双写模式
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
    }
5)、SpringCache原理与不足

image-20221130104706145


1.5 商品检索

1.5.1 搭建页面环境

  • 我们仍然使用的是动静分离的方法,我们将搜索页面的前端代码放到nginx中,我们在nginx中和商品上架一样进行处理。
  1. 将这个文件夹下面的除index.html中的文件夹都放到nginx中(创建search文件夹),index.html这个页面放到gulimall_search中的resourches下的templates下。

image-20221201093010159

image-20221201092658637

  1. 将index.html中关于href=“ 使用ctrl+r查找替换为href="/static/search,将src="查找替换为src=‘’/static/search。这样搜索页面的图片什么的就能展示出来了。

  2. 浏览器中输入search.guimall.com,可以访问到搜索页面,我们需要去修改hosts文件。如下图。

image-20221130153038833

  1. 去到mydata/nginx/conf/conf.d这个文件夹下面修改gulimall.conf 。因为我们以前在商品上架的时候已经设置好了上游服务器,所以我们这个地方只用加入*.gulimall.com

(这个地方不要只修改为*.gulimall.com,还要将gulimall.com这个加上,因为后面测试过程中发现在搜索页跳转不了首页)。

image-20221201095335522

image-20221201095247159

  1. 修改网关路由中的路径。

image-20221201100133186

image-20221201102617668

6.将搜索微服务下的index.html页面修改为list.html。因为我们搜索的时候跳转的地址是http://search.gulimall.com/list.html?catalogId=225

踩坑

image-20221130171531153

我们在首页点击手机分类-手机,跳转到搜索页面。这个时候出现问题,就是点击list,请求的url不是http://search.gulimall.com/list.html?catalogId=225 ,而是http://search.gmall.com/list.html?catalogId=225

  • 解决方法:

去nginx中的html/static/static/index/js文件夹中,将catalogLoader.js中的第22行的gmall修改为gulimall.然后去浏览器中将缓存清除,然后再重新访问。

image-20221130172100244

image-20221130172118745

image-20221130172153387

image-20221130170905378

image-20221130171703129

  1. 我们在首页搜索框点击搜索按钮的时候,比如说搜索手机,会跳转到搜索页面,展示搜索手机的信息。
  • 点击审查元素—搜索按钮找到是search(),然后list.html页面进行查找,即search()方法,将原来的代码修改为以下代码:

image-20221201103411563

image-20221130185309038

  • 遇到的问题:最开始老师课件中只做了如上的修改。但是我们点击的时候跳转页面显示的url如下图:

    image-20221130202045245

    • 问题解决:找到search()这个方法,将search这个方法的href修改为正确的url即可。

image-20221130202015396

1.5.2 检索业务分析

商品检索三个入口:

1)、选择分类进入商品检索

image-20221201104257165

2)、输入检索关键字展示检索页

image-20221201104316228

3)、选择筛选条件进入

image-20221201104330914

image-20221201104343525

4)、检索参数及检索返回结果抽取

gulimall-search包下新建vo封装

image-20221201104929762

  • SearchParam(检索参数)
package com.atguigu.gulimall.search.vo;

import lombok.Data;

import java.util.List;

/**
 * 封装页面可能检索的条件
 * catalog3Id=225&keyword=小米&sort=saleCount_asc&Stock=0/1&brandId=1&brandId=2
 * @author hxld
 * @create 2022-11-30 20:27
 */
@Data
public class SearchParam {

    /**
     * 页面传递过来的全文匹配关键字
     */
    private String keyword;

    /**
     * 品牌id,可以多选
     */
    private List<Long> brandId;

    /**
     * 三级分类id
     */
    private Long catalog3Id;

    /**
     * 排序条件
     *  sort=saleCount_asc/desc 倒序
     *  sort=skuPrice_asc/desc 根据价格
     *  sort=hotScore_asc/desc
     */
    private String sort;

    /**
     * 很多过滤条件
     * hasStock(是否有货) skuPrice价格区间 brandId catalog3Id attrs
     * hasStock 0/1
     * skuPrice=1_500 500_ _500
     * brandId = 1  品牌id
     * attrs1_5寸_6寸  手机大小
     * // 0 无库存 1有库存
     */
    private Integer hasStock;

    /**
     * 价格区间查询
     */
    private String skuPrice;

    /**
     * 按照属性进行筛选
     */
    private List<String> attrs;

    /**
     * 页码
     */
    private Integer pageNum = 1;

    /**
     * 原生的所有查询条件
     */
    private String _queryString;


}
  • SearchResult(返回结果)

查询得到商品、总记录数、总页码
品牌list用于在品牌栏显示,分类list用于在分类栏显示

其他栏每栏用AttrVo表示

  • 不仅要根据关键字从es中检索到商品
  • 还要通过聚合生成品牌等信息,方便分类栏显示
package com.atguigu.gulimall.search.vo;

/**
 * <p>Title: SearchResponse</p>
 * Description:包含页面需要的所有信息
 */
@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;

	/** 导航页   页码遍历结果集(分页)  */
	private List<Integer> pageNavs;
//	================以上是返回给页面的所有信息================

    /** 导航数据*/
    private List<NavVo> navs = new ArrayList<>();

    /** 便于判断当前id是否被使用*/
    private List<Long> attrIds = new ArrayList<>();

    @Data
    public static class NavVo {
        private String name;
        private String navValue;
        private String link;
    }

    @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;
    }
}


1.5.3 ES语句DSL

此处先写出如何检索指定的商品,如检索"华为"关键字

  • 嵌入式的属性
  • highlight:设置该值后,返回的时候就包装过了
  • 查出结果后,附属栏也要对应变化
  • 嵌入式的聚合时候也要注意
1)、dsl

image-20221130225428623

GET gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [ {"match": {  "skuTitle": "华为" }} ], # 检索出华为
      "filter": [ # 过滤
        { "term": { "catalogId": "225" } },
        { "terms": {"brandId": [ "2"] } }, 
        { "term": { "hasStock": "false"} },
        {
          "range": {
            "skuPrice": { # 价格1K~7K
              "gte": 1000,
              "lte": 7000
            }
          }
        },
        {
          "nested": {
            "path": "attrs", # 聚合名字
            "query": {
              "bool": {
                "must": [
                  {
                    "term": { "attrs.attrId": { "value": "6"} }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "sort": [ {"skuPrice": {"order": "desc" } } ],
  "from": 0,
  "size": 5,
  "highlight": {  
    "fields": {"skuTitle": {}}, # 高亮的字段
    "pre_tags": "<b style='color:red'>",  # 前缀
    "post_tags": "</b>"
  },
  "aggs": { # 查完后聚合
    "brandAgg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": { # 子聚合
        "brandNameAgg": {  # 每个商品id的品牌
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
      
        "brandImgAgg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
        
      }
    },
    "catalogAgg":{
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalogNameAgg": {
          "terms": {
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attrs":{
      "nested": {"path": "attrs" },
      "aggs": {
        "attrIdAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

2)、数据转移—修改映射
  • 修改以前不能聚合的属性,比如说商品brand
//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"
  }
}

//##数据迁移
//POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "newba
  • 修改sku数据在es中的索引,由product中的gulimall_product.

image-20221130225517308

1.5.4 检索服务构建(响应和结果提取封装)

  • 新建SearchController
package com.atguigu.gulimall.search.controller;
@Controller
public class SearchController {

   @Autowired
   MallSearchService mallSearchService;

    /**
     * 自动将页面提交过来的所有请求查询参数封装成指定的对象
     * @param param
     * @return
     */

    @GetMapping("/list.html")
    public  String listPage(SearchParam param, Model model){

        //1.根据传递过来的页面的查询参数,去es中检索商品
      SearchResult result =  mallSearchService.search(param);
      model.addAttribute("result",result);


        return "list";
    }


}
  • 新建MallSearchService
package com.atguigu.gulimall.search.service;
public interface MallSearchService {

    /**
     *
     * @param param
     * @return
     */

    SearchResult search(SearchParam param);
}

我们要将上面的dsl语句进行java代码的构建。

  • 新建MallSearchServiceImpl
package com.atguigu.gulimall.search.service.impl;

/**
 * @author hxld
 * @create 2022-11-30 20:30
 */
@Service
public class MallSearchServiceImpl implements MallSearchService {

    @Autowired
    private RestHighLevelClient client;
    //去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;
    }

    /**
     * 准备检索请求 查询!
     * 模糊匹配 过滤 按照属性 分类 品牌 价格区间 库存 排序 分页 高亮 聚合分析
     */
    private SearchRequest buildSearchRequest(SearchParam param) {
        //构建DSL语句
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        /**
         * 模糊匹配 过滤  按照属性  分类  品牌 价格区间  库存
         */
        //1.构建boolQuery   bool:聚合查询 termsQuery方法参数可以传一个或多个或数组  termQuery(域字段名,参数) 方法参数只能传入一个
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //1.1 must 模糊匹配     must :   {skuTitle:华为}
        if(!StringUtils.isEmpty(param.getKeyword())){
            boolQuery.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
        }
        //1.2 bool = filter 按照三级分类id查询   term:{catalogId:225}
        if(param.getCatalog3Id() != null){
            boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
        }
        //1.2 bool = filter  按照品牌id查询    是list
        if(param.getBrandId() != null && param.getBrandId().size() > 0 ){
            boolQuery.filter(QueryBuilders.termsQuery("brandId",param.getBrandId())); 
        }
        //1.2 bool = filter 按照所属属性进行查询
        if(param.getAttrs() != null && param.getAttrs().size()>0){
            //attrs=1_5寸:6寸&attrs=2_16G:8G
            for (String attrStr : param.getAttrs()) {
               BoolQueryBuilder nestBoolQuery = QueryBuilders.boolQuery();
               //attrs=1_5寸:6寸
                String[] s = attrStr.split("_");
                //检索的属性id   1 
                String attrId = s[0];
                //检索的属性值   5寸:6寸 
                String[] attrValues = s[1].split(":");
                nestBoolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
                nestBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));
                //每一个都必须生成一个nested查询   属性有很多	
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestBoolQuery, ScoreMode.None);
                boolQuery.filter(nestedQuery);
            }

        }

        //1.2 bool = filter 按照是否有库存进行查询    "hasStock": "false"  // 0 无库存 1有库存
        if(param.getHasStock() != null){
            boolQuery.filter(QueryBuilders.termsQuery("hasStock",param.getHasStock() == 1));
        }

        //1.2 bool = filter 按照价格区间进行查询
        //1_500 _500 500_
        if(!StringUtils.isEmpty(param.getSkuPrice())){
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
            String[] s = param.getSkuPrice().split("_");
            //1_500
            if(s.length == 2){
                //区间
                rangeQuery.gte(s[0]).lte(s[1]);
            }else  if(s.length == 1){
                if(param.getSkuPrice().startsWith("_")){
                    // _500
                    rangeQuery.lte(s[0]);
                }
                if(param.getSkuPrice().endsWith("_")){
                    //500_
                    rangeQuery.gte(s[0]);
                }
            }
            boolQuery.filter(rangeQuery);
        }
        //把以前的所有条件都拿来进行封装   "query": {"bool":......}
        sourceBuilder.query(boolQuery);

        /**
         * 排序 分页 高亮
         */
        //2.1 排序
        if(!StringUtils.isEmpty(param.getSort())){
            //sort=saleCount_asc/desc  倒序
            String[] s = param.getSort().split("_");
            SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            sourceBuilder.sort(s[0],order);
        }
        //2.2 分页 每页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) * EsContant.PRODUCT_PAGESIZE);
        sourceBuilder.size(EsContant.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);
        }

        /**
         * 聚合分析
         */
        //3.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);
        sourceBuilder.aggregation(brand_agg);


        //3.2分类聚合
        TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
        //子聚合
        catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
        sourceBuilder.aggregation(catalog_agg);

        //3.3 属性聚合
        NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");

        //子聚合
        TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
        //子子聚合 2 个
        //聚合分析出当前所有attrId对应的名字
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName")).size(1);
        //聚合分析出当前attrid对应的所有可能的属性值 attrValue
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue")).size(50);
        attr_agg.subAggregation(attr_id_agg);
        sourceBuilder.aggregation(attr_agg);

        String s = sourceBuilder.toString();
        System.out.println("构建的DSL...." + s);

        SearchRequest searchRequest = new SearchRequest(new String[]{EsContant.PRODUCT_INDEX},sourceBuilder);
        return searchRequest;
    }


    /**
     * 构建结果数据
     * 根据es查询到的结果,分析得到页面真正得到的数据模型
     * @param response
     * @param param
     * @return
     */
    private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {

        //要封装的大对象
        SearchResult result = new SearchResult();
        //1 封装返回的所有查询到的商品
        ArrayList<SkuEsModel> esModels = new ArrayList<>();
        SearchHits hits = response.getHits();
        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 当前所有商品涉及到的所有属性信息  Aggregation -> ParsedNested
        ArrayList<SearchResult.AttrVo> attrVos = new ArrayList<>();
        ParsedNested attr_agg = response.getAggregations().get("attr_agg");
        //nested的第一层 聚合  Aggregation -> ParsedLongTerms
        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();
            //得到属性id
            long attrId = bucket.getKeyAsNumber().longValue();
            attrVo.setAttrId(attrId);
            //子聚合 得到属性名 Aggregation -> ParsedStringTerms
            ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
            String attrName = attr_name_agg.getBuckets().get(0).getKeyAsString();//因为这个属性不是list
            attrVo.setAttrName(attrName);

            //子聚合 复杂 得到属性值 Aggregation -> ParsedStringTerms
            ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
            List<String> attrValues = attr_value_agg.getBuckets().stream().map((item) -> {   //因为这个属性是list
                return item.getKeyAsString();
            }).collect(Collectors.toList());
            attrVo.setAttrValue(attrValues);

            attrVos.add(attrVo);
        }
        result.setAttrs(attrVos);

        //3 当前所有商品所涉及的品牌信息 Aggregation ->ParsedLongTerms
        ArrayList<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();
            //得到品牌id
            long brandId = bucket.getKeyAsNumber().longValue();
            brandVo.setBrandId(brandId);

            //子聚合 得到品牌名 Aggregation -> ParsedStringTerms
            ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
            String brandName = brand_name_agg.getBuckets().get(0).getKeyAsString();//因为这个属性不是List
            brandVo.setBrandName(brandName);

            //子聚合 得到品牌图片
            ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
            String brandImg = brand_img_agg.getBuckets().get(0).getKeyAsString();//因为这个属性不是list
            brandVo.setBrandImg(brandImg);

            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);

        //4 当前所有商品所涉及到的所有分类信息 Aggregation -> ParsedLongTerms
        ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
        for (Terms.Bucket bucket : catalog_agg.getBuckets()) {
            //要封装的小对象
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            //得到分类id
            String keyAsString = bucket.getKeyAsString();
            catalogVo.setCatalogId(Long.parseLong(keyAsString));

            //子聚合 得到分类名 Aggregation -> ParsedStringTerms
            ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
            String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();//因为这个不是List
            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 % EsContant.PRODUCT_PAGESIZE == 0 ? (int)total/EsContant.PRODUCT_PAGESIZE:(int)(total/EsContant.PRODUCT_PAGESIZE+1));
      result.setTotalPages(totalPages);

        return result;
    }
}

image-20221202203301951

1.5.5 页面基本数据渲染

修改 list.html页面。

1)、页面商品展示

1669732023872

1669732083135

1669732226462

1669732326809

1669732382863

1669732660152

1669733273920

th:utext:不转义 ,此处使用是:可以让我们搜索的关键字高亮显示。

2)、品牌、分类等显示

1669733780249

效果:

1669733795992

1669734233498

效果:

1669734251149

显示全部:

修改代码:SearchParam

private Integer hasStock;//是否只显示有货

MallSearchServiceImpl -> buildSearchRequest

//1.2、bool -  filter  - 按照库存是否有进行查询
        if (param.getHasStock() != null){
            boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
        }

效果:

1669734506185

加粗:

1669734596736

  • 分类栏显示
 					<!--分类-->
					<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

1669735623970

1.5.6 页面筛选条件渲染

当我们选择比如品牌,分类,型号等自动拼接上参数。

函数:

   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

  使用函数:th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+brand.brandId+')'}"

测试:

1669736576069

分类:

1669737909309

 th:href="${'javascript:searchProducts(&quot;catalog3Id&quot;,'+catalog.catalogId+')'}"

其他属性:

1669737973606

th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"

1.5.7 页面分页数据渲染

1)、修改搜索导航
  • 实现搜索的时候地址栏加上关键字

1669787163213

1669787179361

搜索关键字:华为,地址栏加上华为。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xE3TM1fL-1673532011376)(null)]

回显搜索过的关键字:

1669790455925

1669790335463

        <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());
           }
2)、 分页调整

image-20221203220647049

从最开始的测试2改为16。

页面修改:

1669790726169

 							<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>&nbsp;&nbsp;到第</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

1669790836623

 /**
     * 构建结果数据
     *
     * @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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pZvwe5D-1673531997987)(谷粒商城项目笔记之高级篇/3019773-20221201095736162-135900037.png)]

1669791798247

1669791783574

thymeleaf中有关indexOf()方法,可以借助这个方法来判断是否包含某个字符串。

image-20221202202859212

1.5.8 页面排序功能

1669794740307

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

  • 按价格排序

1669794906503

1.5.9 页面排序字段回显

1669798477914

 <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>

效果:

1669798646828

1.5.10 页面价格区间搜索&&仅显示有货

  • 页面价格区间搜索

1669800573706

              <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

  • 仅显示有货

1669811235260

                                 <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

bug解决:之前搜索过的关键词在URL地址栏不会被替换,而是一直叠加。

function searchProducts(name, value) {
                //原来的页面
             location.href = replaceAndAddParamVal(location.href,name,value);
            }

1669811476426

1.5.11 面包屑导航

1)、准备及条件删除与URL编码问题

这里我们要使用远程调用查询属性。所以我们可以先引入feign这些。

  • 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
package com.atguigu.gulimall.search.feign;


/**
 * @author hxld
 * @create 2022-12-02 23:20
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {


    @GetMapping("/product/attr/info/{attrId}")
    public R attrInfo(@PathVariable("attrId") Long attrId);

}

主启动类添加调用远程服务注解

@EnableFeignClients //开启远程调用

image-20221203222802074

  • 新建 AttrResponseVo封装结果:这里我们暂时不用 gulimall-product下的 AttrRespVo了,可以将其放到公共服务中去。但是如果我们每个人只能修改自己负责的微服务,我们就新建然后进行封装就行。(最主要是因为我们使用远程调用查询商品信息,这个会给我们返回结果,我们可以使用将返回结果的类型放到公共服务中之后再来去调用,但是我们也可以在自己的微服务下进行封装,如果我们分工各做各的微服务)
package com.atguigu.gulimall.search.vo;

@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

package com.atguigu.gulimall.search.vo;

//面包屑导航数据
    private List<NavVo> navs;

    @Data
    public static class NavVo{
        private String navName;
        private String navValue;
        private String link;
    }

SearchParam

package com.atguigu.gulimall.search.vo;


private String _queryString;//原生的所有查询条件

SearchController

 package com.atguigu.gulimall.search.controller;

@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]));  //2
                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

<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

点 “ x” 地址栏消失属性。

1669816602449

2)、条件筛选联动

商品服务的 BrandController中添加获取品牌id集合的方法:

 package com.atguigu.gulimall.product.app;


@GetMapping("/infos")
    public R info(@RequestParam("brandIds") List<Long> brandIds) {
       List<BrandEntity> brand =  brandService.getBrandsByIds(brandIds);

        return R.ok().put("brand", brand);
    }

BrandServiceImpl

package com.atguigu.gulimall.product.service.impl;

@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

@Data
public class SearchResult {
    
	//面包屑导航数据
    private List<NavVo> navs = new ArrayList<>();
    private List<Long> attrIds = new ArrayList<>();
} 

MallSearchServiceImpl

因为我们经常使用编码的方法,所以提取成一个公共方法。

1669818473465

略做修改

    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;
    }

上一节的属性面包屑导航增加和修改一些代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OuITQITx-1673532011789)(null)]


        //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);

        }

对于品牌,分类的面包屑导航,这里暂时只做 品牌的。

  • 将product中vo包中的BrandVo复制一份到search中的vo包中
//品牌,分类
        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

                    <div class="JD_nav_logo" th:with="brandid= ${param.brandId}">
                        <!--品牌-->
                        <div th:if="${#strings.isEmpty(brandid)}" class="JD_nav_wrap">

1669821704171

<!--其他所有需要展示的属性-->
                        <div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds,attr.attrId)}">

测试

1669821917865

image-20221202203344262

&quot这个表示是字符串.

image-20221204180439804

1.6 商品详情

详情数据:

1669907504151

1.6.1 环境搭建

1)、修改Hosts

image-20221205200325115

加入 item.gulimall.com

2)、NGINX配置

1669897372758

我们可以看到,在以前我们已经配置过*.gulimamll.com这个域名配置了,所以这次可以不用在进行配置了。

3)、网关配置

image-20221206085539350

我们在网关中配置item.gulimall.com。

4)、动静资源设置

image-20221206090312205

将动态资源shangpinxiangqing.html这个页面改为item.html,然后将其复制到商品微服务中。

  1. 静态资源我们照样放在虚拟机中

image-20221206090618497

image-20221206090650042

我们新建item这个文件夹,然后将上面的资源放到文件夹中。

  1. 将静态资源的访问路径修改为正确的nginx下的路径。 href=" --- > href="/static/item/ || src=" --- > src="/static/item/

  2. 实现点击商品图片跳转到商品详情页

    • 最开始点击的时候跳转显示404页面,这个是因为跳转的逻辑没有写对
    • 我们右键图片,审查元素,修改搜索搜索微服务下面的list.html页面。 找到审查元素中的

image-20221206092210493

  • 将连接改为跳转到如下页面。

image-20221205202232267

image-20221206094839696

1.6.2 模型抽取

商品服务下新建 ,这里我们只做一些基本的,其他的比如说多少人预约,预约剩余这些我们暂时不做。

  • SkuItemVo
package com.atguigu.gulimall.product.vo;

@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
package com.atguigu.gulimall.product.vo;

@Data
@ToString
public class SkuItemSaleAttrVo {
    private Long attrId;
    private String attrName;
    private String attrValues;
}
  • SpuItemAttrGroupVo
package com.atguigu.gulimall.product.vo;

@Data
@ToString
public class SpuItemAttrGroupVo {
    private String groupName;
    private List<Attr> attrs;
}

1.6.3 规格参数

  • ItemController
@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

	@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` = #{catalogId} AND pav.`spu_id` = #{spuId}
    </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

  • GulimallProductApplicationTests 测试
@Autowired
    AttrGroupDao attrGroupDao;

    @Test
    public void test() {

        List<SpuItemAttrGroupVo> attrGroupWithAttrsBySpuId = attrGroupDao.getAttrGroupWithAttrsBySpuId(6L, 225L);
        System.out.println(attrGroupWithAttrsBySpuId);
    }

1669905673080

1.6.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

GulimallProductApplicationTests 测试:

 @Autowired
    SkuSaleAttrValueDao skuSaleAttrValueDao;

    @Test
    public void test() {
        List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueDao.getSaleAttrsBySpuId(7L);
        System.out.println(saleAttrVos);
    }

1669908066622

1.6.5 详情页渲染

  • 对item.html这个页面文件进行修改。

1669952027231

<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

						<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>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F3PeCz83-1673532073018)(null)]

                                 <li>
									<span th:text="${item.hasStock?'有货':'无货'}">无货</span>, 此商品暂时售完
								</li>

1669952717987

	<span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>

1669955432132

<li th:each="img : ${item.images}" th:if="${!#strings.isEmpty(img.imgUrl)}"><img th:src="${img.imgUrl}" /></li>

1669955909924

<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

<img class="xiaoguo" th:src="${descp}" th:each="descp:${#strings.listSplit(item.desp.decript,',')}"/>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C85QUSlB-1673532073510)(null)]

1669958712530

								<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

1.6.6 销售属性渲染

修改后台代码

1669974238774

  • 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

  • item.html

1669974520249

<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

1)、点击sku能够动态切换

实现点击 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

1.6.7 异步编排优化代码

①引入依赖:配置类可以有提示,这个可以配也可以不配。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

② 商品服务下新建 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

④商品服务下新建 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

package com.atguigu.gulimall.product.service.impl;

    @Autowired
    Executor executor;

    @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);

        //不需要返回,直接用异步run就行
        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            //2、sku的图片信息  pms_sku_images
            List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);


        //allOf()方法:返回一个新的 CompletableFuture,当所有给定的 CompletableFutures 完成时,该 CompletableFuture 就完成了。
        //等待所有任务都完成
        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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值