2026版商城项目(三)-- ES+认证服务

1. 商品检索功能

1.1 检索页面配置

页面编写、动静分离配置、nginx配置略

1.2 检索条件 & 排序条件分析

  • 全文检索:skuTitle -> keyword
  • 排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
  • 过滤:hasStock(是否有货)、skuPrice(价格区间)、brandId、catalog3Id、attrs
  • 聚合:attrs
  • 完整查询参数
    • keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1&catalog3Id=1&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏
java 复制代码
@Data
public class SearchParam {

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

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

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

    /**
     * 排序条件:sort=price/salecount/hotscore_desc/asc
     */
    private String sort;

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

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

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

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

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

1.3 返回结果分析

从京东的返回值来看,可以看到除了返回具体的商品,还要返回所有涉及到的品牌、分类、运行内存、分类等。

java 复制代码
@Data
public class SearchResult {

    /**
     * 查询到的所有商品信息
     */
    private List<SkuEsModel> product;

    /**
     * 当前页码
     */
    private Integer pageNum;

    /**
     * 总记录数
     */
    private Long total;

    /**
     * 总页码
     */
    private Integer totalPages;

    private List<Integer> pageNavs;

    /**
     * 当前查询到的结果,所有涉及到的品牌
     */
    private List<BrandVo> brands;

    /**
     * 当前查询到的结果,所有涉及到的所有属性
     */
    private List<AttrVo> attrs;

    /**
     * 当前查询到的结果,所有涉及到的所有分类
     */
    private List<CatalogVo> catalogs;

    //===========================以上是返回给页面的所有信息============================//

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

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

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

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

1.4 DSL分析(es查询语句分析)

检索时需要进行:模糊匹配、过滤(按照属性,分类,品牌,价格区间,库存)、排序、分页、高亮、聚合分析 。

原先的映射(mapping)不符合我们现在的检索需求,我们需要先调整映射,在进行分析。

1.4.1 修改映射:

