①第一种是方便检索,但是占用大量内存
②第二种把索引分开,原理就是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" }
}
}
}
}
}
指的是nested这个属性值。为什么要有这个值?如果没有就有可能会造成数据的扁平化。那些相同属性的值会变成一个数组。导致最后查询的时候出现问题。
{
"user":[
{
"first":xxx,
"last":yyy
},
{
"first":xxx,
"last":yyy
}
]
}
//扁平化处理之后就会变成
{
user.first=["xxx","xxx"],
user.last=["yyy","yyy"]
}
思路
①根据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());
}
思路
①现根据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());
思路
①其实就是根据现在查询到的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;
}
思路
①先去到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;
}
}
原因
①如果发现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;
}
思路
①引入thymeleaf的驱动,关闭缓存。
②创建web包和修改controller->app包。
③html静态资源放到static中,首页页面放到template中
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
thymeleaf:
cache: false
思路
①直接根据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 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>
思路
①获取一级目录
②根据一级目录映射成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;
}
概述
意思就是nginx有两种方式。第一种是正向代理,其实就是隐藏客户端的信息,客户端通过nginx来获取服务器的信息。nginx相当于就是做了中介。反向代理其实就是负责为服务器接收请求,然后重新通过网关交给服务器,而不是让客户端直接访问服务器。增加了服务器的安全性。
配置思路
①先通过hosts文件配置对应的域名与虚拟机ip,访问域名能够直接访问虚拟机。
②然后就是修改nginx中http模块的server模块,主要是监听gulimall.com,然后就是反向代理我们的10000产品服务器。
③测试
nginx的原理
我们访问域名,相当于在访问虚拟机,这个时候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。然后交给网关进行负载均衡的处理。
思路
①创建/static/search文件在nginx,并且把静态页面放进去。
②修改nginx配置,server:*.gulimall.com
③修改网关配置,主要就是对应的sear.gulimall.com分配到search服务
④转移页面到search。并且修改路径。
拓展与坑
①json数据是catalog而不是catelog,改错可能会显示不出list
思路
①修改页面的路径跳转到首页
②修改首页的分类点击,写一个Controller跳转到检索页面list.html
③修改搜索按钮,通过javascrpt:方法来处理
<a href="javascript:search();" ><img src="/static/index/img/img_09.png" />a>
思路
①分类栏
②下面的几个属性栏
③还有就是产品集合,和分页数据
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;
}
}
分类和品牌查询,通过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": ">"
}
}
为什么需要聚合?
①需要有各种分类信息,品牌信息。还有属性以及属性值。这些都是需要通过聚合才能单独获取
②比如分类,有多少种?品牌有多少个?属性有多少个?属性里面有多少个属性值?这些都是页面上需要聚合显示的条件查询信息。
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
}
}
}
}
}
}
}
}
思路
①根据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;
}
思路
①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("");
highlightBuilder.postTags("");
builder.highlighter(highlightBuilder);
}
builder.query(boolQuery);
System.out.println(builder.toString());
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},builder);
return null;
}
思路
①必须来了解和清晰整个结果的结构。
②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;
}
思路
①给分类、品牌和属性都添上点击方法,如果点击之后能够加上参数然后重新更新网页。
②遍历分类品牌和属性,思路基本上就是从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("brandId",'+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("catalogId",'+catalog.catalogId+')'}"
th:text="${catalog.catalogName}">5.56英寸及以上a>li>
ul>
<a href="/static/search/#"
th:href="${'javascript:searchProducts("attr",'+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;
}
}
思路
①解决搜索栏的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"
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;
};
思路
①需求:点击排序之后可以有上下箭头提醒,并且能够高亮
(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);
}
}
思路
①需求与问题:点击排序之后会跳转页面,并且会清除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}">
<a href="/static/search/#">
<input th:checked="${#strings.equals(check,'1')}" id="hasStock" type="checkbox">
货到付款
a>
li>
思路
①创建一个NavVo面包屑,attrName对上attrValue还有一个link
②遍历参数attrs,并且远程查询attr的name封装到面包屑对象中。
③取出attr,并且encode成utf-8。替换变成+的空格为%20字符
④接着就是取出所有参数string,然后每次遍历到的attr就换掉这个attr的参数为空。并且生成link放到navVo中。
//9.封装面包屑
if(param.getAttrs()!=null&¶m.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);
}
思路
①先去封装获取的面包屑,远程调用获取品牌信息,封装进面包屑
②替换掉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;
①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 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("好?");
}
}
}
变量
corePoolSize:核心线程,主要执行的线程个数
maximumPoolSize:最多线程个数,主要是核心线程满了,队列满的时候会开启线程
unit:时间单位
Queue:阻塞队列类型
ThreadFactory:线程创建工厂
handler:拒绝策略
keepAliveTime:线程可以存活的时间
1.任务来了,先去到核心线程完成
2.核心线程满了放到阻塞队列
3.阻塞队列满了,开启新的线程直到max
4.max也满了,handler开始执行拒绝任务策略
5.max-core的线程如果执行任务完成之后,就会通过keepAliveTime来释放内存
6.Cache只有最大值,没有核心线程,fixed只有固定的核心线程,Schedule定时任务线程。Single单线程任务。
解决问题:不同任务的同时执行和异步执行。
测试两种不同的实现方式(返回的都是Future对象。如果是Supplier接口的话就能够通过future来返回数据)
public static void main(String[] args) throws ExecutionException, InterruptedException {
// CompletableFuture 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());
思路
①其实就是在后面链接上线程池和要执行的任务
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());
思路
①nginx静态页面配置和转发路径配置
②gateway配置
③转移详情页面到product服务
④写ItemController跳转到对应的页面
⑤修改图片的a地址,主要就是跳转到详情页面。th:href="|xxxx.com/${xx}.html|"
思路
①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;
}
}
思路
①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;
}
思路
①通过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
思路
①标题渲染
②price
③属性渲染,这个地方需要用到#strings.listSplit来分割字符串获取list
④图片
⑤商品介绍,遍历img
⑥规格属性。也是要用到分割。
拓展
①如果字符串需要分割可以使用#strings.listSplit来解决
②如果是需要限制数字的小数和整数位需要用到#numbers.formatDecimal(xx,2,3)
思路
①销售属性的值可以通过自己包含的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"})
})
思路
①先把对应的线程池注入进来。如果需要修改属性可以配置一个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;
}