谷粒商城 高级篇 (五) --------- 商品上架


前言

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


一、商品 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

product 的 mapping

{ 
	"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:

默认 true,如果为 false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能当做检索条件。

doc_values:

默认 true,设置为 false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。还可以通过设定 doc_values 为 true,index 为 false 来让字段不能被搜索但可以用于排序、聚合以及脚本操作。

二、上架细节

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

1、hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要更新一下 es

2、库存补上以后,也需要重新更新一下 es

3、hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新热度值。

4、下架就是从 es 中移除检索项,以及修改 mysql 状态

商品上架步骤:

A、先在 es 中按照之前的 mapping 信息,建立 product 索引。

B、点击上架,查询出所有 sku 的信息,保存到 es 中。

C、es 保存成功返回,更新数据库的上架状态信息。

数据一致性

A、商品无库存的时候需要更新 es 的库存信息
B、商品有库存也要更新 es 的信息

三、具体实现

首先实现 SpuInfioController 下的 spuUp 接口

//商品上架
///product/spuinfo/{spuId}/up
@PostMapping(value = "/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId) {

    spuInfoService.up(spuId);

    return R.ok();
}

对应的 spuInfoServiceImpl 中实现 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 = attrValueService.baseAttrlistforspu(spuId);
    List<Long> attrIds = baseAttrs.stream().map(attr -> {
        return attr.getAttrId();
    }).collect(Collectors.toList());

    List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);

    Set<Long> idSet = new HashSet<>(searchAttrIds);

    List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
        return idSet.contains(item.getAttrId());
    }).map(item -> {
        SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();
        BeanUtils.copyProperties(item, attrs1);
        return attrs1;
    }).collect(Collectors.toList());

    //TODO 1、发送远程调用,库存系统查询是否有库存
    Map<Long, Boolean> stockMap = null;
    try{
        R r = wareFeignService.getSkusHasStock(skuIdList);
        //
        TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {
        };
        stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
    }catch (Exception e){
        log.error("库存服务查询异常:原因{}",e);
    }


    //2、封装每个sku的信息
    Map<Long, Boolean> finalStockMap = stockMap;
    List<SkuEsModel> upProducts = skus.stream().map(sku -> {
        //组装需要的数据
        SkuEsModel esModel = new SkuEsModel();
        BeanUtils.copyProperties(sku,esModel);
        //skuPrice,skuImg,
        esModel.setSkuPrice(sku.getPrice());
        esModel.setSkuImg(sku.getSkuDefaultImg());
        //hasStock,hotScore
        //设置库存信息
        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(esModel.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的状态
        baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
    }else {
        //远程调用失败
        //TODO 7、重复调用?接口幂等性;重试机制?xxx
        //Feign调用流程
        /**
         * 1、构造请求数据,将对象转为json;
         *      RequestTemplate template = buildTemplateFromArgs.create(argv);
         * 2、发送请求进行执行(执行成功会解码响应数据):
         *      executeAndDecode(template);
         * 3、执行请求会有重试机制
         *      while(true){
         *          try{
         *            executeAndDecode(template);
         *          }catch(){
         *              try{retryer.continueOrPropagate(e);}catch(){throw ex;}
         *              continue;
         *          }
         *
         *      }
         */
    }
}

我们来看这个方法,首先查出 spuid 对应的 sku 信息,即 getSkusBySpuId() 方法实现

@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
    List<SkuInfoEntity> list = this.list(new QueryWrapper<SkuInfoEntity>().eq("spu_id",spuId));
    return list;
}

然后 baseAttrlistforspu 的实现,查询可用来被检索的属性,这个我们之前已经实现过。

构造 es 检索基本数据 SkuEsModel,我们将其放到 common 模块下

package com.fancy.common.to.es;

import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

@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;

    }
}

远程调用 ware 模块相关接口的实现

在这里插入图片描述

WareSkuController 中:

//查询sku是否有库存
@PostMapping("/hasstock")
public R getSkusHasStock(@RequestBody List<Long> skuIds){

    //sku_id,stock
    List<SkuHasStockVo> vos =  wareSkuService.getSkusHasStock(skuIds);


    return R.ok().setData(vos);
}

R 工具类的相关调整:

package com.fancy.common.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;

import java.util.HashMap;
import java.util.Map;

import static jdk.management.resource.internal.SimpleResourceContext.get;

public class R extends HashMap<String, Object> {
	private static final long serialVersionUID = 1L;

	//利用fastjson进行逆转
	public <T> T getData(String key, TypeReference<T> typeReference){
		Object data = get(key);//默认是map
		String s = JSON.toJSONString(data);
		T t = JSON.parseObject(s, typeReference);
		return t;
	}

