天天看點

Day409.商品上架 -谷粒商城一、商品上架二、商城業務

一、商品上架

1、項目架構圖

Day409.商品上架 -谷粒商城一、商品上架二、商城業務

2、product-es準備

ES在記憶體中,是以在檢索中優于mysql。

ES也支援叢集

,資料分片存儲。

  • 需求:
    • 上架的商品才可以在網站展示。
    • 上架的商品需要可以被檢索。(

      上架的商品就加入到es去被檢索

      )
    • 分析sku在es中如何存儲 商品mapping
  • 分析: 商品上架在es中是存sku還是spu?
    • 1)檢索的時候

      輸入名字

      ,是需要按照

      sku的title進行全文檢索

    • 2)檢素使用

      商品規格

      ,規格是

      spu的公共屬性

      ,每個spu是一樣的
    • 3)按照

      分類id

      進去的都是直接列出

      spu

      的,還可以切換。
    • 4〕我們如果将sku的全量資訊儲存到es中(包括spu屬性〕就太多字段了
  • 處理方案
    • 方案1:

      存儲的到

      一個索引

      記錄中,每個都帶有對應sku具體的attr屬性
      • {
            skuId:1
            spuId:11
            skyTitile:華為xx
            price:999
            saleCount:99
            attr:[
                {尺寸:5},
                {CPU:高通945},
                {分辨率:全高清}
        	]
                   
      • 缺點:
        • 如果每個sku都存儲規格參數(如尺寸),會有

          備援存儲

          ,因為每個spu對應的sku的規格參數都一樣
    • 方案2:

      存儲到

      兩個索引

      記錄中,一個存儲對應的spu/sku的id,然後去檢索查詢另一個記錄中的資料,就可以解決上面的備援存儲的問題
      • #sku索引
        {
            spuId:1
            skuId:11
        }
        
        
        #attr索引
        {
            skuId:11
            attr:[
                {尺寸:5},
                {CPU:高通945},
                {分辨率:全高清}
        	]
        }
                   
      • 缺點:
        • 先找到4000個符合要求的spu,再根據4000個spu查詢對應的屬性,封裝了4000個id,long 8B*4000=32000B=32KB 1K個人檢索,就是32MB
        • 是以會出現如果有大量并發的請求,就會出現一刹那32MB,我們就加兩個0,就會有3g多的資料在網絡傳輸
        • 這樣子就會出現

          網絡問題

    • 結論

      如果将規格參數單獨建立索引,會出現檢索時出現大量資料傳輸的問題,會引起網絡網絡

      是以選用

      方案1

      ,以空間換時間
  • 建立product索引

    索引資料模型

    PUT product
    {
        "mappings":{
            "properties": {
                "skuId":{ "type": "long" },
                "spuId":{ "type": "keyword" },  # 不可分詞
                "skuTitle": {
                    "type": "text",
                    "analyzer": "ik_smart"  # 中文分詞器
                },
                "skuPrice": { "type": "keyword" },
                "skuImg"  : { "type": "keyword" },
                "saleCount":{ "type":"long" },
                "hasStock": { "type": "boolean" },
                "hotScore": { "type": "long"  },
                "brandId":  { "type": "long" },
                "catalogId": { "type": "long"  },
                "brandName": {"type": "keyword"},
                "brandImg":{
                    "type": "keyword",
                    "index": false,  # 不可被檢索,不生成index
                    "doc_values": false # 不可被聚合
                },
                "catalogName": {"type": "keyword" },
                "attrs": {
                    "type": "nested",
                    "properties": {
                        "attrId": {"type": "long"  },
                        "attrName": {
                            "type": "keyword",
                            "index": false,
                            "doc_values": false
                        },
                        "attrValue": {"type": "keyword" }
                    }
                }
            }
        }
    }
               
    其中
    • “type”: “keyword” 保持資料精度問題,可以檢索,但不分詞
    • “index”:false 代表不可被檢索
    • “doc_values”: false 不可被聚合,es就不會維護一些聚合的資訊
    • 備援存儲的字段: 不用來檢索,也不用來分析,節省空間
    • 庫存是bool。
    • 檢索品牌id,但是不檢索品牌名字、圖檔 用skuTitle檢索
  • nested嵌入式對象

屬性是"type": “nested”,因為是内部的屬性進行檢索

Day409.商品上架 -谷粒商城一、商品上架二、商城業務

數組類型的對象會被

扁平化處理

(對象的每個屬性會分别存儲到一起)

user.name=["aaa","bbb"] 
user.addr=["ccc","ddd"]
           
Day409.商品上架 -谷粒商城一、商品上架二、商城業務

這種存儲方式,可能會發生如下錯誤:

錯誤檢索到{aaa,ddd},這個組合是不存在的

數組的扁平化處理會使檢索能檢索到本身不存在的,為了解決這個問題,就采用了

嵌入式屬性

,數組裡是對象時用嵌入式屬性(不是對象無需用嵌入式屬性)

nested閱讀:https://blog.csdn.net/weixin_40341116/article/details/80778599

使用聚合:https://blog.csdn.net/kabike/article/details/101460578

3、正式編碼

  • controller
//POST /product/spuinfo/{spuId}/up
//上架
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId")Long spuId){
    spuInfoService.up(spuId);
    return R.ok();
}
           
  • 傳輸to對象

因為這裡的search服務也需要使用到這個to對象,是以我們建立在common中

商品上架需要在es中儲存spu資訊并更新spu的狀态資訊,由于SpuInfoEntity與索引的資料模型并不對應,是以我們要建立專門的vo進行資料傳輸

com.achang.common.to.es.SkuEsModel

@Data
public class SkuEsModel implements Serializable {
    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;
    }
}
           
  • service

