谷粒商城(商品上架、首页、异步、商品详细)思路详解

1.商品上架

1.sku模型分析

①第一种是方便检索,但是占用大量内存

②第二种把索引分开,原理就是sku一部分,spu的规格属性占一部分。如果要查询规格参数,那么就可以通过sku中的spuid来查询。这样的好处就是每次查询都不需要先占用大量的空间,spu的attr只占用一份空间。如果是上面那种方法的话方便检索重复的spu占用大量的空间。

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,  
                "doc_values": false 
            },
            "catalogName": {"type": "keyword" }, 
            "attrs": {
                "type": "nested",
                "properties": {
                    "attrId": {"type": "long"  },
                    "attrName": {
                        "type": "keyword",
                        "index": false,
                        "doc_values": false
                    },
                    "attrValue": {"type": "keyword" }
                }
            }
        }
    }
}

2.嵌入式

指的是nested这个属性值。为什么要有这个值?如果没有就有可能会造成数据的扁平化。那些相同属性的值会变成一个数组。导致最后查询的时候出现问题。

{
    "user":[
        {
         "first":xxx,
         "last":yyy
        },
        {
         "first":xxx,
         "last":yyy
        }
    ]
}
//扁平化处理之后就会变成
{
    user.first=["xxx","xxx"],
    user.last=["yyy","yyy"]
}

3.构造基本数据

思路

①根据json数据来封装对应的SkuEsModel数据对象,主要是用于保存到es中

②接着就是封装数据

③根据spuId查询到对应的sku信息然后copy到esModel中

④找出不同的信息然后进行查询

@Override
    public void up(Long spuId) {
        //1.根据spuId查询所有的sku信息
        List<SkuInfoEntity> skuInfoEntities=skuInfoService.getSkusBySpuId(spuId);
        //2.遍历list转换成SkuEsModel
        List<SkuEsModel> upSkuEsList = skuInfoEntities.stream().map(sku -> {
            SkuEsModel skuEsModel = new SkuEsModel();
            BeanUtils.copyProperties(sku, skuEsModel);
            //1.skuPrice和skuImg
            skuEsModel.setSkuPrice(sku.getPrice());
            skuEsModel.setSkuImg(sku.getSkuDefaultImg());
            //2.TODO 远程调用获取库存信息
            //3.TODO 热度评分
            //4.获取品牌和分类信息
            BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId());
            skuEsModel.setBrandImg(brandEntity.getLogo());
            skuEsModel.setBrandName(brandEntity.getName());

            //分类信息
            CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId());
            skuEsModel.setCatalogName(categoryEntity.getName());

            return skuEsModel;
        }).collect(Collectors.toList());


    }

4.检索属性

思路

①现根据spuId获取所有的规格属性,转换成规格属性的属性id集合

②然后就是在这些id集合中寻找能够被检索的属性id

③最后就是在原来找到的规格属性中进行过滤。

//4.TODO 搜索spu可以检索的属性
        //4.1查询所有的productAttr
        List<ProductAttrValueEntity> attrValueEntities=attrValueService.getAttrValueBySpuId(spuId);

        //4.2转化成对应的attr_id
        List<Long> attrIds = attrValueEntities.stream().map(item -> {
            return item.getAttrId();
        }).collect(Collectors.toList());
        //4.3通过AttrId查询可以检索的spu属性.
        List<Long> searchAttrIds=attrService.selectSearchAttr(attrIds);

        Set<Long> searchAttrIdSet=new HashSet<>(searchAttrIds);

        //4.4过滤attrValue中符合检索的属性
        List<SkuEsModel.Attr> attrs = attrValueEntities.stream().filter(item -> {
            return searchAttrIdSet.contains(item.getAttrId());
        }).map(item -> {
            SkuEsModel.Attr attr = new SkuEsModel.Attr();
            BeanUtils.copyProperties(item, attr);
            return attr;
        }).collect(Collectors.toList());

5.设置库存信息

思路

①其实就是根据现在查询到的skuids来查询他们是否有库存。

②通过SkuHasStockVo来保存skuid和库存信息。并且通过给R设置泛型来方便获取data

③把skuids通过map转换成SkuHasStockVo对象,还需要在遍历过程中通过id获取库存信息,set进vo对象。最后返回list。

④SpuInfo进行远程调用。

SpuInfoSer

//TODO 远程调用获取库存信息
        R<List<SkuHasStockVo>> rStock = wareFeignService.hassock(skuIds);

        List<SkuHasStockVo> skuHasStockVoList = rStock.getData();
        //转换成map,好处就是在下面遍历转换成esmodel的时候通过skuid获取库存信息。
        Map<Long, Boolean> skuStockMap = skuHasStockVoList.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));


sql语句

<select id="getStockBySkuId" resultType="java.lang.Integer">
        select sum(stock-stock_locked) from wms_ware_sku
        where sku_id=#{skuId}
    </select>

WareSkuSer

 @Override
    public List<SkuHasStockVo> getSkuHasStockBySkuId(List<Long> skuIds) {
        //id转换成volist
        List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
            SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
            Integer count=this.baseMapper.getStockBySkuId(skuId);
            skuHasStockVo.setSkuId(skuId);
            skuHasStockVo.setHasStock(count>0);
            return skuHasStockVo;
        }).collect(Collectors.toList());
        return collect;
    }

WareSkuCon

 @PostMapping("hasstock")
    public R<List<SkuHasStockVo>> hassock(@RequestBody List<Long> skuIds){
        List<SkuHasStockVo> data=wareSkuService.getSkuHasStockBySkuId(skuIds);

        R<List<SkuHasStockVo>> ok = R.ok();

        ok.setData(data);

        return ok;
    }

6.保存model到es中

思路

①先去到search模块中创建controller接口,productStatusUp

②传入modelList到service中处理。调用bulkReq,注入RestHighLevelClient。然后就是遍历modelList,转换成json格式存入indexReq中最后加入到bulk中。

③执行保存bulk

④如果发现其中出现错误,那么就展示这些item出来,打印错误日志。

⑤返回到elasticCon中,如果发现出现错误,那么就要返回错误信息。错误信息通过BizCodeEnum进行定义。

SpuInfoSer

@Override
    public void up(Long spuId) {

        //4.TODO 搜索spu可以检索的属性
        //4.1查询所有的productAttr
        List<ProductAttrValueEntity> attrValueEntities=attrValueService.getAttrValueBySpuId(spuId);

        //4.2转化成对应的attr_id
        List<Long> attrIds = attrValueEntities.stream().map(item -> {
            return item.getAttrId();
        }).collect(Collectors.toList());
        //4.3通过AttrId查询可以检索的spu属性.
        List<Long> searchAttrIds=attrService.selectSearchAttr(attrIds);

        Set<Long> searchAttrIdSet=new HashSet<>(searchAttrIds);

        //4.4过滤attrValue中符合检索的属性
        List<SkuEsModel.Attr> attrs = attrValueEntities.stream().filter(item -> {
            return searchAttrIdSet.contains(item.getAttrId());
        }).map(item -> {
            SkuEsModel.Attr attr = new SkuEsModel.Attr();
            BeanUtils.copyProperties(item, attr);
            return attr;
        }).collect(Collectors.toList());

        //1.根据spuId查询所有的sku信息
        List<SkuInfoEntity> skuInfoEntities=skuInfoService.getSkusBySpuId(spuId);
        List<Long> skuIds = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());


        //TODO 远程调用获取库存信息
        Map<Long, Boolean> skuStockMap=null;

        try{
            R<List<SkuHasStockVo>> rStock = wareFeignService.hassock(skuIds);

            List<SkuHasStockVo> skuHasStockVoList = rStock.getData();
            //转换成map
            skuStockMap = skuHasStockVoList.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
        }catch(Exception e){
            e.printStackTrace();
        }




        //2.遍历list转换成SkuEsModel
        Map<Long, Boolean> finalSkuStockMap = skuStockMap;
        List<SkuEsModel> upSkuEsList = skuInfoEntities.stream().map(sku -> {
            SkuEsModel skuEsModel = new SkuEsModel();
            BeanUtils.copyProperties(sku, skuEsModel);
            //1.skuPrice和skuImg
            skuEsModel.setSkuPrice(sku.getPrice());
            skuEsModel.setSkuImg(sku.getSkuDefaultImg());

            //3.TODO 热度评分
            //4.获取品牌和分类信息
            BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId());
            skuEsModel.setBrandImg(brandEntity.getLogo());
            skuEsModel.setBrandName(brandEntity.getName());
            //5.设置可以检索的属性信息
            skuEsModel.setAttrs(attrs);

            //6.设置库存信息


            if(finalSkuStockMap ==null){
                skuEsModel.setHasStock(finalSkuStockMap.get(skuEsModel.getSkuId()));
            }else{
                skuEsModel.setHasStock(false);
            }


            //分类信息
            CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId());
            skuEsModel.setCatalogName(categoryEntity.getName());

            return skuEsModel;
        }).collect(Collectors.toList());

        //3.保存esmodel
        R r = searchFeignService.productStatusUp(upSkuEsList);

        if(r.getCode()==0){
            //保存成功,更新商品状态
            this.baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
        }else{
            log.error("商品上架失败");
        }


    }

ElasticSaveCon

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

    @Autowired
    ElasticService elasticService;

    @PostMapping("product")
    public R productStatusUp(@RequestBody List<SkuEsModel> esModelList){
        boolean b=true;
        try{
            b= elasticService.productStatusUp(esModelList);
        }catch (Exception e){
            e.printStackTrace();
            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());
        }
    }
}

ElasticSaveSer

@Slf4j
@Service
public class ElasticSaveServiceImpl  implements ElasticService {

    @Autowired
    RestHighLevelClient client;


    @Override
    public boolean productStatusUp(List<SkuEsModel> esModelList) throws IOException {
        //批量保存
        BulkRequest bulkReq = new BulkRequest(EsConstant.PRODUCT_INDEX);
        //遍历list
        for (SkuEsModel skuEsModel : esModelList) {
            IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
            indexRequest.id(skuEsModel.getSkuId().toString());
            //转换成json字符串
            String skuEsJsonString = JSON.toJSONString(skuEsModel);
            indexRequest.source(skuEsJsonString, XContentType.JSON);
            bulkReq.add(indexRequest);
        }
        //保存
        BulkResponse bulk = client.bulk(bulkReq, GulimallElasticSearchConfig.COMMON_OPTIONS);

        //查看是否有错误。
        boolean hasFailures = bulk.hasFailures();
        if(hasFailures){
            List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
                return item.getId();
            }).collect(Collectors.toList());
            log.error("批量保存出现了问题,{}",collect);
        }

        return hasFailures;
    }
}

7.R的修改

原因

①如果发现R没有把data传过来的原因就是R是一个hashmap,要用key和value的形式来进行传递。

②可以利用object转换json,再利用typeReference接收类来转换正确的类类型。最后json->object

R

public <T> T  getData(TypeReference<T> tTypeReference){
		//接收对象类型,然后通过object->json->参考type->parseObjct->t
		Object data = get("data");
		String s = JSON.toJSONString(data);
		T t = JSON.parseObject(s, tTypeReference);
		return t;
	}


	public R setData(Object data){
		put("data",data);
		return this;
	}

2.首页

1.新建页面

思路

①引入thymeleaf的驱动,关闭缓存。

②创建web包和修改controller->app包。

③html静态资源放到static中,首页页面放到template中

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  thymeleaf:
    cache: false

2.获取一级目录。

思路

①直接根据cid=0获取对应的一级目录

②处理首页的跳转路径问题。访问index.html或者是空都可以直接访问首页

拓展

①默认的prefix是classpath:templates/,默认suffix是.html

②跳转的视图路径是通过mvc进行的处理,最后到达页面。

@Controller
public class IndexController {

    @Autowired
    CategoryService categoryService;

    @GetMapping({"/","index.html"})
    public String getLevel1(Model model){
       List<CategoryEntity> data=categoryService.getLevel1Categorys();
        model.addAttribute("categorys",data);
        return "index";
    }
}

 <ul>
            <li th:each="category:${categorys}">
              <a href="#" class="header_main_left_a" ctg-data="3" th:attr="ctg-data=${category.catId}"><b th:text="${category.getName()}">家用电器</b></a>
            </li>


          </ul>

3.获取整个目录

思路

①获取一级目录

②根据一级目录映射成map,catId作为key,catelog2List作为value,根据一级目录的id查询对应catelog2List,然后封装成catelog2VoList,然后就是遍历catelog2VoList,根据id获取三级目录,最后set进去。

③还需要修改catelogLoader.js的访问路径。

总结:关键就是创建Catelog2Vo对象,内部有3级目录。如果是多级的话,那么这种结构是很难实现的,可以通过递归的方式来创建这样的对象,然后不断的填充内部的List。

IndexCon

@ResponseBody
    @GetMapping("index/catalog.json")
    public Map<String, List<Catelog2Vo>> getCatelog(){
        Map<String, List<Catelog2Vo>> map=categoryService.getCatelogList();
        return map;
    }

CatelogSer

@Override
    public Map<String, List<Catelog2Vo>> getCatelogList() {
        //1.查询一级分类
        List<CategoryEntity> level1Categorys = this.getLevel1Categorys();

        //2.查询一级分类中的二级分类并且变成map格式
        Map<String, List<Catelog2Vo>> map = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //2.1查询二级分类List
            List<CategoryEntity> catelog2List = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
            //2.1转换成Vo对象
            List<Catelog2Vo> catelog2VoList = catelog2List.stream().map(item -> {

                Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());

                //2.3查询3级分类对象。并且set进去。
                List<CategoryEntity> catelog3List = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", item.getCatId()));
                List<Catelog2Vo.Catelog3Vo> catelog3VoList = catelog3List.stream().map(catelog3 -> {
                    Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(item.getCatId().toString(), catelog3.getCatId().toString(), catelog3.getName());
                    return catelog3Vo;
                }).collect(Collectors.toList());

                catelog2Vo.setCatelog3List(catelog3VoList);
                return catelog2Vo;
            }).collect(Collectors.toList());

            return catelog2VoList;
        }));
        return map;
    }

4.nginx反向代理服务器

