• 谷粒商城(四)


    商品服务-商品上架

    上架的商品才可以在网站展示
    上架的商品需要可以检索

    1、商品 Mapping

    分析:商品上架在 es 中是存 sku 还是 spu ?

    1. 检索的时候输入名字,是需要按照 sku 的 title 进行全文检索的
    2. 检索使用商品规格,规格是 spu 的公共属性,每个 spu 是一样的
    3. 按照分类 id 进去的都是直接列出 spu 的,还可以切换
    4. 我们如果将 sku 的全量信息保存到 es 中(包括 spu 属性)就太多量字段了
    5. 我们如果将 spu 以及他包含的 sku 信息保存到 es 中,也可以方便检索。但是 sku 属于 spu 的级联对象,在 es 中需要 nested 模型,这种性能差点
    6. 但是存储与检索我们必须性能折中
    7. 如果我们分拆存储,spu 和 attr 一个索引,sku 单独一个索引可能涉及的问题

    检索商品的名字,如“手机”,对应的 spu 有很多,我们要分析出这些 spu 的所有关联属性,再做一次查询,就必须将所有 spu_id 都发出去。假设有 1 万个数据,数据传输一次就10000*4=4MB;并发情况下假设 1000 检索请求,那就是 4GB 的数据,,传输阻塞时间会很长,业务更加无法继续。
    所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数据库范式

    PUT product
    {
      "mappings":{
        "properties":{
          "skuId":{
            "type":"long"
          },
           "spuId":{
            "type":"keyword"
          },
           "skuTitle":{
            "type":"text",
            "analyzer": "ik_smart"
          },
           "skuPrice":{
            "type":"keyword"
          },
           "skuImg":{
            "type":"text",
            "analyzer": "ik_smart"
          },
           "saleCount":{
            "type":"long"
          },
           "hasStock":{
            "type":"boolean"
          },
          "hotScore":{
            "type":"long"
          },
          "brandId":{
            "type":"long"
          },
          "catelogId":{
            "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"
              }
            }
          }
        }
      }
    }
    
    • 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

    index :
    默认 true,如果为 false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能当做检索条件。
    doc_values :
    默认 true,设置为 false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。还可以通过设定 doc_values 为 true,index 为 false 来让字段不能被搜索但可以用于排序、聚合以及脚本操作
    nested :
    表示数组数据是嵌入式的,默认不是嵌入式,ES默认数组数据是扁平化处理
    在这里插入图片描述

    上架细节

    上架是将后台的商品放在 es 中可以提供检索和查询功能

    1. hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要更新一下 es
    2. 库存补上以后,也需要重新更新一下 es
    3. hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新热度值。
    4. 下架就是从 es 中移除检索项,以及修改 mysql 状态

    商品上架步骤:

    1. 先在 es 中按照之前的 mapping 信息,建立 product 索引。
    2. 点击上架,查询出所有 sku 的信息,保存到 es 中
    3. es 保存成功返回,更新数据库的上架状态信息。

    2、接口编写

    在这里插入图片描述

    查看接口文档:20、商品上架

    因为数据需要存入 ES,在 common 模块里面新建实体类
    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 hotScore;
        private Long brandId;
        private Long catelogId;
        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;
        }
    }
    
    • 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

    SpuInfoController

    //商品上架
    @RequestMapping("/{spuId}/up")
    public R up(@PathVariable("spuId") Long spuId){
        spuInfoService.up(spuId);
        return R.ok();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    SpuInfoServiceImpl

    //商品上架:查出当前 spuid 对应的所有信息封装为 SkuEsModel,发送给 es保存
    @Override
    public void up(Long spuId) {
        //1、查询sku信息(一个spu对应多个sku)
        List<SkuInfoEntity> skus = skuInfoService.getSkuBySpuId(spuId);
        //2、sku信息封装为 SkuEsModel
        //skuPrice skuImg hasStock hotScore brandName brandImg catalogName attrs
        //TODO 2.1、发送远程调用,库存系统查询是否有库存(hasStock)
        List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
        //将list转换为map
        Map<Long, Boolean> skuHasStock = null;
        try {
            R r = wareFeignService.getSkuHasStock(skuIdList);
            TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {};
            skuHasStock = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
        } catch (Exception e) {
            log.error("库存服务异常:原因:{}",e);
            e.printStackTrace();
        }
    
        //TODO 2.4、查询当前sku的所有可以用来被检索的基本规格属性(attrs)
        //基本属性是跟着spu走,销售属性是跟着sku,所以对于同一个spu下的sku,是同一类基本属性
        //属性名、值是在pms_product_attr_value表,是否被检索是在pms_attr表的search_type字段
        List<ProductAttrValueEntity> attrValues = productAttrValueService.list(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
        //筛选出可检索的属性
        List<Long> attrIds = attrValues.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
        List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
        //再次筛选,过滤掉不包含在 searchAttrIds集合中的元素
        HashSet<Long> searchAttrIdsHashSet = new HashSet<>(searchAttrIds);
        //封装可被检索的规格属性
        List<SkuEsModel.Attrs> attrs = attrValues.stream().filter(item  -> {
            //过滤掉不包含在 searchAttrIds集合中的元素
            return searchAttrIds.contains(item.getAttrId());
        }).map(item -> {
            SkuEsModel.Attrs attr = new SkuEsModel.Attrs();
            BeanUtils.copyProperties(item,attr);
            return attr;
        }).collect(Collectors.toList());
    
        Map<Long, Boolean> finalSkuHasStock = skuHasStock;
        List<SkuEsModel> upProducts = skus.stream().map(sku -> {
            SkuEsModel skuEsModel = new SkuEsModel();
            BeanUtils.copyProperties(sku,skuEsModel);
            skuEsModel.setSkuPrice(sku.getPrice());
            skuEsModel.setSkuImg(sku.getSkuDefaultImg());
    
            //2.1 设置库存信息(防止此处多次调用远程服务,所以在循环外部查询)
            skuEsModel.setHasStock(finalSkuHasStock != null && finalSkuHasStock.get(sku.getSkuId()));
    
            //TODO 2.2、热度评分默认为 0
            skuEsModel.setHotScore(0L);
            //TODO 2.3、查询品牌和商品分类的信息
            BrandEntity brand = brandService.getById(sku.getBrandId());
            skuEsModel.setBrandImg(brand.getName());
            skuEsModel.setBrandImg(brand.getLogo());
            CategoryEntity category = categoryService.getById(sku.getCatalogId());
            skuEsModel.setCatalogName(category.getName());
    
            //2.4 设置属性
            skuEsModel.setAttrs(attrs);
    
            return skuEsModel;
        }).collect(Collectors.toList());
    
        //TODO 3、将数据发送给 es保存,直接发送给 search服务
        R r = searchFeignService.productStatusUp(upProducts);
        if (r.getCode() == 0) {
            // 远程调用成功
            // TODO 3.1、修改当前 spu 的状态
            baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
        } else {
            // 远程调用失败
            //TODO 4、重复调用?接口冥等性、重试机制
    
            /**
             * feign源码分析:
             * 1、构造请求数据,将对象转成json
             * RequestTemplate template = buildTemplateFromArgs.create(argv);
             * 2、发送请求进行执行(执行成功进行解码)
             *  executeAndDecode(template);
             * 3、执行请求会有重试机制
             *  while (true) {
             *       try {
             *         return executeAndDecode(template);
             *       } catch (RetryableException e) {
             *         try {
             *           retryer.continueOrPropagate(e);
             *         } catch (RetryableException th) {
             *              throw cause;
             *         }
             *         continute
             */
        }
    }
    
    • 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

    远程服务查询有无库存

    发送远程调用,库存系统查询是否有库存

    common 模块里面新建实体类用于传输数据
    SkuHasStockTo

    @Data
    public class SkuHasStockTo {
        private Long skuId;
        private Boolean hasStock;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    gulimall-ware 服务里的接口
    WareSkuController

    /**
     * 查询指定sku是否有库存
     */
    @PostMapping("/hasStock")
    public R getSkuHasStock(@RequestBody List<Long> skuIds){
        List<SkuHasStockTo> tos = wareSkuService.getSkuStock(skuIds);
        R r = R.ok().setData(tos);
        return r;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    为了方便传递数据,修改了 R 的代码,添加了泛型方法,存取数据

    public class R extends HashMap<String, Object> {
    
    	//利用fastjson进行逆转,泛型方法:调用时指定要拿到的数据类型
    	//注意是 com.alibaba.fastjson.TypeReference
    	public <T> T getData(TypeReference<T> typeReference){
    		Object data = get("data");
    		//此处不能直接将data强转为指定要拿到的数据类型
    		String s = JSON.toJSONString(data);
    		T t = JSON.parseObject(s,typeReference);
    		return t;
    	}
    
    	public R setData(Object object){
    		put("data",object);
    		return this;
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    为什么上面不能直接将data强转为指定要拿到的数据类型?
    注意:
    因为 map 中的 value 为一个对象,在 springmvc 中取出这个对象时会将这个对象默认转为 map
    SpringMVC 对于 Object 转 json 的时候,会变成 key-value 的形式
    示例:
    当方法返回 HashMap——value为List——List泛型为自定义类对象

    以下是正常情况下方法执行结果
    在这里插入图片描述

    以下是远程调用返回结果
    会将 map 值value 里面的集合 list 里面的对象转化为 map键值对
    在这里插入图片描述
    示例:如果直接返回 List 集合也会有上面的问题
    在这里插入图片描述
    但是如果直接返回自定义类对象,不会被转化为键值对
    在这里插入图片描述

    gulimall-product 服务里接收数据

    //TODO 2.1、发送远程调用,库存系统查询是否有库存(hasStock)
    List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
    //将list转换为map
    Map<Long, Boolean> skuHasStock = null;
    try {
        R r = wareFeignService.getSkuHasStock(skuIdList);
        TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {};
        skuHasStock = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
    } catch (Exception e) {
        log.error("库存服务异常:原因:{}",e);
        e.printStackTrace();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    注意:以下写法有误

    gulimall-product 服务里的商品上架 SpuInfoServiceImpl

    R<List<SkuHasStockTo>> r = wareFeignService.getSkuHasStock(skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList()));
    //将list转换为map
    Map<Long, Boolean> skuHasStock = r.getData().stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
    
    • 1
    • 2
    • 3

    gulimall-ware 服务里的接口 WareSkuController

    R<List<SkuHasStockTo>> getSkuHasStock(@RequestBody List<Long> skuIds){
        List<SkuHasStockTo> tos = wareSkuService.getSkuStock(skuIds);
        R r = R.ok();
        r.setData(tos);
        return r; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此处为了方便传递数据,修改了 R 的代码,添加了泛型属性

    public class R<T> extends HashMap<String, Object> { 	
        private T data; 	
        public T getData() {return data;} 	
        public void setData(T data) {this.data = data;} 
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    错误原因:
    因为Jackson对于HashMap类型会有特殊的处理方式,具体来说就是会对类进行向上转型为Map,导致子类的私有属性消失
    就会导致在 gulimall-product 服务里 r.getData() 拿不到属性值数据
    所以将 R 修改为泛型类—— pass

    WareSkuService

    //查询指定skuid列表是否由库存
    List<SkuHasStockTo> getSkuStock(List<Long> skuIds);
    
    @Override
    public List<SkuHasStockTo> getSkuStock(List<Long> skuIds) {
        return skuIds.stream().map(id -> {
            SkuHasStockTo to = new SkuHasStockTo();
            //SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` where sku_id = ?
            //注意这里接收 count 的类型是 Long,因为查询出来的结果可能是null,需要用包装类
            Long count = baseMapper.getSkuStockById(id);
            to.setSkuId(id);
            to.setHasStock(count != null && count > 0);
            return to;
        }).collect(Collectors.toList());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    <select id="getSkuStockById" resultType="java.lang.Long">
        SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` 
        where sku_id = #{id}
    select>
    
    • 1
    • 2
    • 3
    • 4

    gulimall-product 服务里的接口
    WareFeignService

    @FeignClient("gulimall-ware")
    public interface WareFeignService {
        //查询指定sku是否有库存
        @PostMapping("/ware/waresku/hasStock")
        R getSkuHasStock(@RequestBody List<Long> skuIds);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    AttrService
    给定属性id列表,从中筛选出可检索属性列表

    //给定属性id列表,从中筛选出可检索属性列表
    List<Long> selectSearchAttrs(List<Long> attrIds);
    
    @Override
    public List<Long> selectSearchAttrs(List<Long> attrIds) {
        return attrDao.selectSearchAttrs(attrIds);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    <select id="selectSearchAttrs" 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ES保存数据

    gulimall-search 模块中编写保存数据接口

    ElasticSaveController

    @Slf4j
    @RequestMapping("search/save")
    @RestController
    public class ElasticSaveController {
    
        @Autowired
        ProductSaveService productSaveService;
    
        @PostMapping("/product")
        public R productStatusUp(@RequestBody List<SkuEsModel> list) throws IOException {
            boolean b = false;
            try {
                b = productSaveService.productStatusUp(list);
            } catch (IOException e) {
                log.error("ElasticSaveController商品上架错误",e);
                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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    BizCodeEnume :common 模块里面的异常常量类

    public enum BizCodeEnume {
        UNKNOW_EXCEPTION(10000,"系统未知异常"),
        VAILD_EXCEPTION(10001,"参数格式校验失败"),
        PRODUCT_UP_EXCEPTION(11000,"商品上架异常");
    
        private int code;
        private String msg;
        BizCodeEnume(int code,String msg){
            this.code = code;
            this.msg = msg;
        }
    
        public int getCode() {
            return code;
        }
    
        public String getMsg() {
            return msg;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    ProductSaveService

    public interface ProductSaveService{
        /**
         * @param list
         * @return false 批量保存错误;true 批量保存成功
         * @throws IOException
         */
        Boolean productStatusUp(List<SkuEsModel> list) throws IOException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ProductSaveServiceImpl

    @Service("productSaveServiceImpl")
    public class ProductSaveServiceImpl implements ProductSaveService {
        @Autowired
        RestHighLevelClient restHighLevelClient;
    
        @Override
        public Boolean productStatusUp(List<SkuEsModel> list) throws IOException {
            //先要在es中建立索引,再在es中保存数据;因为此处数据较多,使用批量保存
            BulkRequest bulkRequest = new BulkRequest();
            for (SkuEsModel skuEsModel : list) {
                //指定存储的索引
                IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
                indexRequest.id(skuEsModel.getSkuId().toString());  //指定唯一id
                String s = JSON.toJSONString(skuEsModel);
                indexRequest.source(s, XContentType.JSON);
                bulkRequest.add(indexRequest);
            }
            //参数:BulkRequest bulkRequest, RequestOptions options
            BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
            //TODO 如果批量错误
            //false 批量保存错误;true 批量保存成功
            boolean b = bulk.hasFailures();
            return !b;
        }
    }
    
    • 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

    EsConstant 常量类

    public class EsConstant {
        //sku数据在es中的索引
        public static final String PRODUCT_INDEX = "product";
    }
    
    • 1
    • 2
    • 3
    • 4

    商城业务-首页整合

  • 相关阅读:
    【python】算法与数据结构作业记录分析
    AR导览软件定制开发方案
    shell编程
    手机待办事项app哪个好?
    目标检测 YOLOv5 开源代码项目调试与讲解实战土堆 课程笔记
    【TypeScript】学习笔记(二)
    SpringSecurity知识点总结-DX的笔记
    领域模型优先于数据库表
    ROC曲线简明讲解
    激光雷达数据的25个重要应用介绍
  • 原文地址:https://blog.csdn.net/qq_48496502/article/details/126223979