com.achang.achangmall.product.service.impl.SpuInfoServiceImpl

//商品上架
    @Override
    public void up(Long spuId) {

        //1、查詢目前spuId對應的所有sku資訊,品牌的名字
        List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);

        List<Long> skuIdList = skuInfoEntities.stream().map(item -> {
            return item.getSkuId();
        }).collect(Collectors.toList());

        //todo 查詢目前sku可以被檢索的規格屬性
        List<ProductAttrValueEntity> baseAttrList = productAttrValueService.baseAttrList(spuId);
        List<Long> attrIdList = baseAttrList.stream().map(item -> {
            return item.getAttrId();
        }).collect(Collectors.toList());

        List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIdList);
        Set<Long> idSet = new HashSet<>(searchAttrIds);

        List<SkuEsModel.Attrs> attrsList = baseAttrList.stream().filter(item -> {
            return idSet.contains(item.getAttrId());
        }).map(s -> {
            SkuEsModel.Attrs attrsEs = new SkuEsModel.Attrs();
            BeanUtils.copyProperties(s, attrsEs);
            return attrsEs;

        }).collect(Collectors.toList());

        Map<Long, Boolean> stockMap = null;
        try {
            //發送遠端調用,庫存系統查詢是否有庫存
            R r = wareFeignService.getSkuHasStock(skuIdList);
            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
            stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));

        }catch (Exception e){
            log.error("庫存服務查詢出現問題:原因【{}】",e);//如果遠端調用失敗
        }


        //2、封裝每個sku的資訊
        Map<Long, Boolean> finalStockMap = stockMap;
        List<SkuEsModel> collect = skuInfoEntities.stream().map(i -> {
            SkuEsModel model = new SkuEsModel();
            BeanUtils.copyProperties(i,model);
            model.setSkuPrice(i.getPrice());
            model.setSkuImg(i.getSkuDefaultImg());
            model.setHasStock(  (finalStockMap != null)? finalStockMap.get(i.getSkuId()):true );//設定庫存資訊
            //熱度評分 0
            model.setHotScore(0L);
            //查詢品牌 和 分類名字資訊
            BrandEntity brandEntity = brandService.getById(model.getBrandId());
            model.setBrandName(brandEntity.getName());
            model.setBrandImg(brandEntity.getLogo());
            CategoryEntity categoryEntity = categoryService.getById(model.getCatalogId());
            model.setCatalogName(categoryEntity.getName());

            //設定檢索屬性
            model.setAttrs(attrsList);

            return model;
        }).collect(Collectors.toList());

        //發給es進行儲存,遠端調用
        R r = esFeignService.productStatusUp(collect);

        String code = (String) r.get("code");
        if (Integer.parseInt(code) == 0){
            //調用成功
            //todo 修改spu商品狀态
            this.baseMapper.updateSpuStatus(spuId, ProductConsatnt.StatusEnum.SPU_UP.getCode());
        }else {
            //todo 調用失敗,重複調用,接口幂等性;重試機制
        }
    }
           
  • 在指定的所有屬性集合中,篩選中檢索屬性

com.achang.achangmall.product.service.impl.AttrServiceImpl

