【项目学习】谷粒商城学习记录3 - 高级篇
【项目学习】谷粒商城学习记录3 - 高级篇
一、ElasticSearch
1、基本概念
- 官方文档
- ES可以实现快速存储、搜索和分析海量数据
2、准备工作
(1) ES安装(docker)
- 看我的另一个博客
(2) ES学习
- 我的感觉是和学习mysql一样,使用并不难,难的是做一些高进阶的东西。现在学的就是查看信息,查询,保存,修改,删除,批量操作这类,都是固定指令请求,熟悉就行。检索很重要,query DSL
- 推荐博客
- 用
postman
去学习很方便
(3) ik分词安装 & nginx安装
(4) springboot整合high-level-client
- 创建新模块
gulimall-search
- 添加common依赖,配置注册中心
- 导入相关依赖
<!--导入es的rest high level client--> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.4.2</version> </dependency>
- 编写配置,给容器注入一个
RestHighLevelClient
(以下实现在GulimallElasticSearchConfig配置类中)@Bean public RestHighLevelClient esRestClient() { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder(new HttpHost("101.201.39.44", 20004, "http")) ); return client; }
- 最后参照api使用就行
(5) 增删改查测试
a. 新增
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
indexRequest.source("userName", "zhangsan", "age", 18, "gender", "男");
User user = new User();
user.setUserName("zhangsan");
user.setAge(18);
user.setGender("男");
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON);
//执行操作
IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//提取有用的响应数据
System.out.println(index);
}
b.查询
- 所有es的
_search
中的命令,比如match, from, size这些都有接口方法 - 下面例子也使用了聚合
@Test
public void searchData() throws IOException {
//1、创建检索请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//1.1)、构造检索条件
sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
System.out.println(sourceBuilder.toString());
searchRequest.source(sourceBuilder);
//2、执行检索
SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//3、分析结果 searchResponse
System.out.println(searchResponse.toString());
}
3、商城业务-商品上架
(1) sku在es中存储模型分析
-
我们想要将sku信息保存在es中,首先要考虑如何建索引。有以下两种考虑方式
- 索引库设计方案1(推荐,空间换时间):将所有sku可能会用到的信息都放在一个索引内
优点:空间换时间,减少请求次数
缺点:面对一些场景下,如每个sku都有规格参数,会存在大量冗余存储。当每个spu下面sku规格都相同时,会存储多次。{ skuId:1 spuId:11 skyTitile:华为xx price:999 saleCount:99 attr:[ {尺寸:5}, {CPU:高通945}, {分辨率:全高清} ] }
- 索引库设计方案2(不推荐,请求太多,传输数据量大) :和mysql分表一样,将冗余数据分开单独建立索引
优点:减少空间使用
缺点:由于规格标签都是动态变化的,每次每个用户都会发送请求去查询各种不同数据,导致请求访问量太大,可能引起网络阻塞
- 索引库设计方案1(推荐,空间换时间):将所有sku可能会用到的信息都放在一个索引内
-
最终使用方案一,如下是所使用的建立索引请求:
说明:{"type": "keyword"}, 保持数据精度问题,可以检索,但不能分词
{"analyzer", "ik_smart"}, 使用ik中文分词器
{"index", false}, 不可被检索,不生成index
{"doc_values", false}, 不可被聚合,es就不会维护一些聚合信息
为了防止数组扁平化,商品属性字段设为nested类型PUT product { "mappings":{ "properties": { "skuId":{ "type": "long" }, #商品sku "spuId":{ "type": "keyword" }, #当前sku所属的spu。 "skuTitle": { "type": "text", "analyzer": "ik_smart" #只有sku的标题需要被分词 }, "skuPrice": { "type": "keyword" }, "skuImg" : { "type": "keyword" }, "saleCount":{ "type":"long" }, "hasStock": { "type": "boolean" }, #是否有库存。在库存模块添加此商品库存后,此字段更为true "hotScore": { "type": "long" }, "brandId": { "type": "long" }, "catalogId": { "type": "long" }, "brandName": {"type": "keyword"}, "brandImg":{ "type": "keyword", "index": false, #不可被检索 "doc_values": false #不可被聚合。doc_values默认为true }, "catalogName": {"type": "keyword" }, "attrs": { "type": "nested", #对象数组防止扁平化,不能用object类型 "properties": { "attrId": {"type": "long" }, "attrName": { "type": "keyword", "index": false, #在后面“商城业务-检索服务”开发时这里要去掉 "doc_values": false #在后面“商城业务-检索服务”开发时这里要去掉 }, "attrValue": {"type": "keyword" } } } } } }
(2) 实现商品上架服务
实现接口:/product/spuinfo/{spuId}/up
- 整体业务流程:
- 1、查询当前spu下的sku列表;
- 2、将这个sku列表封装成sku_es模型类列表;
- 1.给每个sku加上属性规格列表
- 2.查询每个sku是否有库存,要远程调用库存模块
- 3.给每个sku加上热度、所属品牌、所属分类名、所有可被检索的规格等属性
- 3、将收集的sku_es模型类列表发给es保存,要远程调用查询模块。
- SpuInfoController
@RequestMapping("/{spuId}/up") public R spuUp(@PathVariable("spuId") Long spuId) { spuInfoService.up(spuId); return R.ok(); }
- 在common模块中的to包下新建
es
包,保存用于es的相关数据模型,并创建SkuEsModel@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 brandId; private Long catalogId; private String brandName; private String brandImg; private String catalogName; private List<Attr> attrs; @Data public static class Attr { private Long attrId; private String attrName; private String attrValue; } }
- 接着在SpuInfoServiceImpl的
up
方法中封装数据,首先查出当前spuid对应的所有sku信息,品牌的名字,并通过遍历skus将数据封装成SkuEsModel类型。通过对比SkuInfoEntity和SkuEsModel两个类,会发现有的属性两个同名,这些直接通过BeanUtils.copyProperties赋值。还有一些信息虽然没有同名,但可以通过单独set赋值。最后处理其他需要处理的步骤,这里产生了TODO1 ~ TODO4。 最后TODO5实现上传。List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId); // TODO 4、查询当前sku的所有可以被用来检索的规格属性 //遍历封装sku信息 List<Object> upProducts = skus.stream().map(sku -> { SkuEsModel esModel = new SkuEsModel(); BeanUtils.copyProperties(sku, esModel); esModel.setSkuPrice(sku.getPrice()); esModel.setSkuImg(sku.getSkuDefaultImg()); // TODO 1、发送远程调用,库存系统查询是否有库存 // TODO 2、热度评分。0 // TODO 3、查询品牌和分类的名字信息 return esModel; }).collect(Collectors.toList()); // TODO 5、将数据发送给es进行保存; gulimall-search;
- 接着先实现最容易的TODO3:
// TODO 3、查询品牌和分类的名字信息 BrandEntity brand = brandService.getById(esModel.getBreandId()); esModel.setBrandName(brand.getName()); esModel.setBrandImg(brand.getLogo()); CategoryEntity category = categoryService.getById(esModel.getCatalogId()); esModel.setCatalogName(category.getName());
- 接着实现TODO4, 首先由于我们要查询当前sku的所有可以被用来检索的规格属性,所以在ProductAttrValueService中实现一个根据spuId获得所有该spu商品属性的方法
baseAttrlistForSpu
, 它将返回所有商品属性类型的数据list。我们还要遍历封装成ids//ProductAttrValueServiceImpl中实现 @Override public List<ProductAttrValueEntity> baseAttrlistForSpu(Long spuId) { List<ProductAttrValueEntity> entities = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId)); return entities; }
//up()方法内实现TODO4 // TODO 4、查询当前sku的所有可以被用来检索的规格属性 List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrlistForSpu(spuId); List<Object> attrIds = baseAttrs.stream().map(attr -> { return attr.getAttrId(); }).collect(Collectors.toList());
- 由于上面ids包含了许多不用于search的冗余属性id, 所以我们还要在AttrService中实现一个方法
selectSearchAttrIds
,来实现对传入ids中留下search_type为1的id。这个方法将在mapping中实现特定的sql语句。//AttrService中 /** * 在指定的所有属性集合中,调出检索属性 * @param attrIds * @return */ List<Long> selectSearchAttrIds(List<Long> attrIds);
//AttrServiceImpl中 @Override public List<Long> selectSearchAttrIds(List<Long> attrIds) { // select attr_id from `pms_attr` where attr_id in(?) and search_type = 1 return baseMapper.selectSearchAttrIds(attrIds); }
//AttrDao List<Long> selectSearchAttrIds(@Param("attrIds") List<Long> attrIds);
//AttrDao.xml <select id="selectSearchAttrIds" resultType="java.lang.Long"> select attr_id from `pms_attr` where attr_id in <foreach collection="attrIds" item="id" separator="," open="(" close=")"> #{id} </foreach> and search_type = 1 </select>
- 注意过滤后,还要转成内部类SkuEsModel.Attr。完整的TODO4实现:
// TODO 4、查询当前sku的所有可以被用来检索的规格属性 List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrlistForSpu(spuId); List<Long> attrIds = baseAttrs.stream().map(attr -> { return attr.getAttrId(); }).collect(Collectors.toList()); //查出attrIds中用于search的属性id List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds); Set<Long> idSet = new HashSet<>(searchAttrIds); //留下所有满足条件的商品属性list List<SkuEsModel.Attr> attrsList = baseAttrs.stream().filter(item -> { return idSet.contains(item.getAttrId()); }).map(item -> { SkuEsModel.Attr attrs1 = new SkuEsModel.Attr(); BeanUtils.copyProperties(item, attrs1); return attrs1; }).collect(Collectors.toList());
- TODO2的热度可以先设置为0
// TODO 2、热度评分。0 esModel.setHotScore(0L);
- 接着实现远程查询库存的功能
- 创建vo,接收方法返回数据 SkugHasStockVo
@Data public class SkugHasStockVo { private Long skuId; private Boolean hasStock; }
- 在库存模块的WareSkuController中实现
@PostMapping("/hasstock") public R getSkusHasStock(@RequestBody List<Long> skuIds) { List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds); return R.ok().put("data", vos); }
- 在WareSkuServiceImpl 中具体实现:
@Override public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) { List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> { SkuHasStockVo vo = new SkuHasStockVo(); //查询当前sku的总库存量 long count = baseMapper.getSkuStock(skuId); vo.setSkuId(skuId); vo.setHasStock(count > 0); return vo; }).collect(Collectors.toList()); return collect; }
- 其中,getSkuStock()方法是是现在baseMapper的,也就是自己实现的特定sql语句,用于查询库存总量-已锁定量
<select id="getSkuStock" resultType="java.lang.Long"> select sum(stock - stock_locked) from `wms_ware_sku` where sku_id = #{skuId} </select>
- 创建vo,接收方法返回数据 SkugHasStockVo
- 上面在库存模块中实现了方法,但是要想在商品模块使用,得通过远程调用。在商品模块下的Feign包下新建
WareFeignService
接口。@FeignClient("gulimall-waer") public interface WareFeignService { @PostMapping("/ware/waresku/hasstock") public R getSkusHasStock(@RequestBody List<Long> skuIds); }
- up()中调用远程接口查询库存量信息,注意这里修改了公共类中的R,添加了getData()方法。
下面是R的修改// TODO 1、发送远程调用,库存系统查询是否有库存 Map<Long, Boolean> stockMap = null; try { R skuHasStock = wareFeignService.getSkusHasStock(skuIdList); TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {}; stockMap = skuHasStock.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item-> item.getHasStock())); } catch (Exception e) { log.error("库存服务查询异常: 原因{}", e); }
public <T> T getData(String key, TypeReference<T> typeReference) { Object data = get(key);// 默认是map类型,springmvc做的 String jsonStr = JSON.toJSONString(data); T t = JSON.parseObject(jsonStr, (Type) typeReference); return t; } // 利用fastJson进行逆转 // 这里要声明泛型<T>,这个泛型只跟方法有关,跟类无关。 // 例如类上有个泛型,这里可以使用类上的泛型,就不用声明 public <T> T getData(TypeReference<T> typeReference) { Object data = get("data");// 默认是map类型,springmvc做的 String jsonStr = JSON.toJSONString(data); T t = JSON.parseObject(jsonStr, typeReference); return t; } public R setData(Object data) { put("data", data); return this; }
- 最后实现远程上架接口,完成商品上架
- 使用postman 工具,创建请求:
http://es服务ip地址:端口/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" } } } } } }
- 执行结果:
- 在common.exception.BizCodeEnume中添加错误码:
PRODUCT_UP_EXCEPTION(11000, "商品上架异常")
- 在search模块中实现了商品保存
ProductSaveController中:
ProductSaveServiceImpl中:@PostMapping("/product") public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) { boolean status = false; try{ status = productSaveService.productStatusUp(skuEsModels); } catch (IOException e) { return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg()); } if(status) return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg()); else return R.ok(); }
@Autowired RestHighLevelClient restHighLevelClient; @Override public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException { //1.在es中建立索引,建立映射关系, 使用postman工具发送请求实现,请求json在/resources/product-mapping.txt中 //2、在ES中保存数据 //BulkRequest bulkRequest, RequestOptions options BulkRequest bulkRequest = new BulkRequest(); for (SkuEsModel skuEsModel : skuEsModels) { //构造保存请求 IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX); indexRequest.id(skuEsModel.getSkuId().toString()); String jsonString = JSON.toJSONString(skuEsModel); indexRequest.source(jsonString, XContentType.JSON); bulkRequest.add(indexRequest); } BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); //TODO 如果批量报错 boolean hasFailures = bulk.hasFailures(); List<String> collect = Arrays.asList(bulk.getItems()).stream().map(item -> { return item.getId(); }).collect(Collectors.toList()); log.info("商品上架完成:{}",collect); return hasFailures; }
- 使用postman 工具,创建请求:
- 为了能让product模块使用上面的接口,需要在Feign包中创建接口
- 创建请求接口
@FeignClient("gulimall-search") public interface SearchFeignService { @PostMapping("/search/save/product") public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels); }
- 由于完成上架后要修改spu状态,所以在common.constant包下修改ProductConstant。添加关于商品状态的枚举。
public enum ProductStatusEnum{ NEW_SPU(0, "新建"), SPU_UP(1, "商品上架"), SPU_DOWN(2, "商品下架"); private int code; private String msg; ProductStatusEnum(int code, String msg) { this.code = code; this.msg = msg; } public int getCode() { return code; } public String getMsg() { return msg; } }
- 在dao中实现
updateSpuStatus
: 修改上架产品的状态void updateSpuStatus(@Param("spuId") Long spuId, @Param("code") int code);
<update id="updateSpuStatus"> update `pms_spu_info` set publish_status=#{code}, update_time=NOW() where id=#{spuId} </update>
- 创建请求接口
- up方法的全部:
@Override public void up(Long spuId) { //组装需要的数据 //1、查出当前spuid对应的所有sku信息,品牌的名字 List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId); List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList()); // TODO 4、查询当前sku的所有可以被用来检索的规格属性 List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrlistForSpu(spuId); List<Long> attrIds = baseAttrs.stream().map(attr -> { return attr.getAttrId(); }).collect(Collectors.toList()); //查出attrIds中用于search的属性id List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds); Set<Long> idSet = new HashSet<>(searchAttrIds); //留下所有满足条件的商品属性list List<SkuEsModel.Attr> attrsList = baseAttrs.stream().filter(item -> { return idSet.contains(item.getAttrId()); }).map(item -> { SkuEsModel.Attr attrs1 = new SkuEsModel.Attr(); BeanUtils.copyProperties(item, attrs1); return attrs1; }).collect(Collectors.toList()); // TODO 1、发送远程调用,库存系统查询是否有库存 Map<Long, Boolean> stockMap = null; try { R skuHasStock = wareFeignService.getSkusHasStock(skuIdList); TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {}; stockMap = skuHasStock.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item-> item.getHasStock())); } catch (Exception e) { log.error("库存服务查询异常: 原因{}", e); } Map<Long, Boolean> finalStockMap = stockMap; //遍历封装sku信息 List<SkuEsModel> upProducts = skus.stream().map(sku -> { SkuEsModel esModel = new SkuEsModel(); BeanUtils.copyProperties(sku, esModel); esModel.setSkuPrice(sku.getPrice()); esModel.setSkuImg(sku.getSkuDefaultImg()); //设置库存信息 if(finalStockMap == null) { esModel.setHasStock(true); } else { esModel.setHasStock(finalStockMap.get(sku.getSkuId())); } // TODO 2、热度评分。0 esModel.setHotScore(0L); // TODO 3、查询品牌和分类的名字信息 BrandEntity brand = brandService.getById(sku.getBrandId()); esModel.setBrandName(brand.getName()); esModel.setBrandImg(brand.getLogo()); CategoryEntity category = categoryService.getById(esModel.getCatalogId()); esModel.setCatalogName(category.getName()); // 设置检索属性 esModel.setAttrs(attrsList); return esModel; }).collect(Collectors.toList()); // TODO 5、将数据发送给es进行保存; gulimall-search; R r = searchFeignService.productStatusUp(upProducts); if(r.getCode() == 0) { //远程调用成功 // TODO 6、修改当前spu的状态,具体实现代码在SpuInfoDao.xml this.baseMapper.updateSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode()); } else { //远程调用失败 // TODO 7、重复调用? 接口幂等性? 重试机制? } }
(3) 测试商品上架服务
4、商城业务-首页
(1) 整合thymeleaf渲染首页
- 在product模块的pom.xml文件中导入thymeleaf依赖
<!-- 模板引擎 thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
- 导入前端页面
- 将index文件夹放在product模块resources/static目录下面,将index.html页面放在resources/templates目录下面
- control包改名app,并新增web包
- 以后所有rest接口从app包提供,所有controller从web包提供
- web包下的controller是返回给thymeleaf的,它默认的前缀和后缀分别是:
classpath:/templates/
和.html
,springboot访问项目的时候自动会找index.html
- 进行thymeleaf配置, yml关闭thymeleaf缓存
# 关闭thymeleaf缓存,以便修改代码后刷新页面能看出修改 spring: thymeleaf: cache: false
- 启动服务:
- 注意这里的端口号是你的product服务端口号
- 注意这里的端口号是你的product服务端口号
(4) 开发首页跳转功能
- 新建gulimall/product/web/IndexController.java
- 创建页面跳转方法
indexPage
@Autowired CategoryService categoryService; @GetMapping({"/", "index.html"}) public String indexPage(Model model) { // TODO 1、查出所有一级分类 List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys(); model.addAttribute("categories", categoryEntities); //视图解析器进行拼串,前缀classpath:/templates/返回值.html return "index"; //相当于return "classpath:/templates/index.html"; 拦截GetMapping路径后转到首页 }
- getLevel1Categorys()方法具体实现
@Override public List<CategoryEntity> getLevel1Categorys() { return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); }
(5) thymeleaf基础语法,渲染商城首页一级分类
- 首先,html标签加上thymeleaf命名空间xmlns:th="http://www.thymeleaf.org", 加上命名空间后就行使用thymeleaf的语法
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 添加热部署
<!-- 热部署 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
- index.html中获得数据,并通过thymeleaf遍历渲染
<!--轮播主体内容--> <div class="header_main"> <div class="header_banner"> <div class="header_main_left"> <ul> <li th:each="category : ${categorys}"> <a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}"><b th:text="${category.name}">家用电器</b></a> </li> </ul> ....
- 测试结果:
(6) 渲染二三级分类数据
- 创建接收数据vo: Catalog2Vo
@NoArgsConstructor @AllArgsConstructor @Data public class Catalog2Vo { private String catalog1Id; // 1级父分类ID private List<Catalog3Vo> catalog3List;// 3级子分类集合 private String id; // 2级分类ID private String name; // 2级分类name /** * 三级分类Vo */ @NoArgsConstructor @AllArgsConstructor @Data public static class Catalog3Vo { private String catalog2Id; // 2级父分类ID private String id; // 3级分类ID private String name; // 3级分类name } }
- 新增接口:
getCatalogJson
- 接口:
/** * 查出三级分类 * 1级分类作为key, 2级引用List * @return */ @ResponseBody @GetMapping("/index/catelog.json") public Map<String, List<Catalog2Vo>> getCatalogJson() { Map<String, List<Catalog2Vo>> map = categoryService.getCatalogJson(); return map; }
- 具体实现:
private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) { return selectList.stream().filter(item-> item.getParentCid() == parent_cid).collect(Collectors.toList()); } @Override public Map<String, List<Catalog2Vo>> getCatalogJson() { //获得所有数据 List<CategoryEntity> selectList = baseMapper.selectList(null); //1、 获得所有一级分类数据 List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L); //2、封装数据 Map<String, List<Catalog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> { //查到当前1级分类的2级分类 List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId()); List<Catalog2Vo> catalog2Vos = null; if (category2level != null) { catalog2Vos = category2level.stream().map(level12 -> { //查询当前二级分类的三级分类 List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId()); List<Catalog2Vo.Catalog3Vo> catalog3Vos = null; if (category3level != null) { catalog3Vos = category3level.stream().map(level13 -> { return new Catalog2Vo.Catalog3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName()); }).collect(Collectors.toList()); } return new Catalog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName()); }).collect(Collectors.toList()); } return catalog2Vos; })); return collect; }
- 测试结果:
- 接口:
(7) nginx通过域名访问
- 没有解决阿里云服务器内网穿透,老老实实用本地nginx了
- 本地nginx的nginx.conf:
worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; client_max_body_size 1024m; sendfile on; keepalive_timeout 65; upstream gulimall { server 192.168.56.1:88; } include ./servers/*; }
- severs/gulumall.conf
问题:nginx转发到网关时会丢掉许多信息,如host信息,但我们网关需要host来判断转发到什么微服务,所以需要一些设置避免host信息丢失
server { listen 80; #监听此端口 server_name gulimall.com; #监听此域名 #charset koi8-r; #access_log /var/log/nginx/log/host.access.log main; location / { #配置请求的路由 proxy_set_header Host $host; #坑点:Nginx代理给网关时会丢失请求的host等信息 proxy_pass http://gulimall; #因为主配置文件配置了上游服务器为网关地址,所以可以请求路由到http://192.168.xxx.xxx:10000/ } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} }
- 配置网关跳转信息
- id: gulimall_host_route uri: lb://gulimall-product predicates: - Host=**.gulimall.com
- 本地nginx的nginx.conf:
- 重启网关,nginx进行测试: