SpringBoot整合ES-Rest-Client 基础搭建 模块
创建一个独立的module
依赖 1 2 3 4 5 6 7 8 9 10 <properties > <elasticsearch.version > 7.4.2</elasticsearch.version > </properties > <dependency > <groupId > org.elasticsearch.client</groupId > <artifactId > elasticsearch-rest-high-level-client</artifactId > <version > 7.4.2</version > </dependency >
配置
nacos、springserver等
1 2 spring.cloud.nacos.discovery.server-addr =127.0.0.1:8848 spring.application.name =gulimall-search
配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration public class GulimallElasticSearchConfig { public static final RequestOptions COMMON_OPTIONS; static { RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder(); COMMON_OPTIONS = builder.build(); } @Bean public RestHighLevelClient esRestClient () { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("192.168.128.129" , 9200 , "http" ))); return client; } }
TEST & DEBUG
error creating bean with name ‘‘DataSource’
没有配置数据源,如果不需要就在application类上添加@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 package com.atguigu.gulimall.gulimallsearch;import com.alibaba.fastjson.JSON;import com.atguigu.gulimall.gulimallsearch.config.GulimallElasticSearchConfig;import lombok.Data;import lombok.ToString;import org.elasticsearch.action.index.IndexRequest;import org.elasticsearch.action.index.IndexResponse;import org.elasticsearch.action.search.SearchRequest;import org.elasticsearch.action.search.SearchResponse;import org.elasticsearch.client.RestHighLevelClient;import org.elasticsearch.common.xcontent.XContentType;import org.elasticsearch.index.query.QueryBuilders;import org.elasticsearch.search.SearchHit;import org.elasticsearch.search.SearchHits;import org.elasticsearch.search.aggregations.AggregationBuilders;import org.elasticsearch.search.aggregations.Aggregations;import org.elasticsearch.search.aggregations.bucket.terms.Terms;import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;import org.elasticsearch.search.aggregations.metrics.Avg;import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder;import org.elasticsearch.search.builder.SearchSourceBuilder;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.io.IOException;@RunWith(SpringRunner.class) @SpringBootTest public class GulimallSearchApplicationTests { @Autowired private RestHighLevelClient client; @Test public void complexSearch () throws IOException { SearchRequest searchRequest = new SearchRequest(); searchRequest.indices("bank" ); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.matchQuery("address" , "mill" )); TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg" ).field("age" ).size(10 ); searchSourceBuilder.aggregation(ageAgg); AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg" ).field("balance" ); searchSourceBuilder.aggregation(balanceAvg); System.out.println("检索条件: " + searchSourceBuilder.toString()); searchRequest.source(searchSourceBuilder); SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); SearchHits hits = response.getHits(); SearchHit[] hitsHits = hits.getHits(); for (SearchHit hit : hitsHits) { String sourceAsString = hit.getSourceAsString(); Accout accout = JSON.parseObject(sourceAsString, Accout.class); System.out.println("accout:" + accout); } Aggregations aggregations = response.getAggregations(); Terms terms = aggregations.get("ageAgg" ); for (Terms.Bucket bucket : terms.getBuckets()) { String keyAsString = bucket.getKeyAsString(); System.out.println("年龄:" + keyAsString + "==>" + bucket.getDocCount()); } Avg balanceAvg1 = aggregations.get("balanceAvg" ); System.out.println("平均薪资:" + balanceAvg1.getValue()); } @ToString @Data static class Accout { private int account_number; private int balance; private String firstname; private String lastname; private int age; private String gender; private String address; private String employer; private String email; private String city; private String state; } @Test public void indexData () throws IOException { IndexRequest indexRequest = new IndexRequest("users" ); indexRequest.id("1" ); User user = new User("张三" , "男" , 18 ); String jsonString = JSON.toJSONString(user); indexRequest.source(jsonString, XContentType.JSON); IndexResponse response = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); System.out.println(response); } @Data static class User { private String username; private String gender; private Integer age; public User (String username, String gender, Integer age) { this .username = username; this .gender = gender; this .age = age; } } @Test public void contextLoads () { System.out.println(client); } }
商品上架
商品系统—商品维护—SPU管理—上架
上架即将商品数据保存至ES中,只有上架的商品才能被全文检索。
SKU在ES中存储模型
检索会根据:SKU的标题、价格、销量、品牌、分类、商品规格等进行检索
模型设计需要花费一定心思,详见文档09、商城业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 { "mappings" : { "properties" : { "skuId" : { "type" : "long" }, "spuId" : { "type" : "keyword" }, "skuTitle" : { "type" : "text" , "analyzer" : "ik_smart" }, "skuPrice" : { "type" : "keyword" }, "skuImg" : { "type" : "keyword" , "index" : false , "doc_values" : false }, "saleCount" : { "type" : "long" }, "hasStock" : { "type" : "boolean" }, "hotScore" : { "type" : "long" }, "brandId" : { "type" : "long" }, "catalogId" : { "type" : "long" }, "brandName" : { "type" : "keyword" , "index" : false , "doc_values" : false }, "brandImg" : { "type" : "keyword" , "index" : false , "doc_values" : false }, "catalogName" : { "type" : "keyword" , "index" : false , "doc_values" : false }, "attrs" : { "type" : "nested" , "properties" : { "attrId" : { "type" : "long" }, "attrName" : { "type" : "keyword" , "index" : false , "doc_values" : false }, "attrValue" : { "type" : "keyword" } } } } } }
“index”:false “doc_values”:false
表示该冗余的字段不需要用于检索与聚合
检索服务回来表示,这个设置数据类型的步骤很重要,特别是nested这种
这里的模型必须修改,超级感谢一个大佬,文档很详细
检索服务 · 语雀 (yuque.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 # 查看原来的映射规则 GET gulimall_product/_mapping # 修改为新的映射 并创建新的索引,下面进行数据迁移 PUT /mall_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" }, "catalogId" : { "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" : "gulimall_product" }, "dest" : { "index" : "mall_product" } }
gulimall-product SkuEsModel
写在common的to文件夹中,作为传输对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Data public class SkuEsModel { private Long skuId; private Long spuId; private String skuTitle; private BigDecimal skuPrice; private String skuImg; private Long saleCount; private Boolean hasStock; private Long hotScore; private Long brandId; private Long catalogId; private String brandName; private String brandImg; private String catalogName; private List<Attrs> attrs; @Data public static class Attrs { private Long attrId; private String attrName; private String attrValue; } }
SkuHasStockTo 1 2 3 4 5 @Data public class SkuHasStockTo { private Long skuId; private Boolean hasStock; }
SpuInfoController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("product/spuinfo") public class SpuInfoController { @Autowired private SpuInfoService spuInfoService; @PostMapping("/{spuId}/up}") public R spuUp (@PathVariable("spuId") Long spuId) { spuInfoService.spuUp(spuId); return R.ok(); } }
SpuInfoServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 @Override public void spuUp (Long spuId) { SpuInfoEntity spuInfoEntity = this .getById(spuId); BrandEntity brandEntity = brandService.getById(spuInfoEntity.getBrandId()); Long brandId = brandEntity.getBrandId(); String brandLogo = brandEntity.getLogo(); String brandName = brandEntity.getName(); CategoryEntity categoryEntity = categoryService.getById(spuInfoEntity.getCatalogId()); String categoryName = categoryEntity.getName(); List<ProductAttrValueEntity> baseAttrList = productAttrValueService.baseAttrListForSpu(spuId); List<Long> attrIdList = baseAttrList.stream().map(attr -> { return attr.getAttrId(); }).collect(Collectors.toList()); List<Long> searchAttrIdList = attrService.getIdsCanSearch(attrIdList); Set<Long> idSet = new HashSet<>(searchAttrIdList); List<SkuEsModel.Attrs> skuEsModelAttrsList = baseAttrList.stream().filter(attr -> { return idSet.contains(attr.getAttrId()); }).map(attr -> { SkuEsModel.Attrs attrEntity = new SkuEsModel.Attrs(); BeanUtils.copyProperties(attr, attrEntity); return attrEntity; }).collect(Collectors.toList()); List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkuListBySpuId(spuId); Map<Long, Boolean> skuHasStockCollect = new HashMap<>(); try { List<SkuHasStockTo> skuHasStockList = wareFeignService.getSkuHasStockList(skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList())); skuHasStockCollect = skuHasStockList.stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, item -> item.getHasStock())); } catch (Exception e) { log.error("库存服务远程调用异常:原因{}" , e); } Map<Long, Boolean> finalSkuHasStockCollect = skuHasStockCollect; List<SkuEsModel> upSku = skuInfoEntities.stream().map(skuInfo -> { SkuEsModel skuEsModel = new SkuEsModel(); BeanUtils.copyProperties(skuInfo, skuEsModel); skuEsModel.setSkuPrice(skuInfo.getPrice()); skuEsModel.setSkuImg(skuInfo.getSkuDefaultImg()); if (finalSkuHasStockCollect == null ) { skuEsModel.setHasStock(true ); } else { skuEsModel.setHasStock(finalSkuHasStockCollect.get(skuInfo.getSkuId())); } skuEsModel.setHotScore(0L ); skuEsModel.setBrandId(brandId); skuEsModel.setBrandImg(brandLogo); skuEsModel.setBrandName(brandName); skuEsModel.setCatalogName(categoryName); skuEsModel.setAttrs(skuEsModelAttrsList); return skuEsModel; }).collect(Collectors.toList()); R r = searchFeignService.productStatusUp(upSku); if (r.getCode() == 0 ) { SpuInfoEntity spu = new SpuInfoEntity(); spu.setId(spuId); spu.setPublishStatus(ProductConstant.StatusEnum.SPU_UP.getCode()); this .updateById(spu); } else { } } }
WareFeignService 1 2 3 4 5 6 7 8 @FeignClient("gulimall-ware") public interface WareFeignService { @PostMapping("/ware/waresku/hasstock") R getSkuHasStockList (@RequestBody List<Long> skuIdList) ; }
SearchFeignService 1 2 3 4 5 @FeignClient("gulimall-search") public interface SearchFeignService { @PostMapping("/search/save/product") R productStatusUp (@RequestBody List<SkuEsModel> skuEsModelList) ; }
AttrDao 1 2 3 4 5 6 7 8 9 10 <select id ="selectIdsWithSearchAttr" resultType ="java.lang.Long" > SELECT attr_id from gulimall_pms.pms_attr WHERE attr_id IN <foreach collection ="attrIdList" item ="id" separator ="," open ="(" close =")" > #{id} </foreach > AND search_type = 1 </select >
R
给R加上泛型,用于方便远程调用直接传输对应数据(别的方法也可以)
我不想写泛型,泛型也还是要加一个TO,直接传一个TO呗,我也不想知道R的其他信息
1 2 3 4 5 6 7 8 9 10 11 12 public class R <T > extends HashMap <String , Object > { private static final long serialVersionUID = 1L ; private T data; public T getData () { return data; } public void setData (T data) { this .data = data; }
gulimall-ware WareSkuController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("ware/waresku") public class WareSkuController { @Autowired private WareSkuService wareSkuService; @PostMapping("/hasstock") public List<SkuHasStockTo> getSkuHasStockList (@RequestBody List<Long> skuIdList) { List<SkuHasStockTo> hasStock = wareSkuService.getSkuHasStockList(skuIdList); return hasStock; } }
WareSkuServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public List<SkuHasStockTo> getSkuHasStockList (List<Long> skuIdList) { List<SkuHasStockTo> voList = skuIdList.stream().map(skuId -> { SkuHasStockTo skuHasStockVo = new SkuHasStockTo(); Long count = baseMapper.getSkuStock(skuId); skuHasStockVo.setSkuId(skuId); skuHasStockVo.setHasStock(count != null && count > 0 ); return skuHasStockVo; }).collect(Collectors.toList()); return voList; }
WareSkuDao 1 2 3 4 5 <select id ="getSkuStock" resultType ="java.lang.Long" > SELECT SUM(stock - stock_locked) FROM gulimall_wms.wms_ware_sku WHERE sku_id = #{skuId}; </select >
gulimall-search ElasticSearchController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController @RequestMapping("/search") public class ElasticSearchController { @Autowired ElasticSearchService searchService; @PostMapping("/product") public R productStatusUp (@RequestBody List<SkuEsModel> skuEsModelList) { searchService.productUp(skuEsModelList); return R.ok(); } }
ElasticSearchServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Slf4j @Service public class ElasticSearchServiceImpl implements ElasticSearchService { @Autowired RestHighLevelClient client; @Override public Boolean productUp (List<SkuEsModel> skuEsModelList) throws IOException { BulkRequest bulkRequest = new BulkRequest(); for (SkuEsModel skuEsModel : skuEsModelList) { IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX); indexRequest.id(String.valueOf(skuEsModel.getSkuId())); indexRequest.source(JSON.toJSONString(skuEsModel), XContentType.JSON); bulkRequest.add(indexRequest); } BulkResponse bulk = client.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); boolean hasFailures = bulk.hasFailures(); List<String> collect = Arrays.stream(bulk.getItems()).map(item -> { return item.getId(); }).collect(Collectors.toList()); log.error("商品上架信息:{}" , collect); return hasFailures; } }
DEBUG
懒得写,主要是hasFailures返回值得判断,以及远程调用返回值(使用TO就不会,R泛型需要修改)
kibana -> GET product/_search查看是否有数据
跟着做顺畅,但是还是要再看一遍然后自己做
商城首页 整合themeleaf
震惊,竟然要用模板引擎,想念VUE的第一天
依赖 1 2 3 4 5 6 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > <version > 2.1.8.RELEASE</version > </dependency >
配置
关闭缓存等
所有前台的controller统一存放在web文件夹下,而后台在app当中
1 2 3 springboot: thymeleaf: cache: false
静态资源
静态资源放在resources -> static下,模板页面放在templates文件夹下即可
也可以修改resources location配置
热部署
依赖 1 2 3 4 5 6 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <optional > true</optional > </dependency >
配置 渲染一级分类
简直是文艺复兴了
以后thymeleaf的模板修改我就不PO了,麻烦
IndexController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Controller public class IndexController { @Autowired CategoryService categoryService; @GetMapping({"/", "/index.html"}) public String indexPage (Model model) { List<CategoryEntity> categoryEntityList = categoryService.getLevel1List(); model.addAttribute("categoryList" , categoryEntityList); return "index" ; } }
CategoryServiceImpl 1 2 3 4 5 6 7 8 @Override public List<CategoryEntity> getLevel1List () { List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid" , 0 )); return categoryEntityList; }
index.html 1 2 3 4 5 6 7 8 9 10 11 <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <div class ="header_main" > <div class ="header_banner" > <div class ="header_main_left" > <ul > <li th:each ="category : ${categoryList}" > <a href ="/static/#" class ="header_main_left_a" th:attr ="ctg-data=${category.catId}" > <b th:text ="${category.name}" > 家用电器</b > </a > </li > </ul >
渲染子级分类 CategoryFrontVo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @AllArgsConstructor @NoArgsConstructor @Data public class Catalog2Vo { private String catalog1Id; private List<Catalog3Vo> catalog3List; private String id; private String name; @AllArgsConstructor @NoArgsConstructor @Data public static class Catalog3Vo { private String catalog2Id; private String id; private String name; } }
IndexController 1 2 3 4 5 6 7 8 9 10 @ResponseBody @GetMapping("/index/catalog.json") public Map<String, List<Catalog2Vo>> getCatalogJson(Model model) { Map<String, List<Catalog2Vo>> category = categoryService.getCatalogJson(); return category; }
CategoryServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Override public Map<String, List<Catalog2Vo>> getCatalogJson() { List<CategoryEntity> level1List = this .getLevel1List(); Map<String, List<Catalog2Vo>> catalogMap = level1List.stream().collect(Collectors.toMap(k -> { return k.getCatId().toString(); }, parent -> { List<CategoryEntity> children2List = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid" , parent.getCatId())); List<Catalog2Vo> catalog2VoList = null ; if (children2List != null ) { catalog2VoList = children2List.stream().map(child2 -> { Catalog2Vo catalog2Vo = new Catalog2Vo(parent.getCatId().toString(), null , child2.getCatId().toString(), child2.getName()); List<CategoryEntity> children3List = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid" , child2.getCatId())); if (children3List != null ) { List<Catalog2Vo.Catalog3Vo> catalog3VoList = children3List.stream().map(child3 -> { Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(child2.getCatId().toString(), child3.getCatId().toString(), child3.getName()); return catalog3Vo; }).collect(Collectors.toList()); catalog2Vo.setCatalog3List(catalog3VoList); } return catalog2Vo; }).collect(Collectors.toList()); } return catalog2VoList; })); return catalogMap; }
为啥老师这么喜欢嵌套?
优化子级分类 CategoryServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Override public Map<String, List<Catalog2Vo>> getCatalogJson() { List<CategoryEntity> allCatList = baseMapper.selectList(null ); List<CategoryEntity> level1List = getCatListByParentCid(allCatList, 0L ); Map<String, List<Catalog2Vo>> catalogMap = level1List.stream().collect(Collectors.toMap(k -> { return k.getCatId().toString(); }, parent -> { List<CategoryEntity> children2List = getCatListByParentCid(allCatList, parent.getCatId()); List<Catalog2Vo> catalog2VoList = null ; if (children2List != null ) { catalog2VoList = children2List.stream().map(child2 -> { Catalog2Vo catalog2Vo = new Catalog2Vo(parent.getCatId().toString(), null , child2.getCatId().toString(), child2.getName()); List<CategoryEntity> children3List = getCatListByParentCid(allCatList, child2.getCatId()); if (children3List != null ) { List<Catalog2Vo.Catalog3Vo> catalog3VoList = children3List.stream().map(child3 -> { Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(child2.getCatId().toString(), child3.getCatId().toString(), child3.getName()); return catalog3Vo; }).collect(Collectors.toList()); catalog2Vo.setCatalog3List(catalog3VoList); } return catalog2Vo; }).collect(Collectors.toList()); } return catalog2VoList; })); return catalogMap; } private List<CategoryEntity> getCatListByParentCid (List<CategoryEntity> allCatList, Long parentCid) { List<CategoryEntity> collect = allCatList.stream().filter(categoryEntity -> { return categoryEntity.getParentCid().equals(parentCid); }).collect(Collectors.toList()); return collect; }
配置反向代理💡
正向代理:如科学上网,隐藏客户端信息
反向代理:屏蔽内网服务器信息,负载均衡访问
配置host
C:\Windows\System32\drivers\etc\host 文件添加上地址
使用switchHost软件可以快速应用方案
192.168.128.129/192.168.128.129:80(nginx默认地址) -> gulimall.com
配置nginx
虚拟机外部挂载下的conf文件
/mydata/nginx/conf/nginx.conf
/mydata/nginx/conf/conf.d/default.conf
可以先复制有份conf在配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 http { upstream gulimall{ server 192.168.128.1:88; } } server { listen 80; server_name gulimall.com; #charset koi8-r; #access_log /var/log/nginx/log/host.access.log main; location / { # 需要配置header,添加上host,因为nginx转发代理给网关的时候会丢失许多信息 proxy_set_header Host $host; proxy_pass http://gulimall; }
TODO 转不了为什么?nginx可以访问到,但是nginx转发不到本机,我感觉还是proxy_pass配置的问题,但是我每个ip都试过了?
我不理解:192.168.128.1:10000 是可以的,http://192.168.128.129:5601也是可以的,虚拟机ping 192.168.128.1也是可以的?
目测是本机 -> 虚拟机nginx是成功的,但是nginx -> 本机192.168.128.1:10000失败了
重点说明!!!以上问题已解决,是本机防火墙的原因,操作如下Windows 通过命令行关闭防火墙-百度经验 (baidu.com)
感谢:谷粒商城高级篇p139、p140Nginx反向代理,网关配置不生效解决_含雷的博客-CSDN博客 (被ping通欺骗了,所以一开始排除了防火墙)
关掉本机的防火墙,虚拟机的nginx就能转发到本机了!!!
配置网关 1 2 3 4 5 - id: gulimall_host_route uri: lb://gulimall-product predicates: - Host=**.gulimall.com,gulimall.com
总结一下实现:虚拟机192.168.128.129 通过修改win的host文件,相当于gulimall.com,192.168.128.1:10000 是本机商品服务的地址,现在需要通过请求nginx,然后nginx代理网关,网关再转发给对应服务地址,所以在虚拟机的nginx中配置,请求192.168.128.129:80(nginx端口80),本来是请求虚拟机的nginx,然后nginx配置80端口代理地址192.168.128.1:88(即本机的网关地址),然后网关配置**.gulimall.com转发到gulimall-product服务实现。
本机请求 -> 虚拟机nginx -> 本机网关 -> 本地服务地址
性能压测 压力测试
具体介绍见课件04
Jmeter
正好测试课要写测试工具报告
详细见测试工具报告
性能监控 堆内存与垃圾回收
详见P144
jvisualvm
命令行启动:jvisualvm
安装插件 visual gc(建议手动下载然后添加插件即可)
简单优化吞吐量
开启thymeleaf缓存,关闭日志打印,数据库添加索引
nginx动静分离
将静态资源存放在nginx里,所以静态请求直接从nginx里拿资源即可,不需要过网关到服务器Tomcat里拿
规定:/static/***下的所有请求由nginx直接返回
存放静态资源
将product服务的resource下的所有静态文件(static下)存放到nginx下的html文件夹下的static下(记录一下虚拟机移动文件 mv xxx xxx)
将html中的静态资源路径添加上/static/前缀
nginx配置
当访问/static下的资源时,要求nginx为我们请求nginx下的html文件夹下的static文件夹
1 2 3 location /static { root /usr/share/nginx/html; }
如果请求出现403的问题需要修改nginx文件夹的权限
chmod -R 777 /mydata/nginx/html
缓存
1优化业务逻辑 —> 2使用缓存
适用于访问量达、更新频率不高、及时性数据一致性要求不要的数据,极大提升系统性能
在开发中, 凡是放入缓存中的数据我们都应该指定过期时间, 使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。 避免业务崩溃导致的数据永久不一致问题。
最直接的缓存:一个Map对象,是本地缓存 在分布式下,一个服务存在多个服务器,就会产生多个缓存,可能数据不一致
缓存中间件:redis
Redis 依赖 1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
使用spring-boot-starter的redis,版本由父项目统一管理
配置 1 2 3 4 5 spring: redis: host: 192.168 .128 .129 port: 6379
测试 1 2 3 4 5 6 7 8 9 @Test public void redis () { ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue(); valueOperations.set("hello" , "hello_" + UUID.randomUUID()); String hello = valueOperations.get("hello" ); System.out.println("之前保存的数据是: " + hello); }
可视化工具
RDM、ARDM(后者好看)
优化分类业务 CategoryServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public Map<String, List<Catalog2Vo>> getCatalogJson() { String catalogJSON = redisTemplate.opsForValue().get("catalogJSON" ); if (StringUtils.isEmpty(catalogJSON)) { Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb(); redisTemplate.opsForValue().set("catalogJSON" , JSON.toJSONString(catalogJsonFromDb)); return catalogJsonFromDb; } Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() { }); return result; }
解决OutOfDirectMemoryError
TODO 产生堆外内存溢出:OutOfDirectMemoryError
1)、springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
2)、lettuce的bug导致netty堆外内存溢出 -Xmx300m;netty如果没有指定堆外内存,默认使用-Xmx300m
可以通过-Dio.netty.maxDirectMemory进行设置
解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存。
1)、升级lettuce客户端。 2)、切换使用jedis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > <exclusions > <exclusion > <groupId > io.lettuce</groupId > <artifactId > lettuce-core</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > io.lettuce</groupId > <artifactId > lettuce-core</artifactId > <version > 5.2.0.RELEASE</version > </dependency >
缓存失效
缓存穿透
缓存穿透是指查询一个一定不存在的数据, 由于缓存是不命中, 将去查询数据库, 但是数据库也无此记录, 我们没有将这次查询的 null 写入缓存, 这将导致这个不存在的数据每次请求都要到存储层去查询, 失去了缓存的意义。
在流量大时, 可能 DB 就挂掉了, 要是有人利用不存在的 key 频繁攻击我们的应用, 这就是漏洞。
解决:缓存空结果、 并且设置短的过期时间。
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间, 导致缓存在某一时刻同时失 效, 请求全部转发到 DB, DB 瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值, 比如 1-5 分钟随机, 这样每一个缓存的过期时间的重复率就会降低, 就很难引发集体失效的事件。
缓存击穿
对于一些设置了过期时间的 key, 如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据
这个时候, 需要考虑一个问题: 如果这个 key 在大量请求同时进来前正好失效, 那么所 有对这个 key 的数据查询都落到 db, 我们称为缓存击穿。
解决:加锁
Spring Cache
依赖 1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency >
配置
CacheAutoConfiguration 自动导入 RedisAutoConfiguration,内含 RedisCacheManager 缓存管理器
1 2 3 4 5 6 7 8 spring: cache: type: redis redis: host: 192.168 .128 .129 port: 6379
注解
@Cachable:将数据保存到缓存
@CacheEvict:将数据从缓存删除
@CachePut:保存(不影响方法执行)
@Caching:组合多个缓存操作
@CacheConfig:在类级别共享相同配置
@EnableCaching:在启动类上,开启缓存
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Caching(evict = { @CacheEvict(cacheNames = "category", key = "'level1Category1'"), // 删除缓存 @CacheEvict(cacheNames = "category", key = "'level1Category2'") }) @Override @Transactional public void updateDetail (CategoryEntity category) { this .updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); } @Cacheable({"category"}) @Override public List<CategoryEntity> getLevel1List () { List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid" , 0 )); return categoryEntityList; }
自定义配置
默认
key默认自动生成,名字为 分区名::SimpleKey []
缓存的value值默认使用jdk序列化机制,将序列化后的数据保存到redis
默认ttl时间 -1s
自定义
指定key:接收SpEL表达式(官网文档有解释各种写法)
1 @Cacheable(cacheNames = {"category"}, key = "'level1Category'")
指定存活时间:单位为毫秒
1 2 3 4 cache: type: redis redis: time-to-live: 1000
指定JSON序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Configuration @EnableCaching @EnableConfigurationProperties(CacheProperties.class) public class MyCacheConfig { @Bean RedisCacheConfiguration redisCacheConfiguration (CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return config; } }
分布式锁
RedisTemplates实现 分布式锁演进
原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock" ,uuid,300 ,TimeUnit.SECONDS); if (lock){System.out.println("获取分布式锁成功..." ); Map<String, List<Catelog2Vo>> dataFromDb; try {dataFromDb = getDataFromDb(); }finally { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" ;Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class) , Arrays.asList("lock" ), uuid); } 获取值对比+对比成功删除=原子操作 lua 脚本解锁 return dataFromDb;}else { System.out.println("获取分布式锁失败...等待重试" ); try {Thread.sleep(200 ); }catch (Exception e){ } r eturn getCatalogJsonFromDbWithRedisLock () ;} }
Redisson
Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid) 。 充分的利用了 Redis 键值数据库提供的一系列优势, 基于 Java 实用工具包中常用接口, 为使用者提供了一系列具有分布式特性的常用工具类。 使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力, 大大降低了设计和研发大规模分布式系统的难度。 同时结合各富特色的分布式服务, 更进一步简化了分布式环境中程序相互之间的协作。
官方文档: https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
依赖 1 2 3 4 5 6 <dependency > <groupId > org.redisson</groupId > <artifactId > redisson</artifactId > <version > 3.12.0</version > </dependency >
程序化配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class MyRedissonConfig { @Bean(destroyMethod = "shutdown") public RedissonClient redisson () throws IOException { Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.128.129:6379" ); return Redisson.create(config); } }
前台-检索服务 搭建页面环境
需要向虚拟机传输文件,所有还是用xshel好使,教程在p122
静态资源
css,img等资源放在nginx的html文件夹下
index.html放在项目resource下的templates文件夹下(注意这里index名字改为list,因为首页搜索发请求地址就是search.gulimall.com/list.html
host配置
使用SwitchHost
192.168.128.129 gulimall.com 192.168.128.129 search.gulimall.com
nginx配置
添加server_name对应host即可
server_name gulimall.com *.gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /static {
root /usr/share/nginx/html;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
gateway配置 1 2 3 4 5 6 7 8 9 - id: gulimall_host_route uri: lb://gulimall-product predicates: - Host=gulimall.com - id: gulimall_search_route uri: lb://gulimall-search predicates: - Host=search.gulimall.com
根据之前nginx配置中的proxy_set_header Host $host,网关能根据host来选择不同的服务
list.html里面有很多路径错误,记得修改一下
修改一下发起请求的请求路径
检索模型分析 查询参数模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data public class SearchParamVo { private String keyword; private Long catalog3Id; private String sort; private Integer hasStock; private String skuPrice; private List<Long> brandId; private List<String> attrs; private Integer pageNum; }
返回结果模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Data public class SearchResultVo { private List<SkuEsModel> products; private Integer pageNum; private Long total; private Integer totalPages; private List<BrandVo> brands; private List<CatalogVo> catalogs; private List<AttrVo> attrs; @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; } }
检索DSL测试
TODO P177、P178
十分劝退,头疼
SearchRequest 查询、排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @Service public class MallSearchServiceImpl implements MallSearchService { @Autowired private RestHighLevelClient client; @Override public SearchResultVo search (SearchParamVo searchParamVo) { SearchRequest searchRequest = buildSearchRequest(searchParamVo); try { SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); } catch (IOException e) { e.printStackTrace(); } SearchResult searchResult = buildSearchResult(); return null ; } private SearchResult buildSearchResult () { return null ; } private SearchRequest buildSearchRequest (SearchParamVo param) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (!StringUtils.isEmpty(param.getKeyword())) { MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("skuTitle" , param.getKeyword()); boolQuery.must(matchQuery); } if (param.getCatalog3Id() != null ) { TermQueryBuilder termQuery = QueryBuilders.termQuery("catalogId" , param.getCatalog3Id()); boolQuery.filter(termQuery); } if (param.getBrandId() != null ) { TermsQueryBuilder termsQuery = QueryBuilders.termsQuery("brandId" , param.getBrandId()); boolQuery.filter(termsQuery); } if (param.getAttrs() != null && param.getAttrs().size() > 0 ) { for (String attrStr : param.getAttrs()) { BoolQueryBuilder nestedBool = QueryBuilders.boolQuery(); String[] strings = attrStr.split("_" ); String attrId = strings[0 ]; String[] attrValues = strings[1 ].split(":" ); nestedBool.must(QueryBuilders.termQuery("attrs.attrId" , attrId)); nestedBool.must(QueryBuilders.termsQuery("attrs.attrValue" , attrValues)); boolQuery.filter(QueryBuilders.nestedQuery("attrs" , nestedBool, ScoreMode.None)); } } if (param.getHasStock() != null ) { TermQueryBuilder termQuery = QueryBuilders.termQuery("hasStock" , param.getHasStock() == 1 ); boolQuery.filter(termQuery); } else { TermQueryBuilder termQuery = QueryBuilders.termQuery("hasStock" , true ); boolQuery.filter(termQuery); } if (!StringUtils.isEmpty(param.getSkuPrice())) { RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice" ); String[] priceRange = param.getSkuPrice().split("_" ); if (param.getSkuPrice().startsWith("_" )) { rangeQuery.lte(priceRange[1 ]); } else if (param.getSkuPrice().endsWith("_" )) { rangeQuery.gte(priceRange[0 ]); } else { rangeQuery.gte(priceRange[0 ]).lte(priceRange[1 ]); } boolQuery.filter(rangeQuery); } searchSourceBuilder.query(boolQuery); if (!StringUtils.isEmpty(param.getSort())) { String sort = param.getSort(); String[] sortValue = sort.split("_" ); searchSourceBuilder.sort(sortValue[0 ], SortOrder.fromString(sortValue[1 ])); } if (param.getPageNum() != null ) { searchSourceBuilder.from((param.getPageNum() - 1 ) * EsConstant.PAGE_SIZE); } else { searchSourceBuilder.from(0 ); } searchSourceBuilder.size(EsConstant.PAGE_SIZE); if (!StringUtils.isEmpty(param.getKeyword())) { searchSourceBuilder.highlighter(new HighlightBuilder().field("skuTitle" ).preTags("<b style='color:red'>" ).postTags("</b>" )); } String s = searchSourceBuilder.toString(); System.out.println("构建的DSL:" + s); SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder); return searchRequest; } }
聚合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg" ).field("brandId" ).size(EsConstant.BRAND_SIZE); brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg" ).field("brandImg" ).size(1 )); brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg" ).field("brandName" ).size(1 )); searchSourceBuilder.aggregation(brand_agg); TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg" ).field("catalogId" ).size(EsConstant.CATALOG_SIZE); catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg" ).field("catalogName" ).size(1 )); searchSourceBuilder.aggregation(catalog_agg); NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg" , "attrs" ); TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg" ).field("attrs.attrId" ); attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg" ).field("attrs.attrName" ).size(1 )); attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg" ).field("attrs.attrValue" ).size(50 )); attr_agg.subAggregation(attr_id_agg); searchSourceBuilder.aggregation(attr_agg);
注意:如果使用文档的es模型映射规则,会有问题,详见本文档的商品上架的模型分析
SearchResult 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 private SearchResultVo buildSearchResult (SearchResponse response, SearchParamVo param) { SearchResultVo resultVo = new SearchResultVo(); SearchHits hits = response.getHits(); List<SkuEsModel> skuList = new ArrayList<>(); if (hits.getHits() != null && hits.getHits().length > 0 ) { for (SearchHit hit : hits.getHits()) { SkuEsModel sku = JSON.parseObject(hit.getSourceAsString(), SkuEsModel.class); if (!StringUtils.isEmpty(param.getKeyword())) { HighlightField highlightField = hit.getHighlightFields().get("skuTitle" ); String highlight = highlightField.getFragments()[0 ].toString(); sku.setSkuTitle(highlight); } skuList.add(sku); } } resultVo.setProducts(skuList); List<SearchResultVo.AttrVo> attrVoList = new ArrayList<>(); ParsedNested attr_agg = response.getAggregations().get("attr_agg" ); ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg" ); for (Terms.Bucket bucket : attr_id_agg.getBuckets()) { SearchResultVo.AttrVo attrVo = new SearchResultVo.AttrVo(); attrVo.setAttrId(Long.valueOf(String.valueOf(bucket.getKeyAsNumber()))); ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg" ); attrVo.setAttrName(attr_name_agg.getBuckets().get(0 ).getKeyAsString()); List<String> attrValue = new ArrayList<>(); ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg" ); for (Terms.Bucket aggBucket : attr_value_agg.getBuckets()) { attrValue.add(aggBucket.getKeyAsString()); } attrVo.setAttrValue(attrValue); attrVoList.add(attrVo); } resultVo.setAttrs(attrVoList); List<SearchResultVo.BrandVo> brandVoList = new ArrayList<>(); ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg" ); for (Terms.Bucket bucket : brand_agg.getBuckets()) { SearchResultVo.BrandVo brandVo = new SearchResultVo.BrandVo(); brandVo.setBrandId(Long.valueOf(bucket.getKeyAsString())); ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg" ); brandVo.setBrandName(brand_name_agg.getBuckets().get(0 ).getKeyAsString()); ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg" ); brandVo.setBrandImg(brand_img_agg.getBuckets().get(0 ).getKeyAsString()); brandVoList.add(brandVo); } resultVo.setBrands(brandVoList); List<SearchResultVo.CatalogVo> catalogVoList = new ArrayList<>(); ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg" ); for (Terms.Bucket bucket : catalog_agg.getBuckets()) { SearchResultVo.CatalogVo catalogVo = new SearchResultVo.CatalogVo(); catalogVo.setCatalogId(Long.valueOf(bucket.getKeyAsString())); ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg" ); catalogVo.setCatalogName(catalog_name_agg.getBuckets().get(0 ).getKeyAsString()); catalogVoList.add(catalogVo); } resultVo.setCatalogs(catalogVoList); long total = hits.getTotalHits().value; resultVo.setTotal(total); resultVo.setTotalPages((int ) ((total + EsConstant.PRODUCT_PAGE_SIZE - 1 ) / EsConstant.PRODUCT_PAGE_SIZE)); resultVo.setPageNum(param.getPageNum()); return resultVo; }
wc,typora有毒,两天没了三次内容,而且未保存内容草稿也没用,东西都没了,我吐了,可能是内容太多了,新开一个