概述

意思就是nginx有两种方式。第一种是正向代理,其实就是隐藏客户端的信息,客户端通过nginx来获取服务器的信息。nginx相当于就是做了中介。反向代理其实就是负责为服务器接收请求,然后重新通过网关交给服务器,而不是让客户端直接访问服务器。增加了服务器的安全性。

配置思路

①先通过hosts文件配置对应的域名与虚拟机ip,访问域名能够直接访问虚拟机。

②然后就是修改nginx中http模块的server模块,主要是监听gulimall.com,然后就是反向代理我们的10000产品服务器。

③测试

nginx的原理

我们访问域名,相当于在访问虚拟机,这个时候nginx在监听这个域名,那么就会执行反向代理,给服务器发送请求,获取资源。

5.nginx通过网关访问首页

思路

①配置nginx.conf文件的upstream访问网关,server名为gulimall

②配置conf.d下面的gulimall.conf文件直接访问上游的server网关,并且加上Host头部

③配置gateway服务,如果发现host是gulimall.com那么就跳转到gulimall-product服务上处理。

拓展

①gateway的检测host一定要放到后面,如果放到前面每次访问gulimall.com的时候都会跳转到product先,导致后面的匹配是不起作用的。

②nginx每次发送请求都会把host去除,所以在发送之前需要加上host。然后交给网关进行负载均衡的处理。

3.检索服务

1.配置检索页面

思路

①创建/static/search文件在nginx,并且把静态页面放进去。

②修改nginx配置,server:*.gulimall.com

③修改网关配置,主要就是对应的sear.gulimall.com分配到search服务

④转移页面到search。并且修改路径。

拓展与坑

①json数据是catalog而不是catelog,改错可能会显示不出list

2.修改检索页面

思路

①修改页面的路径跳转到首页

②修改首页的分类点击,写一个Controller跳转到检索页面list.html

③修改搜索按钮,通过javascrpt:方法来处理

<a href="javascript:search();" ><img src="/static/index/img/img_09.png"  /></a>

3.抽取页面的Result主体

思路

①分类栏

②下面的几个属性栏

③还有就是产品集合,和分页数据

SearchResult

@Data
public class SearchResult {
    private List<SkuEsModel> products;

    private Long total;//总数

    private Integer totalPages;//总页数

    private List<BrandVo> brands;//品牌信息

    private List<AttrVo> attrs;//属性信息

    private List<CatalogVo> catalogs;//分类信息




    @Data
    public static class CatalogVo{
        private Long catalogId;
        private String catalogName;
    }

    @Data
    public static class BrandVo{
        private Long brandId;
        private String brandName;
        private String brandImg;
    }
    @Data
    public static class AttrVo{
        private Long attrId;
        private String attrName;
        private List<String> attrValue;
    }
}

4.检索测试

分类和品牌查询,通过filter->term或者是terms来查询对应的数据。如果是查询商品的名字可以用must->match来进行查询

GET product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
          "skuTitle": "华为"
           }
        }
      ],
      "filter": [
        {
          "term":{
             "catalogId":"225"
          }
        },
        {
          "terms":{
            "brandId":["1","2","9"]
          }
        }
      ]
    }
  }
}

查询attrs里面的元素,要用到嵌入式查询。nested,指定path也就是attrs,然后下面的query和平时的查询一样

GET product/_search
{
  "query": {
     "bool": {
       "filter": {
         "nested": {
           "path": "attrs",
           "query": {
             "bool": {
               "must": [
                 {
                   "term": {
                     "attrs.attrId": {
                       "value": "11"
                     }
                   }
                 }
               ]
             }
           }
         }
       }
     }
  }
} 

模糊查询用must+match,后面那些需要精准匹配就用filter->term。还有价格范围可以使用filter-range。分页可以使用from 和size,最后就是模糊匹配成功的加上高亮和前后缀,hightlight

GET product/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "skuTitle": {
            "value": "华为"
          }
        }}
      ]
      ,
      "filter": [
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "11"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "term": {
            "hasStock": false
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 0,
              "lte": 200
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from":0,
  "size":2,
  "highlight": {
    "fields": {"skuTitle": {}},
    "pre_tags": "<",
    "post_tags": ">"
  }
}

5.聚合数据

为什么需要聚合?

①需要有各种分类信息,品牌信息。还有属性以及属性值。这些都是需要通过聚合才能单独获取

②比如分类,有多少种?品牌有多少个?属性有多少个?属性里面有多少个属性值?这些都是页面上需要聚合显示的条件查询信息。

GET gulimall_product/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": {
        "brand_name_agg": {
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
        "brand_img_agg":{
           "terms": {
             "field": "brandImg",
             "size": 10
           }
        }
      }
    },
    "catalog_agg":{
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalog_name_agg": {
          "terms": {
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg":{
       "nested": {
         "path": "attrs"
       },
       "aggs": {
         "attr_id_agg": {
           "terms": {
             "field": "attrs.attrId",
             "size": 10
           },
           "aggs": {
             "attr_name_agg": {
               "terms": {
                 "field": "attrs.attrName",
                 "size": 10
               }
             },
             "attr_value_agg":{
                "terms": {
                  "field": "attrs.attrValue",
                  "size": 10
                }
             }
           }
         }
       }
    }
  }
}

6.模糊查询(商品名称),过滤(属性、属性值、品牌、分类)

思路

①根据json字符串来进行一步步嵌套。主要的思路就是先从SearchSourceBuilder语句构建开始,然后开始构建boolQuery,紧接着就是加入各种termQuery

②比较难的就是这个价格区间,只需要split,然后对字符串进行一个判断,到底是2个还是1个。开头是_还是说结尾是_ _ 进行对应的处理

③还有就是属性,需要创建nest查询,并且在nest里面还需要有一个bool查询,也就是bool->nest->nestbool

 private SearchRequest buildSearchRequest(SearchParam param) {
        //创建普通查询
        SearchSourceBuilder builder = new SearchSourceBuilder();
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();


        //1.skuTile
        String keyword = param.getKeyword();
        if(!StringUtils.isEmpty(keyword)){
             boolQuery.must(QueryBuilders.matchQuery("skuTitle",keyword));
        }
        //2.catalog3Id也就是某个分类param.getCatalog3Id()
        Long catalog3Id = param.getCatalog3Id();
        if(catalog3Id!=null){
            boolQuery.filter(QueryBuilders.termQuery("catalogId",catalog3Id));
        }

        //3.库存
        Integer hasStock = param.getHasStock();
        boolQuery.filter(QueryBuilders.termQuery("hasStock",hasStock==1));

        //4.价格区间
        String skuPrice = param.getSkuPrice();
        if(!StringUtils.isEmpty(skuPrice)){
            //先分开数字和key:value
            String[] s = skuPrice.split("_");
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
            //如果长度是2,那么可以设定正常区间
            if(s.length==2){
                rangeQuery.gte(s[0]).lte(s[1]);
            }else if(s.length==1){
                //如果是1那么就设定一个区间即可
                if(skuPrice.startsWith("_")){
                    rangeQuery.lte(s[0]);
                }
                if(skuPrice.endsWith("_")){
                    rangeQuery.gte(s[0]);
                }
            }
            boolQuery.filter(rangeQuery);
        }

        //5.品牌id
        List<Long> brandIds = param.getBrandIds();
        if(brandIds!=null&&brandIds.size()>0){
            boolQuery.filter(QueryBuilders.termsQuery("brandId",brandIds));
        }

        //6.属性处理
        List<String> attrs = param.getAttrs();
        if(attrs!=null&&attrs.size()>0){

            for (String attr : attrs){
                BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();

                //先查看id
                String[] s = attr.split("_");
                String attrId=s[0];
                //接着就是拆出所有的value值,并且拿去进行匹配操作
                String[] attrValues = s[1].split(":");

                //每一行都需要进行一次属性的嵌套查询
                nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
                nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));

                //构建nest查询
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
                boolQuery.filter(nestedQuery);

            }
        }


        builder.query(boolQuery);
