02谷粒商城-高级篇二
前言
不必纠结当下,也不必太担忧未来,
人生没有无用的经历,
所以,一直走,天一定亮
173~202
6.商城业务-检索服务
6.1检索服务-搭建环境页面
把搜索的静态页面拷贝到gulimall-search
下的src/main/resources/templates
修改html
声明和thymeleaf
命名空间
修改index.html
里的静态资源地址
href="
替换为
href="/static/search/
src="
替换为
src="/static/search/
上传静态资源到nginx
的/root/mall/nginx/html/static/search
导入thymeleaf
依赖
<!-- 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
管理员运行SwicthHosts
,配置hosts
文件,配置搜索页转发地址
192.168.188.180 search.gulimall.com
配置/root/mall/nginx/conf/conf.d/gulimall.conf
server_name *.gulimall.com;
然后重启nginx
docker restart nginx
配置网关
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
访问http://search.gulimall.com/
6.2检索服务-调整页面跳转
配置nginx
配置gulimall.conf
server_name gulimall.com *.gulimall.com;
然后重启nginx
docker restart nginx
配置thymeleaf
spring:
thymeleaf:
cache: false
安装devtools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
首页跳转
# 第一处
<a href="http://gulimall.com" class="header_head_p_a1" style="width:73px;">
谷粒商城首页
</a>
# 第二处
<div class="logo">
<a href="http://gulimall.com"><img src="/static/search/./image/logo1.jpg" alt=""></a>
</div>
二级分类跳转
重命名index.html
为list.html
,并且添加SearchController
@Controller
public class SearchController {
@GetMapping(value = "/list.html")
public String listPage(){
return "list";
}
}
修改catalogLoader.js
var cata3link = $("<a href=\"http://search.gulimall.com/list.html?catalog3Id="+ctg3.id+"\" style=\"color: #999;\">" + ctg3.name + "</a>");
搜索跳转
配置gulimall-product
的src/main/resources/templates/index.html
<a href="javascript:search();" ><img src="/static/index/img/img_09.png" /></a>
6.3检索服务-检索查询参数模型分析抽取
查询参数模型:
keyword
:页面传递过来的全文匹配关键字brandId
:List<Long>
,品牌id,可以多选catalog3Id
:三级分类idsort
:排序条件:sort=price/salecount/hotscore_desc/asc
hasStock
:是否显示有货skuPrice
:价格区间查询attrs
:按照属性进行筛选pageNum
:页码_queryString
:原生的所有查询条件
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;
}
6.4检索服务-检索返回结果模型分析抽取
返回结果模型:
-
product
:List<SkuEsModel>
,查询到的所有商品信息 -
pageNum
:当前页码 -
total
:总记录数 -
totalPages
:总页码 -
pageNavs
: -
brands
:List<BrandVo>
,当前查询到的结果,所有涉及到的品牌brandId
:品牌IdbrandName
:品牌名称brandImg
:品牌图片
-
attrs
:List<AttrVo>
,当前查询到的结果,所有涉及到的所有属性attrId
:属性IdattrName
:属性名称attrValue
:属性值,可能是多个
-
catalogs
:List<CatalogVo>
,当前查询到的结果,所有涉及到的所有分类catalogId
:分类IdcatalogName
:分类名称
-
navs
:List<NavVo>
,面包屑导航数据navName
:名称navValue
:值link
:链接
@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;
}
}
6.5检索服务-检索DSL测试-查询部分
这个查询是在名为 product
的索引中查找 skuTitle
包含 "华为",catalogId
为 225,brandId
为 1、2 或 3,嵌套属性 attrs.attrId
为 "1" 且 attrs.attrValue
为 "华为 Mate60 Pro" 或 "2023",并且 hasStock
为 "true",skuPrice
在 0 到 6000 之间的商品。结果按 skuPrice
降序排序,并返回第1个结果。同时,高亮显示 skuTitle
中的匹配部分。
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": 225
}
},
{
"terms": {
"brandId": [
1,
2,
3
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "1"
}
}
},
{
"terms": {
"attrs.attrValue": [
"华为 Mate60 Pro",
"2023"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 1,
"size": 1,
"highlight": {
"fields": {
"skuTitle": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
}
}
6.6检索服务-检索DSL测试-聚合部分
聚合brandName
、brandImg
catelogName
attrName
字段时失败了
之前在映射中将 brandName
、brandImg
catelogName
attrName
字段设置为 keyword
类型,但禁用了 index
和 doc_values
。这意味着这些字段不能用于搜索、排序或聚合。如果你需要在这些字段上进行聚合操作,需要确保这些字段启用了 doc_values
。
为了保留数据需要进行数据迁移
先查询product
映射
GET product
然后在修改,删除
"index": false,
"doc_values": false
以下是完整代码
PUT gulimall_product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "long"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hosStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catelogId": {
"type": "long"
},
"brandName": {
"type": "keyword"
},
"brandImg": {
"type": "keyword"
},
"catalogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
数据迁移
POST _reindex
{
"source": {
"index": "product"
},
"dest": {
"index": "gulimall_product"
}
}
数据迁移成功,我们要修改代码里的es索引名称
这个查询是在名为 gulimall_product
的索引中查找 skuTitle
包含 "手机",catalogId
为 "225",brandId
为 "3" 或 "5",嵌套属性 attrs.attrId
为 "6" 且 attrs.attrValue
为 "海思(Hisilicon)" 或 "以官网信息为准",并且 hasStock
为 "true",skuPrice
在 0 到 7000 之间的商品。结果按 skuPrice
降序排序,并返回前2个结果。同时,高亮显示 skuTitle
中的匹配部分。聚合部分包括品牌、目录和属性的相关信息。
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "手机"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"3",
"5"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "6"
}
}
},
{
"terms": {
"attrs.attrValue": [
"海思(Hisilicon)",
"以官网信息为准"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 7000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 2,
"highlight": {
"fields": {
"skuTitle": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg":{
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg":{
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}
6.7检索服务-SearchRequest构建-检索
主要步骤:
-
1.准备检索请求
- 构建
bool-query
:BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();
bool-must
:skuTitle
商品名称查询bool-fiter
:boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()))
:catalogId
三级分类查询boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()))
:brandId
品牌查询attrs
基础属性查询boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1))
:hasStock
库存查询skuPrice
商品价格区间查询
- 构建
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
/**
* 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
*/
//1. 构建bool-query
BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();
//1.1 bool-must
if(!StringUtils.isEmpty(param.getKeyword())){
boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
}
//1.2 bool-fiter
//1.2.1 catelogId
if(null != param.getCatalog3Id()){
boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
}
//1.2.2 brandId
if(null != param.getBrandId() && param.getBrandId().size() >0){
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
}
//1.2.3 attrs
if(param.getAttrs() != null && param.getAttrs().size() > 0){
param.getAttrs().forEach(item -> {
//attrs=1_5寸:8寸&2_16G:8G
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//attrs=1_5寸:8寸
String[] s = item.split("_");
String attrId=s[0];
String[] attrValues = s[1].split(":");//这个属性检索用的值
boolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs",boolQuery, ScoreMode.None);
boolQueryBuilder.filter(nestedQueryBuilder);
});
}
//1.2.4 hasStock
if(null != param.getHasStock()){
boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1));
}
//1.2.5 skuPrice
if(!StringUtils.isEmpty(param.getSkuPrice())){
//skuPrice形式为:1_500或_500或500_
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
String[] price = param.getSkuPrice().split("_");
if(price.length==2){
rangeQueryBuilder.gte(price[0]).lte(price[1]);
}else if(price.length == 1){
if(param.getSkuPrice().startsWith("_")){
rangeQueryBuilder.lte(price[1]);
}
if(param.getSkuPrice().endsWith("_")){
rangeQueryBuilder.gte(price[0]);
}
}
boolQueryBuilder.filter(rangeQueryBuilder);
}
//封装所有的查询条件
searchSourceBuilder.query(boolQueryBuilder);
6.8检索服务-SearchRequest构建-排序、分页、高亮&测试
主要步骤:
-
排序:
searchSourceBuilder.sort(sortFileds[0],sortOrder);
-
分页
searchSourceBuilder.from((param.getPageNum()-1)*EsConstant.PRODUCT_PAGESIZE);
:页码searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
:每页条数
-
高亮
HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("skuTitle"); highlightBuilder.preTags("<b style='color:red'>"); highlightBuilder.postTags("</b>"); searchSourceBuilder.highlighter(highlightBuilder);
-
测试:
/**
* 排序,分页,高亮
*/
//排序
//形式为sort=hotScore_asc/desc
if(!StringUtils.isEmpty(param.getSort())){
String sort = param.getSort();
String[] sortFileds = sort.split("_");
SortOrder sortOrder="asc".equalsIgnoreCase(sortFileds[1])?SortOrder.ASC:SortOrder.DESC;
searchSourceBuilder.sort(sortFileds[0],sortOrder);
}
//分页
searchSourceBuilder.from((param.getPageNum()-1)*EsConstant.PRODUCT_PAGESIZE);
searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
//高亮
if(!StringUtils.isEmpty(param.getKeyword())){
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
searchSourceBuilder.highlighter(highlightBuilder);
}
测试http://localhost:8208/list.html?keyword=华为&brandId=2&catalog3Id=225&sort=skuPrice_asc&hasStock=1&skuPrice=0_7000
把构建的语句打印出来
在kibana
执行打印出来的查询语句,能够查询出来即可
6.9检索服务-SearchRequest构建-聚合
主要步骤:
- 品牌聚合
- 分类聚合
- 属性信息聚合
代码
/**
* 聚合分析
*/
//1. 按照品牌进行聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
brand_agg.field("brandId").size(50);
//1.1 品牌的子聚合-品牌名聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg")
.field("brandName").size(1));
//1.2 品牌的子聚合-品牌图片聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg")
.field("brandImg").size(1));
searchSourceBuilder.aggregation(brand_agg);
//2. 按照分类信息进行聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg");
catalog_agg.field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
searchSourceBuilder.aggregation(catalog_agg);
//2. 按照属性信息进行聚合
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
//2.1 按照属性ID进行聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
attr_agg.subAggregation(attr_id_agg);
//2.1.1 在每个属性ID下,按照属性名进行聚合
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
//2.1.1 在每个属性ID下,按照属性值进行聚合
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
searchSourceBuilder.aggregation(attr_agg);
log.debug("构建的DSL语句 {}",searchSourceBuilder.toString());
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder);
return searchRequest;
测试,查看聚合出来的品牌、分类、属性信息
6.10检索服务-SearchRequest分析&封装
主要步骤:
- 1.商品信息
- 2.当前商品涉及到的所有属性信息
- 3.当前商品涉及到的所有品牌信息
- 4.当前商品涉及到的所有分类信息
- 5.分页信息
- 页码
- 总记录数
- 总页码
代码
SearchResult result = new SearchResult();
//1、返回的所有查询到的商品
SearchHits hits = response.getHits();
List<SkuEsModel> esModels = new ArrayList<>();
//遍历所有商品信息
if (hits.getHits() != null && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
//判断是否按关键字检索,若是就显示高亮,否则不显示
if (!StringUtils.isEmpty(param.getKeyword())) {
//拿到高亮信息显示标题
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String skuTitleValue = skuTitle.getFragments()[0].string();
esModel.setSkuTitle(skuTitleValue);
}
esModels.add(esModel);
}
}
result.setProduct(esModels);
//2、当前商品涉及到的所有属性信息
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
//获取属性信息的聚合
ParsedNested attrsAgg = response.getAggregations().get("attr_agg");
ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//1、得到属性的id
long attrId = bucket.getKeyAsNumber().longValue();
attrVo.setAttrId(attrId);
//2、得到属性的名字
ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
attrVo.setAttrName(attrName);
//3、得到属性的所有值
ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
//3、当前商品涉及到的所有品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
//获取到品牌的聚合
ParsedLongTerms brandAgg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
//1、得到品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
brandVo.setBrandId(brandId);
//2、得到品牌的名字
ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandName(brandName);
//3、得到品牌的图片
ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandImg(brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
//4、当前商品涉及到的所有分类信息
//获取到分类的聚合
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
ParsedLongTerms catalogAgg = response.getAggregations().get("catalog_agg");
for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
//得到分类id
String keyAsString = bucket.getKeyAsString();
catalogVo.setCatalogId(Long.parseLong(keyAsString));
//得到分类名
ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalogName);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
//===============以上可以从聚合信息中获取====================//
//5、分页信息-页码
result.setPageNum(param.getPageNum());
//5、1分页信息、总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
//5、2分页信息-总页码-计算
int totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ?
(int)total / EsConstant.PRODUCT_PAGESIZE : ((int)total / EsConstant.PRODUCT_PAGESIZE + 1);
result.setTotalPages(totalPages);
6.11检索服务-验证结果封装正确性
查看SearchResponse
返回结果
获取高亮显示的标题
if (!StringUtils.isEmpty(param.getKeyword())) {
//拿到高亮信息显示标题
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String skuTitleValue = skuTitle.getFragments()[0].string();
esModel.setSkuTitle(skuTitleValue);
}
6.12检索服务-页面基本数据渲染
主要步骤:
- 商品列表:
rig_tab
- 品牌信息:
JD_nav_wrap
- 分类信息:
JD_pre
- 属性信息:
JD_pre
商品列表:rig_tab
<div class="rig_tab">
<div th:each="product : ${result.getProduct()}">
<div class="ico">
<i class="iconfont icon-weiguanzhu"></i>
<a href="/static/search/#">关注</a>
</div>
<p class="da">
<a th:href="|http://item.gulimall.com/${product.skuId}.html|" >
<img class="dim" th:src="${product.skuImg}">
</a>
</p>
<ul class="tab_im">
<li><a href="/static/search/#" title="黑色">
<img th:src="${product.skuImg}"></a></li>
</ul>
<p class="tab_R">
<span th:text="'¥' + ${product.skuPrice}">¥5199.00</span>
</p>
<p class="tab_JE">
<a href="/static/search/#" th:utext="${product.skuTitle}">
Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
</a>
</p>
<p class="tab_PI">已有<span>11万+</span>热门评价
<a href="/static/search/#">二手有售</a>
</p>
<p class="tab_CP"><a href="/static/search/#" title="谷粒商城Apple产品专营店">谷粒商城Apple产品...</a>
<a href='#' title="联系供应商进行咨询">
<img src="/static/search/img/xcxc.png">
</a>
</p>
<div class="tab_FO">
<div class="FO_one">
<p>自营
<span>谷粒商城自营,品质保证</span>
</p>
<p>满赠
<span>该商品参加满赠活动</span>
</p>
</div>
</div>
</div>
</div>
品牌信息:JD_nav_wrap
<!--品牌-->
<div class="JD_nav_wrap">
<div class="sl_key">
<span><b>品牌:</b></span>
</div>
<div class="sl_value">
<div class="sl_value_logo">
<ul>
<li th:each="brand : ${result.brands}">
<a href="#"
th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
<img th:src="${brand.brandImg}" alt="">
<div th:text="${brand.brandName}">
华为(HUAWEI)
</div>
</a>
</li>
</ul>
</div>
</div>
<div class="sl_ext">
<a href="/static/search/#">
更多
<i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
<b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
</a>
<a href="/static/search/#">
多选
<i>+</i>
<span>+</span>
</a>
</div>
</div>
分类信息:JD_pre
<div class="JD_pre">
<div class="sl_key">
<span><b>分类:</b></span>
</div>
<div class="sl_value">
<ul>
<li th:each="catalog : ${result.catalogs}">
<a href="/static/search/#"
th:href="${'javascript:searchProducts("catalog3Id",'+catalog.catalogId+')'}"
th:text="${catalog.catalogName}">5.56英寸及以上
</a>
</li>
</ul>
</div>
<div class="sl_ext">
<a href="/static/search/#">
更多
<i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
<b style='background: url("image/search.ele.png")no-repeat 3px -44px'></b>
</a>
<a href="/static/search/#">
多选
<i>+</i>
<span>+</span>
</a>
</div>
</div>
属性信息
<!--其它的所有需要展示的属性-->
<div class="JD_pre" th:each="attr : ${result.attrs}">
<div class="sl_key">
<span th:text="${attr.attrName}">屏幕尺寸:</span>
</div>
<div class="sl_value">
<ul>
<li th:each="attrValue : ${attr.attrValue}">
<a href="/static/search/#"
th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+attrValue+'")'}"
th:text="${attrValue}">5.56英寸及以上
</a>
</li>
</ul>
</div>
</div>
默认查询所有商品,不管有没有库存
6.13检索服务-页面筛选条件渲染
主要步骤:
-
条件拼接方法
-
品牌信息条件拼接
-
分类信息条件拼接
-
基础属性条件拼接
条件拼接方法,需要判断一开始的查询链接有没有"?"
function searchProducts(name, value) {
//原来的页面
var href = location.href + "";
if (href.indexOf("?") != -1) {
location.href = location.href + "&" + name + "=" + value;
} else {
location.href = location.href + "?" + name + "=" + value;
}
}
品牌信息条件拼接,注意使用动态拼接后双引号(''')使用"
转义
<a href="#" th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
<img th:src="${brand.brandImg}" alt="">
<div th:text="${brand.brandName}">
华为(HUAWEI)
</div>
</a>
分类信息条件拼接,注意使用动态拼接后双引号(''')使用"
转义
<a href="/static/search/#" th:href="${'javascript:searchProducts("catalog3Id",'+catalog.catalogId+')'}"
th:text="${catalog.catalogName}">5.56英寸及以上
</a>
基础属性条件拼接,注意使用动态拼接后双引号(''')使用"
转义
<a href="/static/search/#"
th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+attrValue+'")'}"
th:text="${attrValue}">5.56英寸及以上
</a>
6.14检索服务-页面分页数据渲染
主要步骤:
- 1.搜索页搜索功能
- 2.分页
th:attr="pn=${result.pageNum - 1}"
:自定义特性,代表当前页数th:if="${result.pageNum>1}"
:当前页数>1显示上一页style=${navs == result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
:选中时显示样式<a class="page_a" th:attr="pn=${navs}" th:each="navs : ${result.pageNavs}">\[\[${navs}\]\]</a>
:循环显示页数th:if="${result.pageNum<result.totalPages}"
:当前页<总页数显示下一页
搜索页搜索功能
分页样式
<!--分页-->
<div class="filter_page">
<div class="page_wrap">
<span class="page_span1">
<a class="page_a" th:attr="pn=${result.pageNum - 1}" href="/static/search/#"
th:if="${result.pageNum>1}">
< 上一页
</a>
<a class="page_a"
th:attr="pn=${navs},style=${navs == result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
th:each="navs : ${result.pageNavs}">[[${navs}]]</a>
<a class="page_a" th:attr="pn=${result.pageNum + 1}"
th:if="${result.pageNum<result.totalPages}">
下一页 >
</a>
</span>
<span class="page_span2">
<em>共<b>[[${result.totalPages}]]</b>页 到第</em>
<input type="number" value="1">
<em>页</em>
<a class="page_submit">确定</a>
</span>
</div>
</div>
分页事件
$(".page_a").click(function () {
var pn = $(this).attr("pn");
var href = location.href;
if (href.indexOf("pageNum") != -1) {
//替换pageNum
location.href = replaceParamVal(href, "pageNum", pn,false);
} else {
location.href = location.href + "&pageNum=" + pn;
}
return false;
})
6.15检索服务-页面排序功能
主要步骤:
-
1.自定义特性
sort="hotScore"
sort="saleCount"
sort="skuPrice"
-
2.点击事件:
- 清除
sort_a
的所有样式(选中高亮)和升序↑
降序↓
的标志 - 切换
sort_a
的升序降序的样式 - 清除之前的升序
↑
降序↓
的标志,如果有desc
降序样式就添加降序↓
的标志,没有desc
降序样式就添加升序↑
的标志,
- 清除
样式
<div class="filter_top_left">
<a class="sort_a" sort="hotScore" href="/static/search/#">综合排序</a>
<a class="sort_a" sort="saleCount" href="/static/search/#">销量</a>
<a class="sort_a" sort="skuPrice" href="/static/search/#">价格</a>
</div>
点击事件
// 排序
$(".sort_a").click(function () {
changeStyle(this);
let sort = $(this).attr("sort");
sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc";
location.href = replaceParamVal(location.href, "sort", sort,false);
// 禁用默认行为 防止a标签跳转
return false;
});
function changeStyle(ele) {
// location.href = replaceParamVal(href, "pageNum", pn,flase);
// color: #333; border-color: #ccc; background: #fff
// color: #fff; border-color: #e4393c; background: #e4393c
$(".sort_a").css({"color": "#333", "border-color": "#ccc", "background": "#fff"});
$(".sort_a").each(function () {
let text = $(this).text().replace("↓", "").replace("↑", "");
$(this).text(text);
})
$(ele).css({"color": "#FFF", "border-color": "#e4393c", "background": "#e4393c"});
$(ele).toggleClass("desc");
if ($(ele).hasClass("desc")) {
let text = $(ele).text().replace("↓", "").replace("↑", "");
text = text + "↓";
$(ele).text(text);
} else {
let text = $(ele).text().replace("↓", "").replace("↑", "");
text = text + "↑";
$(ele).text(text);
}
};
6.16检索服务-页面排序字段回显
主要步骤:
-
th:with="p = ${param.sort}"
:param.sort
获取地址栏中sort
的参数,不支持直接使用param
比较 -
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
:如果param
不为空,并且sort
参数hotScore
开始,desc
结尾,class
为sort_a desc
,否则为sort_a
-
th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore'))?'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }"
:默认是综合排序,如果param
为空,并且sort
参数hotScore
开始,设置为选中高亮样式 -
[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ?'↑':'↓' }]]
:如果param
不为空,并且sort
参数hotScore
开始,desc
结尾,添加升序标志↑
样式
<!--综合排序-->
<div class="filter_top">
<div class="filter_top_left" th:with="p = ${param.sort}, priceRange = ${param.skuPrice}">
<a sort="hotScore"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
综合排序[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') &&
#strings.endsWith(p,'desc')) ?'↑':'↓' }]]</a>
<a sort="saleCount"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
销量[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') &&
#strings.endsWith(p,'desc'))?'↑':'↓' }]]</a>
<a sort="skuPrice"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'desc')) ? 'sort_a desc' : 'sort_a'}"
th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice')) ?
'color: #fff; border-color: #e4393c; background: #e4393c;':'color: #333; border-color: #ccc; background: #fff;' }">
价格[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') &&
#strings.endsWith(p,'desc'))?'↑':'↓' }]]</a>
<a sort="hotScore" class="sort_a">评论分</a>
<a sort="hotScore" class="sort_a">上架时间</a>
<input id="skuPriceFrom" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
style="width: 100px; margin-left: 30px">
-
<input id="skuPriceTo" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
style="width: 100px">
<button id="skuPriceSearchBtn">确定</button>
</div>
<div class="filter_top_right">
<span class="fp-text">
<b>1</b><em>/</em><i>169</i>
</span>
<a href="/static/search/#" class="prev"><</a>
<a href="/static/search/#" class="next"> > </a>
</div>
</div>
6.17检索服务-页面价格区间搜索
价格区间
主要步骤:
-
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
:param.skuPrice
获取地址栏skuPrice
参数 -
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
:如果priceRange
不为空,按照_
截取priceRange
,获取priceRange
之前的数字,比如5000_8000,就是5000 -
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
:如果priceRange
不为空,按照_
截取priceRange
,获取priceRange
之后的数字,比如5000_8000,就是8000 -
查询时拼接
skuPriceFrom
和skuPriceTo
文本框的参数添加到地址栏
样式
<input id="skuPriceFrom" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
style="width: 100px; margin-left: 30px">
-
<input id="skuPriceTo" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
style="width: 100px">
<button id="skuPriceSearchBtn">确定</button>
事件
// 价格区间
$("#skuPriceSearchBtn").click(function () {
let from = $(`#skuPriceFrom`).val();
let to = $(`#skuPriceTo`).val();
let query = from + "_" + to;
location.href = replaceParamVal(location.href, "skuPrice", query,false);
});
仅显示有货
主要步骤:
th:with="check = ${param.hasStock}"
:获取地址栏hasStock
参数th:checked="${#strings.equals(check,'1')?true:false}"
:如果hasStock
为1就选中- 选中和取消选中的时候地址栏更新
hasStock
参数
样式
<a th:with="check = ${param.hasStock}">
<input id="showHasStock" type="checkbox" th:checked="${#strings.equals(check,'1')?true:false}">
仅显示有货
</a>
事件
// 是否有库存
$("#showHasStock").change(function () {
//alert( $(this).prop("checked") );
if( $(this).prop("checked") ) {
location.href = replaceParamVal(location.href,"hasStock",1,false);
} else {
let re = eval('/(hasStock=)([^&]*)/gi');
location.href = (location.href+"").replace(re,"");
}
return false;
});
keyword
function searchProducts(name, value) {
//原來的页面
location.href = replaceParamVal(location.href,name,value,true)
}
6.18检索服务-面包屑导航
主要步骤:
- 1.获取基础属性
attrs
(attrs=1_5寸:8寸
) - 2.远程调用商品服务根据
attrId
查询属性名称
gulimall-search
导入远程调用依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
远程调用
获取基础属性attrs
(attrs=1_5寸:8寸
)
6.19检索服务-条件删除与URL编码问题
主要步骤:
- 修改编码为
UTF-8
- 替换空格,将后端的空格符号
+
替换为%20
- 前端动态生成面包屑导航
修改编码为UTF-8
,将后端的空格符号+
替换为%20
前端动态生成面包屑导航
<div class="JD_ipone_one c">
<!-- 遍历面包屑功能 -->
<a th:href="${nav.link}" th:each="nav:${result.navs}"><span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span> x</a>
</div>
6.20检索服务-条件筛选联动
主要步骤:
- 1.远程调用
gulimall-product
查询品牌服务 - 2.替换品牌链接地址
- 3.远程调用
gulimall-product
查询商品分类
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
List<SearchResult.NavVo> navVos = result.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setNavName("品牌");
R r = productFeignService.brandsInfo(param.getBrandId());
if (r.getCode() == 0) {
List<BrandVo> brands = r.getData("brand", new TypeReference<List<BrandVo>>() {
});
String replace = "";
StringBuffer buffer = new StringBuffer();
for (BrandVo brandVo : brands) {
buffer.append(brandVo.getName() + ";");
replace = replaceQueryString(param, "brandId", brandVo.getBrandId().toString());
}
navVo.setNavValue(buffer.toString());// 品牌拼接值
navVo.setLink("http://search.gulimall.com/list.html?" + replace);// 回退品牌面包屑等于删除所有品牌条件
navVos.add(navVo);
}
}
// 构建面包屑导航数据_分类
if (param.getCatalog3Id() != null) {
List<SearchResult.NavVo> navs = result.getNavs();
SearchResult.NavVo nav = new SearchResult.NavVo();
nav.setNavName("分类");
R r = productFeignService.categoryinfo(param.getCatalog3Id());
if (r.getCode() == 0) {
CategoryVo categoryVo = r.getData(new TypeReference<CategoryVo>() {
});
nav.setNavValue(categoryVo.getName());// 分类名
}
// StringBuffer buffer = new StringBuffer();
// String replace = replaceQueryString(param, "catalog3Id", param.getCatalog3Id().toString());
// nav.setLink("http://search.gulimall.com/list.html?" + replace);
navs.add(nav);
}
7.商城业务-异步
7.1异步复习
主要步骤:
- 1.继承
Thread
- 2.实现
Runnable
接口 - 3.实现
Callable
接口 - 4.线程池:线程池直接提交任务
区别:
- 1、2不能得到返回值
- 1、2、3都不能直接控制资源
- 4可以控制资源
public static ExecutorService service = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 1.继承Thread
* 2.实现Runnable接口
* 3.实现Callable接口
* 4.线程池:线程池直接提交任务
*
* 区别:
* 1、2不能得到返回值
* 1、2、3都不能直接控制资源
* 4可以控制资源
* */
System.out.println("main......start.....");
Thread thread = new Thread01();
thread.start();
System.out.println("main......end.....");
System.out.println("main......start.....");
Runable01 runable01 = new Runable01();
new Thread(runable01).start();
System.out.println("main......start.....");
System.out.println("main......start.....");
FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
new Thread(futureTask).start();
System.out.println(futureTask.get());
System.out.println("main......end.....");
System.out.println("main......start.....");
service.execute(new Runable01());
Future<Integer> submit = service.submit(new Callable01());
submit.get();
System.out.println("main......end.....");
}
private static void threadPool() {
ExecutorService threadPool = new ThreadPoolExecutor(
200,
10,
10L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(10000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//定时任务的线程池
ExecutorService service = Executors.newScheduledThreadPool(2);
}
public static class Thread01 extends Thread {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}
}
public static class Runable01 implements Runnable {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}
}
public static class Callable01 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}
}
7.2线程池详解
线程池的七大参数:
corePoolSize
:池中一直保持的线程的数量,即使线程空闲。除非设置allowCoreThreadTimeOut
maximumPoolSize
:池中允许的最大的线程数keepAliveTime
:当线程数大于核心线程数的时候,线程在最大多长时间没有接到新任务就会终止释放, 最终线程池维持在corePoolSize
大小unit
:时间单位workQueue
:阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了corePoolSize
大小,就会放在这里等待空闲线程执行。threadFactory
:创建线程的工厂,比如指定线程名等handler
:拒绝策略,如果线程满了,线程池就会使用拒绝策略。
一个线程池 core 7;max 20;queue:50;100并发进来怎么分配ide
7个立即执行,50个放入队列,在执行13个(max-core)
,剩下30个使用拒绝策略
常见的 4 种线程池
newCachedThreadPool
:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newFixedThreadPool
:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。newScheduledThreadPool
:创建一个定长线程池,支持定时及周期性任务执行。newSingleThreadExecutor
:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// Executors.newCachedThreadPool() core是0,所有都可回收()
// Executors.newFixedThreadPao() 固定大小,core=max;都不可回收
// Executors.newScheduledThreudPool() 定时任务的线程池
// Executors.newsingleThreadExecutor() 单线程的线程池,后台从队列里面获取任务,挨个执行
7.3CompletableFuture
为什么使用线程池
- 降低资源的消耗
- 通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗
- 提高响应速度
- 因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务 的状态,当任务来时无需创建新的线程就能执行
- 提高线程的可管理性
- 线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来 的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配
CompletableFuture 异步编排
业务场景
在 Java 8 中, 新增加了一个包含 50 个方法左右的类: CompletableFuture,提供了非常强大的 Future 的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以 通过回调的方式处理计算结果,并且提供了转换和组合 CompletableFuture 的方法。 CompletableFuture 类实现了 Future 接口,所以你还是可以像以前一样通过get
方法阻塞或 者轮询的方式获得结果,但是这种方式不推荐使用。 CompletableFuture 和 FutureTask 同属于 Future 接口的实现类,都可以获取线程的执行结果
7.4CompletableFuture-启动异步任务
CompletableFuture
-
runxxx 都是没有返回结果的
-
supplyxxx 都是可以获取返回结果的
-
可以传入自定义的线程池,否则就用默认的线程池
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main......start.....");
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
CompletableFuture<Integer> CFres = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor);
Integer res = CFres.get();
System.out.println(res);
System.out.println("main......end.....");
}
7.5CompletableFuture-完成回调与异步感知
计算完成时回调方法
whenComplete 可以处理正常和异常的计算结果,exceptionally 处理异常情况
whenComplete 和 whenCompleteAsync 的区别:
- whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
- whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池
System.out.println("main......start.....");
CompletableFuture<Integer> CFres = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res,ex)->{
System.out.println("异步任务执行完成...结果是:"+res+";异常是:"+ex);
}).exceptionally(throwable -> {
// 可以感知异常 同时返回默认值
return 10;
});
Integer res = CFres.get();
System.out.println(res);
System.out.println("main......end.....");
7.6CompletableFuture-handle最终处理
handle
和 complete 一样,可对结果做最后的处理(可处理异常),可改变返回值。
// handle
System.out.println("main......start.....");
CompletableFuture<Integer> CFres = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
// int i = 10 / 0; //exception
System.out.println("运行结果:" + i);
return i;
}, executor).handle((res, ex) -> {
if (res != null) {
return res * 2;
}
if (ex != null) {
return 0;
}
return 0;
});
Integer res = CFres.get();
System.out.println(res);
System.out.println("main......end.....");
7.7CompletableFuture-线程串行化
thenRun、thenAccept、thenApply
- thenRun 方法:只要上面的任务执行完成,就开始执行 thenRun,只是处理完任务后,执行 thenRun 的后续操作
- thenAccept 方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
- thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前 任务的返回值。
- 带有 Async 默认是异步执行的。
- 同之前。 以上都要前置任务成功完成
// 线程串行化
System.out.println("main......start.....");
CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 5;
// int i = 10 / 0; //exception
System.out.println("运行结果:" + i);
return i;
}, executor).thenRunAsync(() -> {
System.out.println("任务执行完成" + Thread.currentThread().getId());
}, executor);
CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 5;
// int i = 10 / 0; //exception
System.out.println("运行结果:" + i);
return i;
}, executor).thenAcceptAsync(res -> {
System.out.println("任务执行完成" + Thread.currentThread().getId());
System.out.println("上一步结果:"+res);
}, executor);
// supplyAsync:接受上一步结果,有返回值
CompletableFuture<String> CFres = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 5;
// int i = 10 / 0; //exception
System.out.println("运行结果:" + i);
return i;
}, executor).thenApplyAsync(res -> {
return "hello" + res;
}, executor);
String res = CFres.get();
System.out.println(res);
System.out.println("main......end.....");
7.8CompletableFuture-俩任务组合-都要完成
俩任务组合-都要完成
- runAfterBoth:组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后, 处理该任务。
- thenAcceptBoth:组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有 返回值。
- thenCombine:组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值
创建2个任务
// 组合任务 都要完成
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
System.out.printf("任务1线程:" + Thread.currentThread().getId());
System.out.println("任务1结束");
return "hello2";
}, executor);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
System.out.printf("任务2线程:" + Thread.currentThread().getId());
System.out.println("任务2结束");
return "hello1";
}, executor);
runAfterBothAsync
future1.runAfterBothAsync(future2,()->{
System.out.printf("任务1,任务2完成");
},executor);
thenAcceptBothAsync
future1.thenAcceptBothAsync(future2,(f1,f2)->{
System.out.println("任务1结果:"+f1);
System.out.println("任务2结果:"+f2);
System.out.println("任务结束");
},executor);
CompletableFuture
CompletableFuture<String> future3 = future1.thenCombineAsync(future2, (f1, f2) -> {
System.out.println("任务1结果:" + f1);
System.out.println("任务2结果:" + f2);
return f1 + " " + f2 + " " + "hello3";
}, executor);
System.out.printf("任务3结果:" + future3.get());
7.9CompletableFuture-俩任务组合-一个完成
俩任务组合-一个完成
- runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返 回值。
- acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。
- applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。
创建2个任务
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程:" + Thread.currentThread().getId());
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
System.out.println("任务1结束");
return "hello1";
}, executor);
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("任务2结束");
return "hello2";
}, executor);
runAfterEitherAsync
future1.runAfterEitherAsync(future2,()->{
System.out.println("任务3线程:" + Thread.currentThread().getId());
System.out.println("任务3结束");
});
acceptEitherAsync
future1.acceptEitherAsync(future2, (res) -> {
System.out.println("接受结果:" + res);
System.out.println("任务3线程:" + Thread.currentThread().getId());
System.out.println("任务3结束");
});
applyToEitherAsync
CompletableFuture<String> future3 = future1.applyToEitherAsync(future2, (res) -> {
System.out.println("接受结果:" + res);
System.out.println("任务3线程:" + Thread.currentThread().getId());
System.out.println("任务3结束");
return res + " hello3";
});
System.out.println("任务3结果:" + future3.get());
7.10CompletableFuture-多任务组合
多任务组合
allOf
:等待所有任务完成anyOf
:只要有一个任务完成
创建3个任务,任务2休眠3s,模拟耗时操作
System.out.println("main......start.....");
// 多任务组合
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("获取图片");
return "hello1.jpg";
}, executor);
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
System.out.println("获取属性");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "白色12+512GB";
}, executor);
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
System.out.println("获取描述");
return "华为Mate70";
}, executor);
allOf
CompletableFuture<Void> allof = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
allof.get(); //阻塞等待三个任务完成
// // allof.join();
System.out.println(futureImg.get());
System.out.println(futureAttr.get());
System.out.println(futureDesc.get());
anyOf
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get();
System.out.println(anyOf.get());
System.out.println("main......end.....");
8.商城业务-商品详情
8.1环境搭建
主要步骤:
- 1.配置
Hosts
- 2.配置
nginx
- 3.上传商品服务的静态资源到
nginx
的static/item
目录下 - 4.配置
item.gulimall.com
的网关 - 5.
gulimall-product
添加控制器跳转和商品静态页面 - 6.
gulimall-search
修改商品详情跳转地址
管理员运行SwitchHosts
192.168.188.180 item.gulimall.com
因为*.gulimall.com
可以匹配item.gulimall.com
,这里不需要配置,检查一下即可
上传商品服务的静态资源到nginx
的static/item
目录下
配置item.gulimall.com
的网关,让item.gulimall.com
可以转发到商品服务
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=gulimall.com,item.gulimall.com
gulimall-product
添加商品静态页面,并改名item.html
替换静态资源地址为nginx
地址
href="
href="/static/item/
替换图片地址为nginx
地址
src="
src="/static/item/
gulimall-product
添加控制器跳转
@Controller
public class ItemController {
/**
* 展示当前sku的详情
* @param skuId
* @return
*/
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId, Model model) throws ExecutionException, InterruptedException {
System.out.println("准备查询" + skuId + "详情");
return "item";
}
}
gulimall-search
修改商品详情的链接地址
<p class="da">
<a th:href="|http://item.gulimall.com/${product.skuId}.html|">
<img class="dim" th:src="${product.skuImg}">
</a>
</p>
访问http://item.gulimall.com/1.html
8.2模型抽取
主要步骤:
- 1.
sku
基本信息获取pms_sku_info
- 2.
sku
的图片信息pms_sku_images
- 3.获取
spu
的销售属性组合 - 4.获取
spu
的介绍 - 5.获取
spu
的规格参数信息。
@Data
public class SkuItemVo {
//1、sku基本信息的获取 pms_sku_info
private SkuInfoEntity info;
private boolean hasStock = true;
//2、sku的图片信息 pms_sku_images
private List<SkuImagesEntity> images;
//3、获取spu的销售属性组合
private List<SkuItemSaleAttrVo> saleAttr;
//4、获取spu的介绍
private SpuInfoDescEntity desc;
//5、获取spu的规格参数信息
private List<SpuItemAttrGroupVo> groupAttrs;
}
8.3规格参数
主要步骤:
- 1.sku基本信息的获取
- 2.sku的图片信息
- 4.获取spu的介绍
- 5.获取spu的规格参数信息
1、2、3简单的查询
@Override
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
// 1、sku基本信息的获取 pms_sku_info
SkuInfoEntity info = this.getById(skuId);
skuItemVo.setInfo(info);
// 2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(imagesEntities);
// 3、获取spu的销售属性组合
// 4、获取spu的介绍
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(info.getSpuId());
skuItemVo.setDesc(spuInfoDescEntity);
// 5、获取spu的规格参数信息
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(info.getSpuId(), info.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
return skuItemVo;
}
获取spu的规格参数信息
AttrGroupServiceImpl
@Override
public List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {
//1、查出当前spu对应的所有属性的分组信息以及当前分组下的所有属性对应的值
AttrGroupDao baseMapper = this.getBaseMapper();
List<SpuItemAttrGroupVo> vos = baseMapper.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
return vos;
}
AttrGroupDao.xml
<resultMap id="spuAttrGroup" type="com.peng.product.vo.SpuItemAttrGroupVo">
<result property="groupName" column="attr_group_name"/>
<collection property="attrs" ofType="com.peng.product.vo.Attr">
<result property="attrId" column="attr_id"></result>
<result property="attrName" column="attr_name"></result>
<result property="attrValue" column="attr_value"></result>
</collection>
</resultMap>
<select id="getAttrGroupWithAttrsBySpuId" resultMap="spuAttrGroup">
SELECT
product.spu_id,
pag.attr_group_id,
pag.attr_group_name,
product.attr_id,
product.attr_name,
product.attr_value
FROM
pms_product_attr_value product
LEFT JOIN pms_attr_attrgroup_relation paar ON product.attr_id = paar.attr_id
LEFT JOIN pms_attr_group pag ON paar.attr_group_id = pag.attr_group_id
WHERE
product.spu_id = #{spuId}
AND pag.catelog_id = #{catalogId}
</select>
单元测试
@Test
public void test(){
List<SpuItemAttrGroupVo> attrGroupWithAttrsBySpuId = attrGroupService.getAttrGroupWithAttrsBySpuId(1L, 225L);
System.out.println(attrGroupWithAttrsBySpuId);
}
8.4销售属性组合
主要步骤:
- 查询sku对应的销售属性
- 单元测试
SkuSaleAttrValueDao
List<SkuItemSaleAttrVo> getSaleAttrBySpuId(@Param("spuId")Long spuId);
SkuSaleAttrValueDao.xml
<select id="getSaleAttrBySpuId" resultMap="skuItemSaleAttrVo">
SELECT
ssav.attr_id attr_id,
ssav.attr_name attr_name,
ssav.attr_value,
group_concat( DISTINCT info.sku_id ) sku_ids
FROM
pms_sku_info info
LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
WHERE
info.spu_id = #{spuId}
GROUP BY
ssav.attr_id,
ssav.attr_name,
ssav.attr_value
</select>
单元测试
@Test
public void test2(){
List<SkuItemSaleAttrVo> saleAttrBySpuId = skuSaleAttrValueService.getSaleAttrBySpuId(1L);
System.out.println(saleAttrBySpuId);
}
8.5详情页渲染
主要步骤:
- 默认图片
- 商品标题
- 有/无货(库存)状态
- 销售属性
- 商品介绍
- 规格与包装
默认图片
<div class="imgbox">
<div class="probox">
<img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
<div class="hoverbox"></div>
</div>
<div class="showbox">
<img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
</div>
</div>
<div class="box-lh">
<div class="box-lh-one">
<ul>
<li th:each="img : ${item.images}"><img th:src="${img.imgUrl}"/></li>
</ul>
</div>
<div id="left">
<
</div>
<div id="right">
>
</div>
</div>
<div class="boxx-one">
<ul>
<li>
<span>
<img src="/static/item/img/b769782fe4ecca40913ad375a71cb92d.png" alt="" />关注
</span>
<span>
<img src="/static/item/img/9224fcea62bfff479a6712ba3a6b47cc.png" alt="" />
对比
</span>
</li>
<li>
</li>
</ul>
</div>
商品标题
<div class="box-name" th:text="${item.info.skuTitle}">
华为 HUAWEI Mate 10 6GB+128GB 亮黑色 移动联通电信4G手机 双卡双待
</div>
<div class="box-hide" th:text="${item.info.skuSubtitle}">预订用户预计11月30日左右陆续发货!麒麟970芯片!AI智能拍照!
<a href="/static/item/"><u></u></a>
</div>
无货
<li>
<span th:text="${item.hasStock?'有货':'无货'}">无货</span>, 此商品暂时售完
</li>
销售属性
<div class="box-attr-3">
<div class="box-attr clear" th:each="attr : ${item.saleAttr}">
<dl>
<dt>选择[[${attr.attrName}]]</dt>
<dd th:each="val : ${attr.attrValues}">
<a th:attr=" class=${#lists.contains(#strings.listSplit(val.skuIds,','),item.info.skuId.toString())
? 'sku_attr_value checked': 'sku_attr_value'}, skus=${val.skuIds} "
>
[[${val.attrValue}]]
<!-- <img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
</a>
</dd>
</dl>
</div>
</div>
商品介绍
<div class="shanpinsssss">
<img class="xiaoguo" th:src="${descp}"
th:each="descp : ${#strings.listSplit(item.desc.decript,',')}"/>
</div>
规格与包装
<li class="baozhuang actives" id="li2">
<div class="guiGebox">
<div class="guiGe" th:each="group : ${item.groupAttrs}">
<h3 th:text="${group.groupName}">主体</h3>
<dl>
<div th:each="attr : ${group.attrs}">
<dt th:text="${attr.attrName}">品牌</dt>
<dd th:text="${attr.attrValue}">华为(HUAWEI)</dd>
</div>
</dl>
</div>
<div class="package-list">
<h3>包装清单</h3>
<p>手机(含内置电池) X 1、5A大电流华为SuperCharge充电器X 1、5A USB数据线 X 1、半入耳式线控耳机 X 1、快速指南X 1、三包凭证 X
1、取卡针 X 1、保护壳 X 1</p>
</div>
</div>
</li>
8.6销售属性渲染
主要步骤:
- 销售属性渲染的时候如果当前
skuId
包含销售属性就添加选中的class
- 页面刷新时给属性选中的class加样式
销售属性渲染的时候如果当前skuId
包含销售属性就添加选中的class
<dl>
<dt>选择[[${attr.attrName}]]</dt>
<dd th:each="val : ${attr.attrValues}">
<a th:attr=" class=${#lists.contains(#strings.listSplit(val.skuIds,','),item.info.skuId.toString())
? 'sku_attr_value checked': 'sku_attr_value'}, skus=${val.skuIds} "
>
[[${val.attrValue}]]
<!-- <img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
</a>
</dd>
</dl>
页面刷新时给属性选中的class加样式
$(function () {
changeCheckedStyle();
});
function changeCheckedStyle() {
$(".sku_attr_value").parent().css({"border": "solid 1px #ccc"});
$("a[class='sku_attr_value checked']").parent().css({"border": "solid 1px red"});
};
8.7sku组合切换
主要步骤:
-
点击属性时清空当前销售属性组的所有选中样式(
checked
),给当前点击元素添加checked
类名 -
获取所有选中的销售属性
skus
,skus
是拥有该销售属性的所有商品skuId
集合 -
根据
skus
求交集,得到skuId
-
跳转到对应的商品详情页面
代码
$(".sku_attr_value").click(function () {
// 1、点击的元素添加上自定义的属性
let skus = new Array();
let curr = $(this).attr("skus").split(",");
$(this).parent().parent().find(".sku_attr_value").removeClass("checked");
$(this).addClass("checked");
changeCheckedStyle();
$("a[class='sku_attr_value checked']").each(function () {
skus.push($(this).attr("skus").split(","));
});
let filterEle = skus[0];
for (let i = 1; i < skus.length; i++) {
filterEle = $(filterEle).filter(skus[i])[0];
}
location.href = "http://item.gulimall.com/" + filterEle + ".html";
return false;
});
8.8异步编排优化
主要步骤:
-
导入自定义配置依赖
configuration-processor
-
gulimall-product
增加线程池配置 -
使用异步编排优化商品详情查询
supplyAsync
:用于异步执行一个返回结果的任务runAsync
:用于异步执行一个不返回结果的任务。thenAcceptAsync
:用于在CompletableFuture
完成后异步执行一个操作,该操作使用CompletableFuture
的结果但不返回新结果。它接受一个Consumer
接口实现,并返回一个新的CompletableFuture
,该对象在操作完成时表示完成状态。
创建配置文件ThreadPoolConfigProperties
,并在application.yaml
配置线程池参数
导入自定义配置依赖configuration-processor
<!--configuration-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
ThreadPoolConfigProperties
@ConfigurationProperties(prefix = "gulimall.thread")
// @Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
application.yaml
gulimall:
thread:
coreSize: 20
maxSize: 200
keepAliveTime: 10
线程池配置
@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
return new ThreadPoolExecutor(
pool.getCoreSize(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
}
使用异步编排优化商品详情查询
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
// 1、sku基本信息的获取 pms_sku_info
SkuInfoEntity info = this.getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
// 3、获取spu的销售属性组合
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
// 4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesc(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
// 5、获取spu的规格参数信息
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
// 2、sku的图片信息 pms_sku_images
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(imagesEntities);
}, executor);
// 等到所有任务都完成
CompletableFuture.allOf(infoFuture, saleAttrFuture, descFuture, baseAttrFuture, imageFuture).get();
return skuItemVo;
}