  1. 添加新的index并设置映射
java 复制代码
PUT mall_product
{
  "mappings": {
    "properties": {
      "attrs": {
        "type": "nested",
        "properties": {
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword"
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      },
      "autoGeneratedTimestamp": {
        "type": "long"
      },
      "brandId": {
        "type": "long"
      },
      "brandImg": {
        "type": "keyword"
      },
      "brandName": {
        "type": "keyword"
      },
      "catalogId": {
        "type": "long"
      },
      "catalogName": {
        "type": "keyword"
      },
      "description": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "hasStock": {
        "type": "boolean"
      },
      "hotScore": {
        "type": "long"
      },
      "parentTask": {
        "properties": {
          "id": {
            "type": "long"
          },
          "nodeId": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "set": {
            "type": "boolean"
          }
        }
      },
      "refreshPolicy": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "retry": {
        "type": "boolean"
      },
      "saleCount": {
        "type": "long"
      },
      "shouldStoreResult": {
        "type": "boolean"
      },
      "skuId": {
        "type": "long"
      },
      "skuImg": {
        "type": "keyword"
      },
      "skuPrice": {
        "type": "keyword"
      },
      "skuTitle": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "spuId": {
        "type": "keyword"
      }
    }
  }
}
  1. 迁移数据:
java 复制代码
POST _reindex
{
  "source": {
    "index": "product"
  },
  "dest": {
    "index": "gulimall_product"
  }
}
  1. 修改原来的index常量

1.4.2 DSL检索语句:

需要查询出2部分内容:

  1. 具体的数据
  2. 聚合查询出的属性数据
json 复制代码
GET gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
            "term": {
              "catelogId": 225
            }
        },
        {
            "terms": {
            "brandId": [
              9
            ]
          }
        },
        {
          "term": {
            "hasStock": false
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 1000,
              "lte": 7000
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": 15
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "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": {
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
      
        "brandImgAgg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
        
      }
    },
    "catelogAgg":{
      "terms": {
        "field": "catelogId",
        "size": 10
      },
      "aggs": {
        "catelogNameAgg": {
          "terms": {
            "field": "catelogName",
            "size": 10
          }
        }
      }
    },
    "attrs":{
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attrIdAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

1.5 检索代码编写

  1. es7.x版本代码:使用的是Java High Level REST Client
java 复制代码
//去es进行检索
@Override
public SearchResult search(SearchParam param) {
    // 动态构建出查询需要的DSL语句
    SearchResult result = null;
    // 1、准备检索请求
    SearchRequest searchRequest = buildSearchRequest(param);
    try {
        // 2、执行检索请求
        SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
        // 分析响应数据封装我们需要的格式
        result = buildSearchResult(response, param);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

/**
 * 准备检索请求
 * 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存),排序,分页,高亮,聚合分析
 *
 * @return
 */
private SearchRequest buildSearchRequest(SearchParam param) {
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
    /**
     * 1、过滤(按照属性、分类、品牌、价格区间、库存)
     */
    // 1、构建bool-query
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    sourceBuilder.query(boolQuery);
    // 1.1、must-模糊匹配、
    if (!StringUtils.isEmpty(param.getKeyword())) {
        boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
    }
    // 1.2.1、filter-按照三级分类id查询
    if (null != param.getCatalog3Id()) {
        boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
    }
    // 1.2.2、filter-按照品牌id查询
    if (null != param.getBrandId() && param.getBrandId().size() > 0) {
        boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
    }
    // 1.2.3、filter-按照是否有库存进行查询
    if (null != param.getHasStock()) {
        boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
    }
    // 1.2.4、filter-按照区间进行查询  1_500/_500/500_
    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
    if (!StringUtils.isEmpty(param.getSkuPrice())) {
        String[] prices = param.getSkuPrice().split("_");
        if (prices.length == 1) {
            if (param.getSkuPrice().startsWith("_")) {
                rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
            } else {
                rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
            }
        } else if (prices.length == 2) {
            // _6000会截取成["","6000"]
            if (!prices[0].isEmpty()) {
                rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
            }
            rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
        }
        boolQuery.filter(rangeQueryBuilder);
    }
    // 1.2.5、filter-按照属性进行查询
    List<String> attrs = param.getAttrs();
    if (null != attrs && attrs.size() > 0) {
        // attrs=1_5寸:8寸&2_16G:8G
        attrs.forEach(attr -> {
            BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
            String[] attrSplit = attr.split("_");
            queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));//检索的属性的id
            String[] attrValues = attrSplit[1].split(":");
            queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));//检索的属性的值
            // 每一个必须都得生成一个nested查询
            NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
            boolQuery.filter(nestedQueryBuilder);
        });
    }
    // 把以前所有的条件都拿来进行封装
    sourceBuilder.query(boolQuery);
    /**
     * 2、排序,分页,高亮,
     */
    // 2.1、排序  eg:sort=saleCount_desc/asc
    if (!StringUtils.isEmpty(param.getSort())) {
        String[] sortSplit = param.getSort().split("_");
        sourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
    }

    // 2.2、分页
    sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
    sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

    // 2.3、高亮highlight
    if (!StringUtils.isEmpty(param.getKeyword())) {
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("skuTitle");
        highlightBuilder.preTags("<b style='color:red'>");
        highlightBuilder.postTags("</b>");
        sourceBuilder.highlighter(highlightBuilder);
    }

    /**
     * 3、聚合分析
     */
    // 3、聚合
    // 3.1、按照品牌聚合
    TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
    // 3.1.1、品牌聚合的子聚合
    TermsAggregationBuilder brand_name_agg = AggregationBuilders.terms("brand_name_agg").field("brandName").size(1);
    brand_agg.subAggregation(brand_name_agg);
    TermsAggregationBuilder brand_img_agg = AggregationBuilders.terms("brand_img_agg").field("brandImg");
    brand_agg.subAggregation(brand_img_agg);
    
    sourceBuilder.aggregation(brand_agg);

    // 3.2、按照catalog聚合
    TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
    TermsAggregationBuilder catalog_name_agg = AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1);
    catalog_agg.subAggregation(catalog_name_agg);
    sourceBuilder.aggregation(catalog_agg);