//        SearchRequest searchRequest = new SearchRequest(EsConstant.PRODUCT_INDEX,builder);
        return null;
    }

7.排序、分页和高亮

思路

①sort先拆开,然后s[0]作为要排序的字段, 后面那个判断是asc还是desc来决定用什么order

②分页,from和size。设置一个pagesize在constant中。from就是(pageNum-1)*pageSize

③高亮只需要判断keyword,然后new一个Builder放进去就可以了

 private SearchRequest buildSearchRequest(SearchParam param) {
        //创建普通查询
        SearchSourceBuilder builder = new SearchSourceBuilder();
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();


        //1.skuTile
        String keyword = param.getKeyword();
        if(!StringUtils.isEmpty(keyword)){
             boolQuery.must(QueryBuilders.matchQuery("skuTitle",keyword));
        }
        //2.catalog3Id也就是某个分类param.getCatalog3Id()
        Long catalog3Id = param.getCatalog3Id();
        if(catalog3Id!=null){
            boolQuery.filter(QueryBuilders.termQuery("catalogId",catalog3Id));
        }

        //3.库存
        Integer hasStock = param.getHasStock();
        if(hasStock!=null){
            boolQuery.filter(QueryBuilders.termQuery("hasStock",hasStock==1));
        }


        //4.价格区间
        String skuPrice = param.getSkuPrice();
        if(!StringUtils.isEmpty(skuPrice)){
            //先分开数字和key:value
            String[] s = skuPrice.split("_");
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
            //如果长度是2,那么可以设定正常区间
            if(s.length==2){
                rangeQuery.gte(s[0]).lte(s[1]);
            }else if(s.length==1){
                //如果是1那么就设定一个区间即可
                if(skuPrice.startsWith("_")){
                    rangeQuery.lte(s[0]);
                }
                if(skuPrice.endsWith("_")){
                    rangeQuery.gte(s[0]);
                }
            }
            boolQuery.filter(rangeQuery);
        }

        //5.品牌id
        List<Long> brandIds = param.getBrandIds();
        if(brandIds!=null&&brandIds.size()>0){
            boolQuery.filter(QueryBuilders.termsQuery("brandId",brandIds));
        }

        //6.属性处理
        List<String> attrs = param.getAttrs();
        if(attrs!=null&&attrs.size()>0){

            for (String attr : attrs){
                BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();

                //先查看id
                String[] s = attr.split("_");
                String attrId=s[0];
                //接着就是拆出所有的value值,并且拿去进行匹配操作
                String[] attrValues = s[1].split(":");

                //每一行都需要进行一次属性的嵌套查询
                nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
                nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));

                //构建nest查询
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
                boolQuery.filter(nestedQuery);

            }
        }


        //排序,分页和高亮操作

        //1.排序
        String sort = param.getSort();
        if(!StringUtils.isEmpty(sort)){
            String[] s = sort.split("_");
            SortOrder sortOrder=s[1].equalsIgnoreCase("asc")?SortOrder.ASC:SortOrder.DESC;
            builder.sort(s[0],sortOrder);
        }
        //2.分页拼接
        builder.from((param.getPageNum()-1)*EsConstant.PAGE_SIZE);

        builder.size(EsConstant.PAGE_SIZE);

        //3.高光
        if(!StringUtils.isEmpty(keyword)){
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            builder.highlighter(highlightBuilder);
        }


        builder.query(boolQuery);



        System.out.println(builder.toString());

        SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},builder);



        return null;
    }

8.封装结果集

思路

①必须来了解和清晰整个结果的结构。

②SkuEsModel的数据放在了hits中的hits

③通过pageNum和totalHits来封装分页属性

④然后就是逐一遍历聚合,思路就是一边debug,一边参考聚合的类型强转,主要是拿到bucket中的值,或者是bucket中aggregation再次获取buckets。只有一个·值的时候就是get(0);而且可以直接转换成long类型或者是sting类型,主要是获取key类型的值。

⑤最后就是修改高亮的部分,高亮通常是模糊查询才会出现的text,所以必须出现进行模糊查询的keyword才能够看到hits部分的高亮。然后取出这部分text并set进这个SkuEsModel中去。

 private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {
        SearchResult result = new SearchResult();
        //1.分析result
        SearchHits hits = response.getHits();
        //2.商品
        SearchHit[] hitsProducts = hits.getHits();
        List<SkuEsModel> products=new ArrayList<>();
        for (SearchHit hit : hitsProducts) {
            //获取命中的所有product
            String sourceAsString = hit.getSourceAsString();
            SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
            if(!StringUtils.isEmpty(param.getKeyword())){
                HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                String highlightText= skuTitle.getFragments()[0].toString();
                skuEsModel.setSkuTitle(highlightText);

            }
            products.add(skuEsModel);
        }
        result.setProducts(products);

        //4.分类
        List<SearchResult.CatalogVo> catalogVos=new ArrayList<>();
        ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
        List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            long catalogId = bucket.getKeyAsNumber().longValue();
            String catalogName = ((ParsedStringTerms) bucket.getAggregations().get("catalog_name_agg")).getBuckets().get(0).getKeyAsString();

            catalogVo.setCatalogId(catalogId);
            catalogVo.setCatalogName(catalogName);

            catalogVos.add(catalogVo);
        }
        result.setCatalogs(catalogVos);

        //3.品牌
        List<SearchResult.BrandVo> brandVos=new ArrayList<>();
        ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
        for (Terms.Bucket bucket : brand_agg.getBuckets()) {
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            long brandId = bucket.getKeyAsNumber().longValue();
            String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
            String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
            brandVo.setBrandId(brandId);
            brandVo.setBrandImg(brandImg);
            brandVo.setBrandName(brandName);

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

        //4.属性
        ParsedNested attr_agg = response.getAggregations().get("attr_agg");
        ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
        List<SearchResult.AttrVo> attrVos=new ArrayList<>();
        for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            long attrId = bucket.getKeyAsNumber().longValue();
            //获取name和value
            String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
            List<String> attrValueList = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
                String keyAsString = item.getKeyAsString();
                return keyAsString;
            }).collect(Collectors.toList());

            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            attrVo.setAttrValue(attrValueList);

            attrVos.add(attrVo);

        }
        result.setAttrs(attrVos);


        long total = hits.getTotalHits().value;
        //5.页码
        result.setPageNum(param.getPageNum());
        //6.总记录数
        result.setTotal(total);
        //7.总页码
        result.setTotalPages(total%EsConstant.PAGE_SIZE==0?total/EsConstant.PAGE_SIZE:total/EsConstant.PAGE_SIZE+1);
        return result;
    }