//在指定的所有屬性集合中,篩選中檢索屬性
@Override
public List<Long> selectSearchAttrIds(List<Long> attrIdList) {

    List<AttrEntity> attrEntities = this.baseMapper.selectList(new QueryWrapper<AttrEntity>()
                                                               .eq("search_type", 1)
                                                               .in("attr_id", attrIdList).select("attr_id"));

    List<Long> searchAttrIdList = attrEntities.stream().map(item -> {
        return item.getAttrId();
    }).collect(Collectors.toList());

    return searchAttrIdList;
}
           
  • 遠端調用接口—庫存服務

com.achang.achangmall.ware.controller.WareSkuController

//查詢sku是否有庫存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List<Long> skuIdList){
    List<SkuHasStockVo> vos =  wareSkuService.getSkuHasStock(skuIdList);
    return R.ok().put("data",vos);
}
           

com.achang.achangmall.ware.vo.SkuHasStockVo

@Data
public class SkuHasStockVo {
    private Long skuId;
    private Boolean hasStock;
}
           

com.achang.achangmall.ware.controller.WareSkuController

//查詢sku是否有庫存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List<Long> skuIdList){
    List<SkuHasStockVo> vos =  wareSkuService.getSkuHasStock(skuIdList);
    return R.ok().setData(vos);
}
           

com.achang.achangmall.ware.service.impl.WareSkuServiceImpl

@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIdList) {
    List<SkuHasStockVo> collect = skuIdList.stream().map(item -> {
        SkuHasStockVo vo = new SkuHasStockVo();
        Long count = baseMapper.getSkuStock(item);
        vo.setSkuId(item);
        vo.setHasStock(count == null?false:count>0);
        return vo;
    }).collect(Collectors.toList());
    return collect;
}
           

achangmall-ware/src/main/resources/mapper/ware/WareSkuDao.xml

<select id="getSkuStock" resultType="java.lang.Long">
    SELECT  SUM(stock - stock_locked)
    FROM  `wms_ware_sku`
    WHERE `sku_id` = #{item}
</select>
           

遠端調用com.achang.achangmall.product.feign.WareFeignService

@FeignClient("achangmall-ware")
public interface WareFeignService {
    @PostMapping("/ware/waresku/hasstock")
    public R getSkuHasStock(@RequestBody List<Long> skuIdList);
}
           

遠端調用ES添加索引功能

com.achang.achangmall.search.controller.ESSaveController

@RequestMapping("/search")
@RestController
@Slf4j
public class ESSaveController {

    @Autowired
    private ProductSaveService productSaveService;

    //上架商品
    @PostMapping("/save/product")
    public R productStatusUp(@RequestBody List<SkuEsModel> esModels) {
        Boolean flag = false;
        try {
            flag =  productSaveService.productStatusUp(esModels);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("ESSaveController,商品上架錯誤:{}",e);
            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
        }

        if (!flag){
            return R.ok();
        }else {
            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
        }
    }

}
           

com.achang.achangmall.search.constant.ESConstant

public class ESConstant {
    public static final String PRODUCT_INDEX = "product";//sku資料在es中的索引
}
           

com.achang.achangmall.search.service.impl.ProductSaveServiceImpl

@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    //上架商品
    @Override
    public Boolean productStatusUp(List<SkuEsModel> esModels) throws IOException {
        //給es中建立索引
        //給es儲存資料
        BulkRequest bulkRequest = new BulkRequest();
        esModels.forEach(item->{
            //構造儲存請求
            IndexRequest indexRequest = new IndexRequest(ESConstant.PRODUCT_INDEX);
            String json = JSON.toJSONString(item);
            indexRequest.source(json, XContentType.JSON);
            indexRequest.id(item.getSkuId().toString());

            bulkRequest.add(indexRequest);
        });
        BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, ElasticsearchConfig.COMMON_OPTIONS);

        //todo 批量錯誤
        boolean b = bulk.hasFailures();
        List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
            return item.getId();
        }).collect(Collectors.toList());
        log.info("商品上架完成:【{}】",collect);

        return b;
    }
    
}
           

com.achang.achangmall.product.feign.ESFeignService

@FeignClient("achangmall-search")
public interface ESFeignService {

    @PostMapping("/search/save/product")
    public R productStatusUp(@RequestBody List<SkuEsModel> esModels);
}
           

achangmall-product/src/main/resources/mapper/product/SpuInfoDao.xml

<update id="updateSpuStatus">
        update `pms_spu_info`
        set publish_status =#{code},update_time = NOW()
        where id = #{spuId}
    </update>
           

二、商城業務

明天繼續