    // 3.3、按照attrs聚合
    NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attr_agg", "attrs");
    // 3.3.1、按照attrId聚合
    TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
    // 3.3.2、按照attrId聚合之后再按照attrName和attrValue聚合
    TermsAggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1);
    attr_id_agg.subAggregation(attr_name_agg);
    TermsAggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50);
    attr_id_agg.subAggregation(attr_value_agg);

    nestedAggregationBuilder.subAggregation(attr_id_agg);
    sourceBuilder.aggregation(nestedAggregationBuilder);

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

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

}

/**
 * 构建结果数据
 *
 * @param response
 * @return
 */
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
    SearchResult result = new SearchResult();
    // 1、返回的所有查询到的商品
    SearchHits hits = response.getHits();
    List<SkuEsModel> esModels = new ArrayList<>();
    if (null != hits.getHits() && 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");
                esModel.setSkuTitle(skuTitle.fragments()[0].string());
            }
            esModels.add(esModel);
        }
    }
    result.setProducts(esModels);
    // 2、当前所有商品涉及到的所有属性
    List<SearchResult.AttrVo> attrVos = new ArrayList<>();
    ParsedNested attr_agg = response.getAggregations().get("attr_agg");
    ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
    for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
        SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
        // 1、得到属性的id;
        long attrId = bucket.getKeyAsNumber().longValue();
        // 2、得到属性的名字
        String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
        // 3、得到属性的所有值
        List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
            String keyAsString = item.getKeyAsString();
            return keyAsString;
        }).collect(Collectors.toList());
        attrVo.setAttrId(attrId);
        attrVo.setAttrName(attrName);
        attrVo.setAttrValue(attrValues);
        attrVos.add(attrVo);
    }

    result.setAttrs(attrVos);
    // 3、当前所有品牌涉及到的所有属性
    List<SearchResult.BrandVo> brandVos = new ArrayList<>();
    ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
    for (Terms.Bucket bucket : brand_agg.getBuckets()) {
        SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
        // 1、得到品牌的id
        long brandId = bucket.getKeyAsNumber().longValue();
        // 2、得到品牌的名
        String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
        // 3、得到品牌的图片
        String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
        brandVo.setBrandId(brandId);
        brandVo.setBrandName(brandName);
        brandVo.setBrandImg(brandImg);
        brandVos.add(brandVo);
    }
    result.setBrands(brandVos);
    // 4、当前商品所涉及的分类信息
    ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");

    List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
    List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
    for (Terms.Bucket bucket : buckets) {
        SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
        // 得到分类id
        String keyAsString = bucket.getKeyAsString();
        catalogVo.setCatalogId(Long.parseLong(keyAsString));

        // 得到分类名
        ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
        String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
        catalogVo.setCatalogName(catalog_name);
        catalogVos.add(catalogVo);
    }
    result.setCatalogs(catalogVos);
    //===========以上从聚合信息获取到=============
    // 5、分页信息-页码
    result.setPageNum(param.getPageNum());
    // 6、分页信息-总记录数
    long total = hits.getTotalHits().value;
    result.setTotal(total);
    // 7、分页信息-总页码-计算
    int totalPages = total % EsConstant.PRODUCT_PAGESIZE == 0 ? (int) total / EsConstant.PRODUCT_PAGESIZE : ((int) total / EsConstant.PRODUCT_PAGESIZE + 1);
    result.setTotalPages(totalPages);
    return result;
}
  1. es8.X写法:
java 复制代码
private SearchRequest buildSearchRequest(SearchParam param) {
    // 1. 构建 Bool 查询
    BoolQuery.Builder boolBuilder = new BoolQuery.Builder();

    // must 条件:关键词匹配
    if (!StringUtils.isEmpty(param.getKeyword())) {
        boolBuilder.must(m -> m.match(t -> t.field("skuTitle").query(param.getKeyword())));
    }

    // filter 条件
    // 三级分类
    if (param.getCatalog3Id() != null) {
        boolBuilder.filter(f -> f.term(t -> t.field("catalogId").value(param.getCatalog3Id())));
    }
    // 品牌
    if (param.getBrandId() != null && !param.getBrandId().isEmpty()) {
        List<FieldValue> brandIdValues = param.getBrandId().stream()
                .map(FieldValue::of)
                .collect(Collectors.toList());
        boolBuilder.filter(f -> f.terms(t -> t.field("brandId").terms(ts -> ts.value(brandIdValues))));
    }

    // 属性(nested 查询)
    if (param.getAttrs() != null && !param.getAttrs().isEmpty()) {
        for (String attr : param.getAttrs()) {
            String[] s = attr.split("_");
            String attrId = s[0];
            String[] attrValues = s[1].split(":");
            List<FieldValue> values = Arrays.stream(attrValues)
                    .map(FieldValue::of)
                    .collect(Collectors.toList());
            Query nestedQuery = Query.of(q -> q
                    .bool(b -> b
                            .must(m -> m.term(t -> t.field("attrs.attrId").value(attrId)))
                            .must(m -> m.terms(t -> t.field("attrs.attrValue").terms(ts -> ts.value(values))))
                    )
            );
            boolBuilder.filter(f -> f.nested(n -> n.path("attrs").query(nestedQuery)));
        }
    }
    // 库存
    if (param.getHasStock() != null) {
        boolBuilder.filter(f -> f.term(t -> t.field("hasStock").value(param.getHasStock() == 1)));
    }
    // 价格区间
    if (!StringUtils.isEmpty(param.getSkuPrice())) {
        String[] price = param.getSkuPrice().split("_");
        try {
            if (price.length == 2) {
                RangeQuery rangeQuery = RangeQuery.of(r -> r
                        .number(n -> n
                                .field("skuPrice")
                                .gte(Double.parseDouble(price[0]))
                                .lte(Double.parseDouble(price[1]))
                        )
                );
                boolBuilder.filter(f -> f.range(rangeQuery));
            } else if (price.length == 1) {
                // 单边界:_to 或 from_
                if (param.getSkuPrice().startsWith("_")) {
                    RangeQuery rangeQuery = RangeQuery.of(r -> r
                            .number(n -> n
                                    .field("skuPrice")
                                    .lte(Double.parseDouble(price[0]))
                            )
                    );
                    boolBuilder.filter(f -> f.range(rangeQuery));
                } else if (param.getSkuPrice().endsWith("_")) {
                    RangeQuery rangeQuery = RangeQuery.of(r -> r
                            .number(n -> n
                                    .field("skuPrice")
                                    .gte(Double.parseDouble(price[0]))
                            )
                    );
                    boolBuilder.filter(f -> f.range(rangeQuery));
                }
            }
        } catch (NumberFormatException e) {
            log.warn("价格参数格式错误: {}", param.getSkuPrice());
        }
    }

    Query finalQuery = Query.of(q -> q.bool(boolBuilder.build()));

    // 2. 分页、排序、高亮
    SearchRequest.Builder searchBuilder = new SearchRequest.Builder()
            .index(EsConstant.PRODUCT_INDEX)
            .query(finalQuery)
            .from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE)
            .size(EsConstant.PRODUCT_PAGESIZE);

    // 排序
    if (!StringUtils.isEmpty(param.getSort())) {
        String[] sortFields = param.getSort().split("_");
        SortOrder order = "asc".equalsIgnoreCase(sortFields[1]) ? SortOrder.Asc : SortOrder.Desc;
        searchBuilder.sort(so -> so.field(f -> f.field(sortFields[0]).order(order)));
    }

    // 高亮
    if (!StringUtils.isEmpty(param.getKeyword())) {
        searchBuilder.highlight(h -> h
                .fields("skuTitle", HighlightField.of(hf -> hf
                        .preTags("<b style='color:red'>")
                        .postTags("</b>")
                ))
        );
    }

    // 3. 聚合分析(修正:使用官方推荐方式构建)
    // 品牌聚合
    searchBuilder.aggregations("brand_agg", Aggregation.of(a -> a
            .terms(t -> t.field("brandId").size(50))
            .aggregations("brand_name_agg", Aggregation.of(na -> na
                    .terms(nt -> nt.field("brandName").size(1))))
            .aggregations("brand_img_agg", Aggregation.of(na -> na
                    .terms(nt -> nt.field("brandImg").size(1))))
    ));

    // 分类聚合
    searchBuilder.aggregations("catalog_agg", Aggregation.of(a -> a
            .terms(t -> t.field("catalogId").size(20))
            .aggregations("catalog_name_agg", Aggregation.of(na -> na
                    .terms(nt -> nt.field("catalogName").size(1))))
    ));

    // 属性聚合(nested)
    searchBuilder.aggregations("attr_agg", Aggregation.of(a -> a
            .nested(n -> n.path("attrs"))
            .aggregations("attr_id_agg", Aggregation.of(na -> na
                    .terms(t -> t.field("attrs.attrId").size(50))
                    .aggregations("attr_name_agg", Aggregation.of(nb -> nb
                            .terms(nt -> nt.field("attrs.attrName").size(1))))
                    .aggregations("attr_value_agg", Aggregation.of(nb -> nb
                            .terms(nt -> nt.field("attrs.attrValue").size(50))))
            ))
    ));

    SearchRequest request = searchBuilder.build();
    log.info("构建的 DSL 语句:{}", request.toString());
    return request;
}

//封装检索结果
private SearchResult buildSearchResult(SearchResponse<SkuEsModel> response, SearchParam param) {
    SearchResult result = new SearchResult();

    // 1. 商品列表
    List<SkuEsModel> products = new ArrayList<>();
    for (Hit<SkuEsModel> hit : response.hits().hits()) {
        SkuEsModel model = hit.source();
        if (model != null) {
            // 高亮处理
            if (!StringUtils.isEmpty(param.getKeyword()) && hit.highlight() != null) {
                Map<String, List<String>> highlights = hit.highlight();
                if (highlights.containsKey("skuTitle") && !highlights.get("skuTitle").isEmpty()) {
                    model.setSkuTitle(highlights.get("skuTitle").get(0));
                }
            }
            products.add(model);
        }
    }
    result.setProduct(products);

    // 2. 属性聚合结果(修正:正确处理 nested 聚合的层级)
    List<SearchResult.AttrVo> attrVos = new ArrayList<>();
    Aggregate attrAgg = response.aggregations().get("attr_agg");
    if (attrAgg != null && attrAgg.isNested()) {
        NestedAggregate nestedAgg = attrAgg.nested();
        Aggregate attrIdAgg = nestedAgg.aggregations().get("attr_id_agg");
        // Lterms代表接收的Long类型的数据,Sterms代表接收的String类型数据
        if (attrIdAgg != null && attrIdAgg.isLterms()) {
            LongTermsAggregate stringTerms = attrIdAgg.lterms();
            for (LongTermsBucket bucket : stringTerms.buckets().array()) {
                SearchResult.AttrVo attrVo = new SearchResult.AttrVo();

                // 属性id
                attrVo.setAttrId(bucket.key());

                // 属性名
                Aggregate nameAgg = bucket.aggregations().get("attr_name_agg");
                if (nameAgg != null && nameAgg.isSterms()) {
                    StringTermsAggregate nameTerms = nameAgg.sterms();
                    if (!nameTerms.buckets().array().isEmpty()) {
                        // FieldValue 需要调用 .stringValue() 获取字符串值
                        attrVo.setAttrName(nameTerms.buckets().array().get(0).key().stringValue());
                    }
                }

                // 属性值
                Aggregate valueAgg = bucket.aggregations().get("attr_value_agg");
                if (valueAgg != null && valueAgg.isSterms()) {
                    StringTermsAggregate valueTerms = valueAgg.sterms();
                    List<String> values = valueTerms.buckets().array().stream()
                            .map(a -> a.key().stringValue())
                            .collect(Collectors.toList());
                    attrVo.setAttrValue(values);
                }
                attrVos.add(attrVo);
            }
        }
    }
    result.setAttrs(attrVos);

    // 3. 品牌聚合
    List<SearchResult.BrandVo> brandVos = new ArrayList<>();
    Aggregate brandAgg = response.aggregations().get("brand_agg");
    if (brandAgg != null && brandAgg.isLterms()) {
        LongTermsAggregate longTerms = brandAgg.lterms();
        for (LongTermsBucket bucket : longTerms.buckets().array()) {
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            brandVo.setBrandId(bucket.key());

            Aggregate nameAgg = bucket.aggregations().get("brand_name_agg");
            if (nameAgg != null && nameAgg.isSterms()) {
                StringTermsAggregate nameTerms = nameAgg.sterms();
                if (!nameTerms.buckets().array().isEmpty()) {
                    brandVo.setBrandName(nameTerms.buckets().array().get(0).key().stringValue());
                }
            }

            Aggregate imgAgg = bucket.aggregations().get("brand_img_agg");
            if (imgAgg != null && imgAgg.isSterms()) {
                StringTermsAggregate imgTerms = imgAgg.sterms();
                if (!imgTerms.buckets().array().isEmpty()) {
                    brandVo.setBrandImg(imgTerms.buckets().array().get(0).key().stringValue());
                }
            }
            brandVos.add(brandVo);
        }
    }
    result.setBrands(brandVos);

    // 4. 分类聚合
    List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
    Aggregate catalogAgg = response.aggregations().get("catalog_agg");
    if (catalogAgg != null && catalogAgg.isLterms()) {
        LongTermsAggregate longTerms = catalogAgg.lterms();
        for (LongTermsBucket bucket : longTerms.buckets().array()) {
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            catalogVo.setCatalogId(bucket.key());

            Aggregate nameAgg = bucket.aggregations().get("catalog_name_agg");
            if (nameAgg != null && nameAgg.isSterms()) {
                StringTermsAggregate nameTerms = nameAgg.sterms();
                if (!nameTerms.buckets().array().isEmpty()) {
                    catalogVo.setCatalogName(nameTerms.buckets().array().get(0).key().stringValue());
                }
            }
            catalogVos.add(catalogVo);
        }
    }
    result.setCatalogs(catalogVos);

    // 5. 分页信息
    long total = response.hits().total() != null ? response.hits().total().value() : 0;
    // 总记录数
    result.setTotal(total);
    // 当前页码
    result.setPageNum(param.getPageNum());
    int totalPages = (int) ((total + EsConstant.PRODUCT_PAGESIZE - 1) / EsConstant.PRODUCT_PAGESIZE);
    // 总页码
    result.setTotalPages(totalPages);
    List<Integer> pageNavs = new ArrayList<>();
    for (int i = 1; i <= totalPages; i++) {
        pageNavs.add(i);
    }
    result.setPageNavs(pageNavs);

    log.info("=========结果为:" + JSON.toJSONString(result));
    return result;
}