9.页面筛选条件渲染

思路

①给分类、品牌和属性都添上点击方法,如果点击之后能够加上参数然后重新更新网页。

②遍历分类品牌和属性,思路基本上就是从product里面获取信息,然后遍历通过thymeleaf来解决渲染问题

拓展与坑

①注意要通过‘& quot;’来代替",用于修饰字符串不然最后出现的1_11会变成111

②接着就是要传入javascript方法在th:href=,需要${‘javascript:xxxx()’}这样的格式来进行传输。

示例

//品牌

<li th:each="brand:${result.brands}">
                                    <a href="/static/search/#"
                                       th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+brand.brandId+')'}"
                                    >
                                        <img th:src="${brand.brandImg}" alt="">
                                        <div th:text="${brand.brandName}">
                                            华为(HUAWEI)
                                        </div>
                                    </a>
                                </li>

//分类
 <ul>
                            <li th:each="catalog:${result.catalogs}">
                                <a href="/static/search/#"
                                   th:href="${'javascript:searchProducts(&quot;catalogId&quot;,'+catalog.catalogId+')'}"
                                   th:text="${catalog.catalogName}">5.56英寸及以上</a></li>
                        </ul>
<a href="/static/search/#"
                                   th:href="${'javascript:searchProducts(&quot;attr&quot;,'+attr.attrId+'_'+attr.attrValue+')'}"
                                   th:text="${val}">5.56英寸及以上</a></li>



function searchProducts(key,value) {
        var href=location.href+"";
        if(href.indexOf("?")!=-1){
            location.href=location.href+"&"+key+"="+value;
        }else{
            location.href=location.href+"?"+key+"="+value;
        }
    }

10.分页数据渲染

思路

①解决搜索栏的keyword搜索,还是调用上一次的添加属性的方法。

②然后就是解决上一页和页栏和下一页的问题。他们都需要一个属性pn来维护自己属于的一个页码,比如上一页应该就是pageNum-1,下一页的就是pageNum+1.然后页栏通过pageNav来进行维护pn。而且需要给他们加上一个class。然后点击调用方法跳转页面,其实就是给路径加上一个参数。location.href=url加工。本质上无论是上一页下一页还是说页栏都是通过维护自己的pn,也就是跳转的页面数值,来进行跳转。最后就是我们通过获取点击对象的值,并且加工url就能够进行跳转。

拓展

①style如果需要根据数值进行改变可以通过th:attr="style=${数值比较?‘style的值1’:‘style的值2’}"来进行修改。

②分页需要了解的知识(1)页栏可以通过后端list根据totalPages来进行维护(2)可以通过获取所有的对象总数和每页大小total%pageSize==0?total/pageSize:total/pageSize+1这样来进行一个计算总页数(3)需要通过th:if来判断上一页与下一页是否需要出现的合法性(4)后端需要提供pageNum和total、totalPages。数据用于维护分页条。

分页条

 <span class="page_span1">
                                <a class="page_a"
                                   th:if="${result.pageNum>1}"
                                   th:attr="pn=${result.pageNum+1}"
                                   href="/static/search/#">
                                    < 上一页
                                </a>
                                <a class="page_a"
                                   th:each="nav:${result.pageNavs}"
                                   th:attr="pn=${nav},style=${nav==result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
                                    style="border: 0;color:#ee2222;background: #fff">[[${nav}]]</a>

                                <a class="page_a"
                                   th:if="${result.pageNum<result.totalPages}"
                                   th:attr="pn=${result.pageNum-1}"
                                  >
                                    下一页 >
                                </a>
                            </span>

点击事件和跳转方法(location.href修改并跳转)

//给page_a进行一个点击事件
    $(".page_a").click(function () {
        var pn =$(this).attr("pn");
        location.href=replaceParamVal(location.href,"pageNum",pn,false);
    })



    function replaceParamVal(url, paramName, replaceVal,forceAdd) {
        var oUrl = url.toString();
        var nUrl;
        if (oUrl.indexOf(paramName) != -1) {
            if( forceAdd && oUrl.indexOf(paramName+"="+replaceVal)==-1) {
                if (oUrl.indexOf("?") != -1) {
                    nUrl = oUrl + "&" + paramName + "=" + replaceVal;
                } else {
                    nUrl = oUrl + "?" + paramName + "=" + replaceVal;
                }
            } else {
                var re = eval('/(' + paramName + '=)([^&]*)/gi');
                nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
            }
        } else {
            if (oUrl.indexOf("?") != -1) {
                nUrl = oUrl + "&" + paramName + "=" + replaceVal;
            } else {
                nUrl = oUrl + "?" + paramName + "=" + replaceVal;
            }
        }
        return nUrl;
    };

11.页面排序功能

思路

①需求:点击排序之后可以有上下箭头提醒,并且能够高亮

(1)从高亮开始,其实就是修改css样式,逻辑就是点击之后能够触发事件,可以通过class属性来触发点击事件,然后先初始化所有的排序按钮,然后就是给点击的按钮添加上颜色。

(2)箭头首先就是要遍历所有的节点,清除所有箭头,然后toggleClass给当前点击元素加上或者去掉desc 的class。然后根据这个来判断是倒序还是升序。如果有desc这个class,那么先清除箭头,然后加上↓这个箭头。反之也是。相对来说前端逻辑和api会比较难一点,需要多总结和思考》

②跳转页面

(1)给三个排序加上sort属性

(2)点击事件获取sort属性,并且判断是否有desc来加上后缀_desc或者是_asc

拓展与采坑

①判断元素是否有class,$(xx).hasClass

②toggleClass就像是开关,点击一次就加上,再点击就去掉

③如果要为字符串后面加上什么,为了防止重复加上,可以选择先replace来进行清空操作,然后再+上

④如果要跳转到某个页面那么就可以修改location.href.

⑤为了阻止a跳转可以在点击方法中加上return false。(好像是错的)

⑥修改css样式,可以通过each来遍历清空class的css再修改对应元素的。

$(".sort_a").click(function () {

        //1.修改样式
        changeStyle(this);
        //2.跳转页面,判断是desc还是asc,先获取sort
        var sort=$(this).attr("sort");
        sort=$(this).hasClass("desc")?sort+"_desc":sort+"_asc";
        location.href=replaceParamVal(location.href,"sort",sort)
        //能够防止跳转页面
        return false;
    })

    function changeStyle(ele) {

         $(".sort_a").css({"background":"#FFF" ,"color":"#333","border-color":"#CCC"})
         //遍历并且修改这个位置
         $(".sort_a").each(function () {
              //修改text有↑,有↓
             var text=$(this).text().replace("↑","").replace("↓","");
            $(this).text(text);
         });


         //修改箭头
        $(ele).css({"color":"#FFF","border-color":"#e4393c","background":"#e4393c"})


        $(ele).toggleClass("desc")

        if($(ele).hasClass("desc")){
            var text=$(ele).text().replace("↓","").replace("↑","")

            text+="↓";
            $(ele).text(text);
        }else{
            var text=$(ele).text().replace("↓","").replace("↑","")
            text+="↑";
            $(ele).text(text);
        }
    }