	//利用fastjson进行逆转
	public <T> T getData(TypeReference<T> typeReference){
		Object data = get("data");//默认是map
		String s = JSON.toJSONString(data);
		T t = JSON.parseObject(s, typeReference);
		return t;
	}
	public R setData(Object data){
		put("data",data);
		return this;
	}

	public R() {
		put("code", 0);
		put("msg", "success");
	}

	public static R error() {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
	}

	public static R error(String msg) {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}

	public static R error(int code, String msg) {
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R ok(String msg) {
		R r = new R();
		r.put("msg", msg);
		return r;
	}

	public static R ok(Map<String, Object> map) {
		R r = new R();
		r.putAll(map);
		return r;
	}

	public static R ok() {
		return new R();
	}

	public R put(String key, Object value) {
		super.put(key, value);
		return this;
	}
	public  Integer getCode() {

		return (Integer) this.get("code");
	}

}

SkuHasStockVo 的封装

package com.fancy.gulimall.ware.vo;

import lombok.Data;

@Data
public class SkuHasStockVo {

    private Long skuId;
    private Boolean hasStock;
}

getSkusHasStock 的实现

在这里插入图片描述

getSkuStock 的 mapper 映射文件

在这里插入图片描述

将数据发送给 es 进行保存的相关实现

在这里插入图片描述
ElasticSaveController 的实现:

package com.fancy.gulimall.search.controller;


import com.fancy.common.exception.BizCodeEnum;
import com.fancy.common.to.es.SkuEsModel;
import com.fancy.common.utils.R;
import com.fancy.gulimall.search.service.ProductSaveService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

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

    @Autowired
    ProductSaveService productSaveService;

    //上架商品
    @PostMapping("/product")
    public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
        boolean b = false;
        try {
            b = productSaveService.productStatusUp(skuEsModels);
        } catch (Exception e) {
            log.error("ElasticSaveController商品上架错误:{}", e);
            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
        }

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

BizCodeEnum 的调整

package com.fancy.common.exception;
/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为 5 为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 * 10: 通用
 * 001:参数格式校验
 * 11: 商品
 * 12: 订单
 * 13: 购物车
 * 14: 物流
 *
 *
 */
public enum BizCodeEnum {


    UNKNOWN_EXCEPTION(10000, "系统未知异常"),
    VALID_EXCEPTION(10001, "参数格式校验失败"),

    TOO_MANY_REQUEST(10002,"请求流量过大"),

    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),

    USER_EXIST_EXCEPTION(15001,"用户存在"),

    PHONE_EXIST_EXCEPTION(15002,"手机号存在"),

    NO_STOCK_EXCEPTION(21000,"商品库存不足"),

    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号密码错误");

    private int code;
    private String msg;

    BizCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

productStatusUp 方法的实现:

@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {

    //保存到es
    //1、给es中建立索引。product,建立好映射关系。

    //2、给es中保存这些数据
    //BulkRequest bulkRequest, RequestOptions options
    BulkRequest bulkRequest = new BulkRequest();
    for (SkuEsModel model : skuEsModels) {
        //1、构造保存请求
        IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
        indexRequest.id(model.getSkuId().toString());
        String s = JSON.toJSONString(model);
        indexRequest.source(s, XContentType.JSON);

        bulkRequest.add(indexRequest);
    }

    BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, ElasticSearchConfig.COMMON_OPTIONS);

    //TODO 1、如果批量错误
    boolean b = bulk.hasFailures();
    List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
        return item.getId();
    }).collect(Collectors.toList());
    log.info("商品上架完成:{},返回数据:{}",collect,bulk.toString());


    return b;

}

ElasticSearchConfig 中:

package com.fancy.gulimall.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticSearchConfig {

    public static final RequestOptions COMMON_OPTIONS;
    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
//        builder.addHeader("Authorization", "Bearer " + TOKEN);
//        builder.setHttpAsyncResponseConsumerFactory(
//                new HttpAsyncResponseConsumerFactory
//                        .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
        COMMON_OPTIONS = builder.build();
    }

    @Bean
    public RestHighLevelClient esRestClient(@Value("${spring.elasticsearch.jest.uris}")String esUrl){

        //TODO 修改为线上的地址
        RestClientBuilder builder = null;
        //final String hostname, final int port, final String scheme

//        builder = RestClient.builder(new HttpHost("192.168.38.130", 9200, "http"));
        builder = RestClient.builder(HttpHost.create(esUrl));
        RestHighLevelClient client = new RestHighLevelClient(builder);
//        RestHighLevelClient client = new RestHighLevelClient(
//                RestClient.builder(
//                        new HttpHost("192.168.38.130", 9200, "http")));
        return client;
    }

}

在配置文件中,将spring.elasticsearch.jest.uris映射成 es 地址即可

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在森林中麋了鹿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值