1.6 页面渲染编写

1.7 面包屑导航

2. 异步编程

2.1 创建线程的四种方式

多线程学习

2.2 线程池详解

可见线程池学习

2.3 CompletableFuture组合式异步编程

CompletableFuture异步编程

3. 商品详情

其他内容:略

3.1 使用异步编排完成详情页的内容展示:

java 复制代码
    @Override
    public SkuItemVo item(Long skuId) {
        SkuItemVo skuItemVo = new SkuItemVo();
        // 使用异步编排
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            // 1、sku基本信息获取    pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);
 
        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            // 2、sku的图片信息      pms_sku_images
            List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, 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 desc = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesc(desc);
        }, executor);
 
        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            // 5、获取spu的规格参数信息
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);
        
        // 等待所有任务执行完成
        try {
            CompletableFuture.allOf(saleAttrFuture, imageFuture, descFuture, baseAttrFuture).get();
        } catch (InterruptedException e) {
            log.error("1等待所有任务执行完成异常{}", e);
        } catch (ExecutionException e) {
            log.error("2等待所有任务执行完成异常{}", e);
        }
        return skuItemVo;
    }

4. 认证服务

4.1 验证码发送

阿里云、腾讯云都可以,这里配置略

4.2 验证码防刷

由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。

  • 在redis中以phone-code将电话号码和验证码进行存储并将当前时间与code一起存储
  • 如果调用时以当前phone取出的值不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息
  • 60s以后再次调用,需要删除之前存储的phone-code
  • code存在一个过期时间,我们设置为10min,10min内验证该验证码有效

获取验证码接口编写:

java 复制代码
@ResponseBody
@GetMapping(value = "/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone) {

    //1、接口防刷
    String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
    if (!StringUtils.isEmpty(redisCode)) {
        //活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
        long currentTime = Long.parseLong(redisCode.split("_")[1]);
        if (System.currentTimeMillis() - currentTime < 60000) {
            //60s内不能再发
            return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMessage());
        }
    }

    //2、验证码的再次效验 redis.存key-phone,value-code
    int code = (int) ((Math.random() * 9 + 1) * 100000);
    String codeNum = String.valueOf(code);
    String redisStorage = codeNum + "_" + System.currentTimeMillis();

    //存入redis,防止同一个手机号在60秒内再次发送验证码
    stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,
            redisStorage,10, TimeUnit.MINUTES);

    thirdPartFeignService.sendCode(phone, codeNum);

    return R.ok();
}

4.3 注册登陆业务

4.4 登陆业务

4.5 社交登陆-Oauth2

Oauth2即社交登陆,通过绑定其他大型社交网站(QQ 、微博、 github 等网站的用户量非常大)的账号,来实现简化自己网站登陆与注册的模式。

步骤:

  1. 用户点击 QQ 按钮
  2. 引导跳转到 QQ 授权页
  3. 用户主动点击授权,跳回之前网页

4.5.1 OAuth2.0

  • OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
  • OAuth2.0:对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
  • 官方版流程:

    (A )用户打开客户端以后,客户端要求用户给予授权。

    (B )用户同意给予客户端授权。

    (C )客户端使用上一步获得的授权,向认证服务器申请令牌。

    (D )认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

    (E )客户端使用令牌,向资源服务器申请获取资源。

    (F )资源服务器确认令牌无误,同意向客户端开放资源。

  • OAuth2.0流程:

使用Code换取AccessToken,Code只能用一次

同一个用户的accessToken一段时间是不会变化的,即使多次获取

4.5.2 gitee创建应用示例:

  1. 登录 Gitee,进入设置,在左侧菜单找到并点击 "第三方应用"。
  2. 点击 "创建应用",填写以下信息:
    • 应用名称:你应用的名称。
    • 应用描述:简短介绍你的应用。
    • 应用主页:你的网站首页地址。
    • 应用回调地址:这是最关键的一步。填写用户授权后,Gitee 要重定向回你网站的后端接口地址。例如,如果你的后端接口是 https://yourdomain.com/oauth/callback,就填这个。
  • 权限:根据需求勾选,比如 user_info(获取用户信息)。
  1. 提交后,你将获得 Client ID 和 Client Secret,请务必妥善保存。

4.5.3 gitee测试



4.5.4 代码编写与测试

4.5.5 总结

Oauth2.0 ;授权通过后,使用 code 换取 access_token ,然后去访问任何开放 API

  1. code 用后即毁
  2. access_token 在几天内是一样的
  3. uid 永久固定

4.6 SpringSession

4.6.1 session原理:

jsessionid相当于银行卡,存在服务器的session相当于存储的现金,每次通过jsessionid取出保存的数据。

问题:但是正常情况下session不可跨域,它有自己的作用范围

4.6.2 分布式下session共享问题

  • 同一个服务,复制多份,session不同步问题
  • 不同服务,session不能共享问题

4.6.3 Session共享问题解决

  1. session复制

  2. 客户端存储

  3. hash一致性

  4. 统一存储

  5. 不同服务,子域session共享

4.6.4 SpringSession整合redis

通过SpringSession修改session的作用域

  1. 环境搭建:mall-auth-server模块

    • common模块中pom导入依赖
    xml 复制代码
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    • 修改apllication.properties配置:spring.session.store-type=redis
    • 主配置类添加注解:@EnableRedisHttpSession
  2. 自定义配置

    • 由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化
    • 并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com
    • 添加"MallSessionConfig"类,代码如下:
java 复制代码
@Configuration
public class GulimallSessionConfig {
 
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }
 
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}
  • 把MemberResponseVo类移到mall-common服务里,并且序列化

  • 修改".Oauth2Controller"类,代码略

  • gulimall-product模块

    • 修改配置:
    java 复制代码
    spring:
      session:
        store-type: redis   #指定session的存储格式
    • 配置"MallSessionConfig"类,代码如下:
    java 复制代码
    @Configuration
    public class GulimallSessionConfig {
     
        @Bean
        public CookieSerializer cookieSerializer(){
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setDomainName("gulimall.com");
            cookieSerializer.setCookieName("GULISESSION");
            return cookieSerializer;
        }
     
        @Bean
        public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
            return new GenericJackson2JsonRedisSerializer();
        }
    }