12.排序页面回显

思路

①需求与问题:点击排序之后会跳转页面,并且会清除class,和样式设计。如何动态改变点击排序的之后的css?

思路其实就是利用我们传输的参数param.sort的值,通过这个值的前缀和后缀能够判断哪个需要高亮。后缀能够进行判断箭头方向。第一个问题就是怎么修改样式?可以通过th:attr="style=${判断? xx:yy}"这样来进行style的一个设置。判断的依据就是sort的前缀是什么排序。还有一个就是关于class的修改,首先是sort是否空,然后就是判断排序类型,最后判断的就是后缀,后缀决定最后箭头的方向。这里会有一个问题那就是如何切换上下箭头?如果点击一次之后加上了class,那么第二次点击由应该依据什么?还是依据class,并且使用toggleClass把desc当做开关那样,点击一次开,再点就关,这样就可以根据class来组合最后sort的值。因为在刷新页面之前会对sort进行一次拼接,拼接的依据就是class的值,前面已经给class进行对参数判断的添加,现在只需要通过toggle来修改class的样式,就能动态改变参数。

总结:主要通过参数可以跳转传递,但是样式和class不能,那么就只能通过参数进行判断。前缀是排序类型,后缀排序是从上到下还是从下到上。

拓展与思考

KaTeX parse error: Expected '}', got '#' at position 2: {#̲strings.isEmpty…{#strings.startsWith(x,‘y’)}前缀判断,后缀相似。

③可移植的思路:解决需求点击页面跳转->样式需要改变。(抓住参数和class的变化。和各种字符串判断的api)

<div class="filter_top_left" th:with="p=${param.sort}">
                            <a class="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;background:#FFF;border-color:#CCC;'}"
                            >综合排序[[${(!#strings.isEmpty(p)&&#strings.startsWith(p,'hotScore')&&#strings.endsWith(p,'desc'))?'↓':'↑'}]]</a>
                            <a class="sort_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;background:#FFF;border-color:#CCC;'}"
                            >销量[[${(!#strings.isEmpty(p)&&#strings.startsWith(p,'saleCount')&&#strings.endsWith(p,'desc'))?'↓':'↑'}]]</a>
                            <a class="sort_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;background:#FFF;border-color:#CCC;'}"
                            >价格[[${(!#strings.isEmpty(p)&&#strings.startsWith(p,'skuPrice')&&#strings.endsWith(p,'desc'))?'↓':'↑'}]]</a>
                            <a class="sort_a" sort="hotScore">评论分</a>
                            <a class="sort_a" sort="hotScore">上架时间</a>
                        </div>

13.价格区间和是否有货

思路

需求:查询价格区间和是否有货

①查询价格区间首先是开两input记录对应的skuPrice,然后后面一个按钮。绑定事件,拼接两个price。然后就是拼接到字符串。如果需要回显价格数据,绑定参数,然后判空,获取前半段或者是后半段。

②是否有货可以通过一个checkbox来解决。并且来个change方法,点击之后就进行路径修改。

 <input id="skuPriceFrom" th:value="${(#strings.isEmpty(skuRange))?'':#strings.substringBefore(skuRange,'_')}"  type="number" style="margin-left: 30px;width: 100px">-
                            <input id="skuPriceTo" th:value="${(#strings.isEmpty(skuRange))?'':#strings.substringAfter(skuRange,'_')}" type="number" style="width: 100px"> <button id="skuPriceBtn">确定</button>
                        
 $("#skuPriceBtn").click(function () {
         var from =$("#skuPriceFrom").val();
         var to=$("#skuPriceTo").val();

         var range=from+"_"+to;
         location.href=replaceParamVal(location.href,"skuPrice",range);
    })


    $("#hasStock").change(function () {
         var check=$(this).prop("checked");

         if(check){
             //被选中加上这个参数
             location.href=replaceParamVal(location.href,"hasStock",1);
         }else{
             //没被选中直接删除操作
//没选中
             var re = eval('/(hasStock=)([^&]*)/gi');
             location.href = (location.href+"").replace(re,'');
         }
    })
 <li th:with="check=${param.hasStock}">
<!--                                    如果是1那么就选中-->

                                    <a href="/static/search/#">
                                        <input th:checked="${#strings.equals(check,'1')}" id="hasStock" type="checkbox">
                                        货到付款
                                        </a>
                                </li>

13.封装面包屑

思路

①创建一个NavVo面包屑,attrName对上attrValue还有一个link

②遍历参数attrs,并且远程查询attr的name封装到面包屑对象中。

③取出attr,并且encode成utf-8。替换变成+的空格为%20字符

④接着就是取出所有参数string,然后每次遍历到的attr就换掉这个attr的参数为空。并且生成link放到navVo中。

  //9.封装面包屑

        if(param.getAttrs()!=null&&param.getAttrs().size()>0){
            List<SearchResult.NavVo> navVos = param.getAttrs().stream().map(attr -> {
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                //9.1分割属性id和属性值
                String[] s = attr.split("_");
                navVo.setNavValue(s[1]);
                Long attrId = Long.parseLong(s[0]);
                //9.2远程调用返回属性的值
                R r = productFeignService.info(attrId);
                if (r.getCode() == 0) {
                    //换成json
                    AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
                    });
                    navVo.setNavName(data.getAttrName());
                }else{
                    navVo.setNavName(attrId.toString());
                }
                //9.3根据attrs的值进行遍历,并且删除自己的那部分形成link

                String queryString = param.get_queryString();
                String encode=null;
                try {
                    encode = URLEncoder.encode(attr, "UTF-8");
                    encode=encode.replace("+","%20");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                String replace = queryString.replace("&attrs=" + encode, "");

                navVo.setLink("http://search.gulimall.com/list.html"+(replace.isEmpty()?"":"?"+replace));
                return navVo;
            }).collect(Collectors.toList());
            result.setNavs(navVos);
        }

14.品牌的面包屑

思路

①先去封装获取的面包屑,远程调用获取品牌信息,封装进面包屑

②替换掉brandIds。

③去到前端页面获取param并判断是否存在商品id,如果存在那么就显示

④关于属性显示就是加上我们参数中的属性的id,如果发现遍历展现聚合的属性过程中有对应的属性id,那么就把这个属性的模块给移除

拓展

①通过th:with="xx=${param}"来获取参数

②th:if用来判断

③${#lists.contains}可以用来判断集合里面是否存在某些元素。

private Long brandId;
	/**
	 * 品牌名
	 */
	@NotBlank(message = "名字不能为空",groups = {UpdateGroup.class,AddGroup.class})
	private String name;
	/**
	 * 品牌logo地址
	 */
	@URL(message = "不符合url规则",groups = {AddGroup.class,UpdateGroup.class})
	@NotEmpty(message = "不能为空",groups = {AddGroup.class})
	private String logo;
	/**
	 * 介绍
	 */
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	//在updateStatus下需要验证数据
	@ListValue(value={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@Pattern(regexp = "^[a-zA-Z]$",message = "只能是一个字母",groups = {AddGroup.class,UpdateGroup.class})
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull(message = "sort不能为空",groups =AddGroup.class )
	@Min(value = 0,message = "只能是>0的整数",groups = {AddGroup.class,UpdateGroup.class})
	private Integer sort;

4.异步

1.线程复习

①Callable+FutureTask有返回值

②Runable确定线程任务

③Thread内部实现了Runable,可以自己重写run方法。

④Executors.newFixedThreadPool()能够创建线程池,防止应用创建过多线程。

public class ThreadTest {
    public static ExecutorService service=Executors.newFixedThreadPool(10);


    public static void main(String[] args) throws ExecutionException, InterruptedException {
//        new Thread01().start();
//        new Thread(new Runable01()).start();
//        FutureTask<Integer> integerFutureTask = new FutureTask<>(new Callable01());
//        Thread thread = new Thread(integerFutureTask);
//        //一定需要执行任务才能返回值
//        thread.start();
//        System.out.println(integerFutureTask.get());
        Integer q = service.submit(() -> {
            System.out.println("好的");
            return 1;
        }).get();
        System.out.println(q);
        System.out.println("未来");
    }

    public static class Callable01 implements Callable<Integer>{

        @Override
        public Integer call() throws Exception {
            System.out.println("返回数值");
            return 2;
        }
    }

    public static class  Runable01 implements  Runnable{
        @Override
        public void run() {
            System.out.println("好");
        }
    }

    public static class  Thread01 extends Thread{
        @Override
        public void run() {
            System.out.println("好?");
        }
    }

}

2.线程池详解

变量

corePoolSize:核心线程,主要执行的线程个数

maximumPoolSize:最多线程个数,主要是核心线程满了,队列满的时候会开启线程

unit:时间单位

Queue:阻塞队列类型

ThreadFactory:线程创建工厂

handler:拒绝策略

keepAliveTime:线程可以存活的时间

1.任务来了,先去到核心线程完成

2.核心线程满了放到阻塞队列

3.阻塞队列满了,开启新的线程直到max

4.max也满了,handler开始执行拒绝任务策略

5.max-core的线程如果执行任务完成之后,就会通过keepAliveTime来释放内存

6.Cache只有最大值,没有核心线程,fixed只有固定的核心线程,Schedule定时任务线程。Single单线程任务。

3.异步编排CompletableFuture

解决问题:不同任务的同时执行和异步执行。

测试两种不同的实现方式(返回的都是Future对象。如果是Supplier接口的话就能够通过future来返回数据)

 public static void main(String[] args) throws ExecutionException, InterruptedException {
//        CompletableFuture<Void> good = CompletableFuture.runAsync(() -> {
//            System.out.println("好人");
//        }, service);

        CompletableFuture<Integer> numberFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("测试一下");
            Integer number = 1;
            return number;
        }, service);
        System.out.println(numberFuture.get());


    }

感知异常

①whencomplete不能够返回值。

②但是exceptionally可以返回值。并且感知异常。

 CompletableFuture<Integer> numberFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("测试一下");
            Integer number = 1;
            return number;
        }, service)
                .whenComplete((res,exception)->{
            System.out.println("结果:"+res+",异常:"+exception.getMessage());
        })
                .exceptionally(exception->{
            return 20;
        });
        System.out.println(numberFuture.get());

③handle:方法完成后的处理,能够处理结果返回,并且感知异常之后进行处理返回。BiFunction。

CompletableFuture<Integer> numberFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("测试一下");
            Integer number = 10/1;
            return number;
        }, service).handle((res,exception)->{
            if(res!=null){
                return res *2;
            }
            if(exception!=null){
                return 0;
            }
            return 0;
        });
        System.out.println(numberFuture.get());

4.异步编排的串行线程

思路

①其实就是在后面链接上线程池和要执行的任务

thenRunAsync():执行但是没有返回值

thenAcceptAsync(res):需要接受上一个线程的参数,才能继续执行但是没有返回值

thenApplyAsync(res->{return}):需要接受上一线程参数,并且有返回值。

 CompletableFuture.supplyAsync(() -> {
            System.out.println("测试一下");
            Integer number = 10/1;
            return number;
        }, service).thenRunAsync(()->{
            System.out.println("测试run");
        },service);//没有返回值

        System.out.println();

        CompletableFuture.supplyAsync(() -> {
            System.out.println("测试一下");
            Integer number = 10/1;
            return number;
        }, service).thenAccept((res)->{
            System.out.println("测试:"+res);
        });


        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            return 1;
        }, service).thenApplyAsync((res) -> {
            System.out.println("测试" + res);
            return 4;
        });
        System.out.println(future1.get());

介绍

两个任务组合,都要完成

①runAterBothAsync:同时运行前面两个线程,然后最后一个再运行,没有返回值

②runAcceptBothAsync:没有返回值,但是能够获取前两个线程执行后的结果

③runCombineAsync:有返回值,而且能够获取前两个线程执行后的结果

 CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1开始");
            System.out.println("任务1结束");
            return "任务1";
        });


        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2开始");
            System.out.println("任务2结束");
            return "任务2";
        });
//
//        future1.runAfterBothAsync(future2,()-> {
//            System.out.println("执行任务3");
//        },service);

//        future1.thenAcceptBothAsync(future2,(f1,f2)->{
//            System.out.println(f1);
//            System.out.println(f2);
//        },service);


        CompletableFuture<String> future = future1.thenCombineAsync(future2, (f1, f2) -> {
            return f1 + " " + f2 + "->hahah";
        }, service);
        System.out.println(future.get());

两个任务其中一个完成就可以了,执行线程3了。思路和上面的一模一样。只有加上async才会使用线程池。

//        future1.runAfterEitherAsync(future2,()->{
//            System.out.println("任务3");
//        },service);

//
//        future1.acceptEitherAsync(future2,(res)->{
//            System.out.println("任务3+"+res);
//        },service);


        CompletableFuture<String> future = future1.applyToEitherAsync(future2, (res) -> {
            return "任务3和" + res;
        }, service);
        System.out.println(future.get());

allOf:一起执行完才能执行下一个线程,anyOf:其中一个线程执行之后就能够执行下面的语句。相当于阻塞了主线程,要让前面的线程先执行完毕。

CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
            System.out.println("futureImg");
            return "img";
        }, service);

        CompletableFuture<String> futureSku = CompletableFuture.supplyAsync(() -> {
            System.out.println("futurnSku");
            return "sku";
        }, service);


        CompletableFuture<String> futureSaleAttr = CompletableFuture.supplyAsync(() -> {
            System.out.println("futureSaleAttr");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "saleAttr";
        }, service);

        System.out.println("start");
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureSku, futureSaleAttr);
        anyOf.get();
//        System.out.println(allOf.get());
        System.out.println("end:"+anyOf.get());

5.商品详细

1.详情页面准备

思路

①nginx静态页面配置和转发路径配置

②gateway配置

③转移详情页面到product服务

④写ItemController跳转到对应的页面

⑤修改图片的a地址,主要就是跳转到详情页面。th:href="|xxxx.com/${xx}.html|"

2.详情页面数据分析

思路

①skuInfo包括 title,subtitle、name

②skuImg

③销售属性,销售属性的name和values

④spudesc

⑤属性分组和规格属性。

@Data
public class SkuItemVo {
    //1.skuInfo
    private SkuInfoEntity info;

    //2.skuImg
    private List<SkuImagesEntity> images;

    //3.saleAttr
    List<SkuItemAttrVo> saleAttrs;

    //4.spuDesc
    private SpuInfoDescEntity desc;

    //5.spuItemAttr
    List<SpuItemBaseAttrGroupVo> groupAttrs;


    public static class SkuItemAttrVo{
        private Long attrId;
        private String attrName;
        private List<String> attrValue;
    }

    public static class SpuItemBaseAttrGroupVo{
        private String groupName;
        private List<SpuBaseAttrVo> attrs;
    }

    public static class SpuBaseAttrVo{
        private String attrName;
        private String attrValue;
    }
}

3.查询对应的规格属性sql以及其他返回参数查询

思路

①img、info、desc都是很好查询的

②但是规格属性就难一些。需要通过catalogId获取所有属性分组,然后通过属性分组获取所有的属性,最后就是通过spuId获取这个类型的物品的规格参数。简单来说就是获取手机的规格参数,分组。先通过分类id获取分组属性->属性->spuId->规格属性。因为同一个分类的属性不一定是同一个spu的。spu改变其实属性分组里面的属性的值。但是他们拥有的规格属性基本一致。

select 
 ag.attr_group_name groupName,
 aar.attr_group_id,
 aar.attr_id,
 pa.attr_name attrName,
 pav.attr_value attrValuex
from  pms_attr_group ag
left join pms_attr_attrgroup_relation aar on ag.attr_group_id=aar.attr_group_id
left join pms_attr  pa on pa.attr_id  = aar.attr_id
left join pms_product_attr_value pav on pa.attr_id= pav.attr_id
where ag.catelog_id=225 and pav.spu_id=17
 @Override
    public SkuItemVo item(Long skuId) {
        SkuItemVo skuItemVo = new SkuItemVo();
        //1.skuinfo
        SkuInfoEntity skuInfoEntity = this.getById(skuId);
        Long catalogId = skuInfoEntity.getCatalogId();
        Long spuId = skuInfoEntity.getSpuId();
        skuItemVo.setInfo(skuInfoEntity);
        //2.skuImg
        List<SkuImagesEntity> images=skuImagesService.getImgById(skuId);
         skuItemVo.setImages(images);
        //3.skuSaleAttr


        //4.spuDesc
        SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(spuId);
        skuItemVo.setDesc(spuInfoDescEntity);


        //5.AttrGroup
        List<SpuItemBaseAttrGroupVo> spuItemBaseAttrGroupVos=attrGroupService.getSpuAttrBaseItemGroupVos(spuId,catalogId);
        skuItemVo.setGroupAttrs(spuItemBaseAttrGroupVos);
        return null;
    }

4.销售属性

思路

①通过spuid获取所有的skuInfo,然后就是通过skuInfo的skuid与saleAttr左外连接,然后获取属性的信息。并且通过group by通过attrid和attrname进行的一个分类查询属性的信息,属性和属性值。

select 
ssav.attr_id attr_id,
ssav.attr_name attr_name,
group_CONCAT(DISTINCT ssav.attr_value) attr_values
from pms_sku_info psi
left join pms_sku_sale_attr_value ssav  on  psi.sku_id=ssav.sku_id
where psi.spu_id=17
group by ssav.attr_id,ssav.attr_name

5.页面渲染展示

思路

①标题渲染

②price

③属性渲染,这个地方需要用到#strings.listSplit来分割字符串获取list

④图片

⑤商品介绍,遍历img

⑥规格属性。也是要用到分割。

拓展

①如果字符串需要分割可以使用#strings.listSplit来解决

②如果是需要限制数字的小数和整数位需要用到#numbers.formatDecimal(xx,2,3)

6.销售属性渲染

思路

①销售属性的值可以通过自己包含的skuid,与其他属性合集来取出他们之间相同的skuid,这样才能够获取对应的页面。

②解决办法就是value变成一个对象里面存入value和skuIds(一个值对应的多个skuids)

③点击属性之后需要给他加上clicked,而且需要移除这一行的checked。接着就是获取其它属性的checked的skuids,并且把他们全部放到一个array里面。最后就是通过filter,也就是寻找交集的一个方法遍历循环。最后获取一个对应的skuId

拓展与踩坑

①第一个是对比的时候,skuid和val.skuIds一个是long一个是string。

$(".sku_attr_value").click(function(){
			//0.创建一个存入所有被选中的集合
			var skuIds=new Array();
			skuIds.push($(this).attr("skus").split(","));

			$(this).addClass("clicked");

			//1.清除同一行的checked
			$(this).parent().parent().removeClass("checked");

			//2.找到其他被选中的
			$("a[class='sku_attr_value checked']").each(function(){
				skuIds.push($(this).attr("skus"));
			})

			var filterEle=skuIds[0];
			for(var i=1;i<skuIds.length;i++){
				filterEle=$(filterEle).filter(filterEle);
			}
			console.log(filterEle[0]);
		})

		$(function () {
            $(".sku_attr_value").parent().css({"border":"solid 1px #cccccc"})
		    $("a[class='sku_attr_value checked']").parent().css({"border":"solid 1px red"})
		})

7.异步编排优化

思路

①先把对应的线程池注入进来。如果需要修改属性可以配置一个properties,然后通过mvc放进我们的configuration的方法,然后使用

②接着就是异步编排,先查询到skuInfo,然后才能够查询商品简介,商品销售属性和商品基本属性,接着与查询商品img同时进行。也就是两个异步,一个是skuInfo和skuImg,另一个是商品简介,商品销售属性和商品基本属性。

@Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
        SkuItemVo skuItemVo = new SkuItemVo();
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            //1.skuinfo
            SkuInfoEntity skuInfoEntity = this.getById(skuId);
            skuItemVo.setInfo(skuInfoEntity);
            return skuInfoEntity;
        },executor);

        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //3.skuSaleAttr
            List<SkuItemAttrVo> skuItemAttrVos = skuSaleAttrValueService.getSkuItemAttrVosBySpuId(res.getSpuId());
            skuItemVo.setSaleAttrs(skuItemAttrVos);
        },executor);

        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
            //4.spuDesc
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesc(spuInfoDescEntity);
        },executor);

        CompletableFuture<Void> spuAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //5.AttrGroup
            List<SpuItemBaseAttrGroupVo> spuItemBaseAttrGroupVos = attrGroupService.getSpuAttrBaseItemGroupVos(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(spuItemBaseAttrGroupVos);
        },executor);


        CompletableFuture<Void> imgFuture = CompletableFuture.runAsync(() -> {
            //2.skuImg
            List<SkuImagesEntity> images = skuImagesService.getImgById(skuId);
            skuItemVo.setImages(images);
        },executor);

        CompletableFuture.allOf(infoFuture,imgFuture,saleAttrFuture,spuAttrFuture,descFuture).get();


        return skuItemVo;
    }
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值