【项目学习】谷粒商城学习记录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分表一样,将冗余数据分开单独建立索引
      优点:减少空间使用
      缺点:由于规格标签都是动态变化的,每次每个用户都会发送请求去查询各种不同数据,导致请求访问量太大,可能引起网络阻塞
  • 最终使用方案一,如下是所使用的建立索引请求:

    说明:{"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>
      
  • 上面在库存模块中实现了方法,但是要想在商品模块使用,得通过远程调用。在商品模块下的Feign包下新建WareFeignService接口。
    @FeignClient("gulimall-waer")
    public interface WareFeignService {
        @PostMapping("/ware/waresku/hasstock")
        public R getSkusHasStock(@RequestBody List<Long> skuIds);
    }
    
  • up()中调用远程接口查询库存量信息,注意这里修改了公共类中的R,添加了getData()方法。
    // 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);
    }
    
    下面是R的修改
    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中:
      @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();
      }
      
      ProductSaveServiceImpl中:
      @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;
      
      }
      
  • 为了能让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服务端口号

(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进行测试:
posted @ 2023-11-26 17:23  A_sc  阅读(102)  评论(0编辑  收藏  举报