4.6.5 SpringSession原理:

  • @EnableRedisHttpSession导入RedisHttpSessionConfiguration.class配置作用:
  1. 给容器中添加了一个组件:SessionRepository=》》》 【RedisIndexedSessionRepository】=>redis操作session.session的增删改查的封装类
  2. SessionRepositoryFilter=》Filter: session存储过滤器,每个请求过来都必须经过filter
    1. 创建的时候,就自动从容器中获取到了SessionRepository:
    2. 原生的request,response都被包装为:SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
    3. 以后获取session.request.getSession()都是从:wrapperedRequest.getSession();===>SressionRepository中获取到
  • 自动延期。redis中的数据也是有过期时间的

  • 装饰者模式 - SessionRepositoryFilter

    • 原生的获取session时是通过HttpServletRequest获取的
    • 这里对request进行包装,并且重写了包装request的getSession()方法

4.7 SSO单点登录

本质上是浏览器不同顶级域名的页面共享token,来达到免登录的效果。

不同顶级域名的登录要使用同一个sso登录服务器。

SSO单点登录的流程:

存在:

三个完全不同域名,Cookie 互不共享。

  • 访问 a.com(没登录)

    1. a.com 发现你没登录
    2. 把你重定向到 sso.com,并带上回调地址:sso.com/login?redirect=a.com
    3. 你在 sso.com 输入账号密码登录
    4. sso.com 验证成功
      → 在浏览器种下 sso.com 的 Cookie(全局登录态)
      → 生成一个一次性票据 ticket
    5. 把你重定向回 a.coma.com?ticket=xxxx
    6. a.com 后端拿着 ticket 去 sso.com 验证
    7. 验证通过 → a.com 认为你已登录
      → 此时你在 sso.com 是登录状态,但 a.comb.com 互相不知道。
  • 关键:再访问 b.com(实现 SSO 免登录)

    1. b.com 发现你没登录
    2. 同样把你重定向到 sso.comsso.com/login?redirect=b.com
    3. 重点来了:
      你浏览器自动带上了 sso.com 的 Cookie!
    4. sso.com 一看 Cookie 有效 → 知道你已经登录过
    5. 直接生成新 ticket,不再让你输密码
    6. 重定向回 b.comb.com?ticket=yyyy
    7. b.com 后端去 sso 验证 ticket → 登录成功
  • 所以:

    • sso.com 根本不认识 a.comb.com
    • 它只认自己的 Cookie
    • 只要 Cookie 一样 → 就是同一个人
  • 终极总结(一定要记住)

    • a.comb.com 之间互相看不见对方的 Cookie
    • 但它们都能把你重定向到 sso.com
    • 一旦到了 sso.com,浏览器就会自动带上 sso 自己的 Cookie
    • sso 通过这个 Cookie 识别:你还是刚才那个人
    • 于是直接发票据,不用再登录
  • 注意:SSO只能在一个浏览器中实现

相关推荐
NPE~2 小时前
[App逆向]环境搭建下篇 — — 逆向源码+hook实战
android·javascript·python·教程·逆向·hook·逆向分析
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年4月7日
人工智能·python·信息可视化·自然语言处理·ai编程
deephub2 小时前
向量数据库对比:Pinecone、Chroma、Weaviate 的架构与适用场景
人工智能·python·大语言模型·embedding·向量检索
星马梦缘2 小时前
强化学习实战5——BaseLine3使用自定义环境训练【输入状态向量】
pytorch·python·jupyter·强化学习·baseline3·gymnasium
Hadoop_Liang2 小时前
构建Spring Boot项目Docker镜像
spring boot·后端·docker
阿捞23 小时前
JVM排查工具单
java·jvm·python
weixin_423533993 小时前
【ubuntu20.04安装nvidia显卡驱动及pytorch】
python
I疯子3 小时前
2026-04-08 打卡第 5 天
开发语言·windows·python
自珍JAVA3 小时前
Gobrs-Async 框架
后端