商品上架
接口功能详情
- 修改spu状态为已上架
保存sku信息到es中,以skuId作为文档ID
sku默认图片sku标题sku价格销量spu允许被检索的基本规格(尺寸、CPU、分辨率)
商品json文档格式分析
方式一:
格式1:(商品索引保存sku+attr信息)【浪费空间节省时间】{skuId:1spuId:11skuTitle:华为xxprice:998attrs:[{尺寸:5寸},{CPU:高通945},{分辨率:全高清}]}
每个商品都需要保存一个属性值,缺点是浪费空间,但检索效率更高。
格式2:(sku与attr分开保存)【节省空间浪费时间】sku索引 {skuId:1spuId:11skuTitle:华为xxprice:998}attr索引 {spuId:11attrs:[{尺寸:5寸},{CPU:高通945},{分辨率:全高清}]}
将商品和索引分开,优势:数据不冗余,劣势:网络带宽大
例:查询小米,会搜到粮食、手机、电器,假设会查到10000个商品,包含4000个spu,因为要汇总所有商品的属性并列出,所以需要向后台传4000个spuId查询attr索引,spuId是Long类型,假设8byte,8byte*4000=32000byte=32Kb如果此时1000,000人检索,数据传输达到32GB
最终采用方式一。
商品文档格式(nested、doc_values、analyzer)
解析:"type": "text",// 全文检索字段"type": "keyword",// 非全文检索字段"index": false,// 不参与检索"doc_values": false// 不参与聚合统计(term)"analyzer": "ik_smart"// 使用ik分词器
PUT product{"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"}}}}}}
解释attr的 “type”: “nested”的nested值:
nested数据类型分析
未加nested数据类型的数组类型数据保存时会被es扁平化处理,导致查询的时候回出现错误,查看下例未加nested数据类型导致扁平化的案例。

案例:(存储一条id=1的数据)PUT my_index/_doc/1{"group": "fans","user": [{"first": "John","last": "Smith"},{"first": "Alice","last": "White"}]}es会处理成:{"group": "fans","user.first": ["Alice", "John"],"user.last": ["Smith", "White"]}查询my_index索引映射GET my_index/_mapping{"my_index" : {"mappings" : {"properties" : {"group" : {"type" : "text","fields" : {"keyword" : {"type" : "keyword","ignore_above" : 256}}},"user" : {"properties" : {"first" : {"type" : "text","fields" : {"keyword" : {"type" : "keyword","ignore_above" : 256}}},"last" : {"type" : "text","fields" : {"keyword" : {"type" : "keyword","ignore_above" : 256}}}}}}}}}检索:(查询Alice+Smith也可以查到,实际应该是没有这条数据的)GET my_index/_search{"query": {"bool": {"must": [{"match": {"user.first": "Alice"}},{"match": {"user.last": "Smith"}}]}}}
使用nested类型映射
上面案例的正确映射方式:(避免了es自动对数据扁平化处理)PUT my_index{"mappings": {"properties": {"user": {"type": "nested"}}}}然后再存储数据,按照以上查询方式查询不会再查询到值基本规格案例:"attrs": {"type": "nested","properties": {"attrId": {"type": "long"},"attrName": {"type": "keyword","index": false,"doc_values": false},"attrValue": {"type": "keyword"}}
商品上架接口
产品模块上架接口要调用es模块,所以在common模块新增TO对象
@Datapublic 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;// 存储为es数据时应该作为nested类型存储@Datapublic static class Attrs {private Long attrId;private String attrName;private String attrValue;}}
商品上架的接口:
@Overridepublic void up(Long spuId) {// 先查出spu对应的sku信息List<SkuInfoEntity> skuInfoEntityList = skuInfoService.getSkuBySpuId(spuId);// 获取所有的skuIdList<Long> skuIdList = skuInfoEntityList.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());// 设置属性List<ProductAttrValueEntity> attrValueEntityList = productAttrValueService.baseAttrlistforspu(spuId);List<Long> attrIds = attrValueEntityList.stream().map(attrValueEntity -> attrValueEntity.getAttrId()).collect(Collectors.toList());List<Long> attrSearchIds = attrService.selectSearchAttrIds(attrIds); // 查出满足检索条件的属性HashSet<Long> set = new HashSet<>(attrSearchIds);// 封装成搜索的属性List<SkuEsModel.Attrs> searchAttrs = attrValueEntityList.stream().filter(attrValueEntity -> set.contains(attrValueEntity.getAttrId())).map(attrValueEntity -> {SkuEsModel.Attrs attr = new SkuEsModel.Attrs();BeanUtils.copyProperties(attrValueEntity, attr);return attr;}).collect(Collectors.toList());// 远程调用查询库存信息Map<Long, Boolean> stockMap = new HashMap<>();try {R r = wareFeignService.getSkuHasStock(skuIdList);// TypeReference构造器失手保护,需要以内部类构造TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {};stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, skuHasStockTo -> skuHasStockTo.getHasStock()));} catch (Exception e) {log.error("远程调用出现异常:" + e);}Map<Long, Boolean> finalStockMap = stockMap;List<SkuEsModel> skuEsModelList = skuInfoEntityList.stream().map(skuInfo -> {SkuEsModel skuEsModel = new SkuEsModel();// 将一些属性进行对拷BeanUtils.copyProperties(skuInfo, skuEsModel);// 设置不对应的属性值skuEsModel.setSkuPrice(skuInfo.getPrice());skuEsModel.setSkuImg(skuInfo.getSkuDefaultImg());// 查询品牌信息进行封装BrandEntity brandById = brandService.getById(skuEsModel.getBrandId());skuEsModel.setBrandId(brandById.getBrandId());skuEsModel.setBrandName(brandById.getName());skuEsModel.setBrandImg(brandById.getLogo());// TODO 热度评分,先默认设置为0skuEsModel.setHotScore(0L);// 设置库存信息if (finalStockMap == null) {skuEsModel.setHasStock(true);} else {skuEsModel.setHasStock(finalStockMap.get(skuEsModel.getSkuId()));}// 查出分类信息进行封装CategoryEntity categoryEntityById = categoryService.getById(skuEsModel.getCatalogId());skuEsModel.setCatalogName(categoryEntityById.getName());// 设置检索属性值skuEsModel.setAttrs(searchAttrs);return skuEsModel;}).collect(Collectors.toList());// 将数据保存到ESR r = searchFeignService.productStatusUp(skuEsModelList);if (r.getCode() == 0) {// 远程调用成功,修改当前spu状态baseMapper.updateSpuUpStatus(spuId, SpuConstant.PublishStatusEnum.SPU_UP.getCode());} else {// 远程调用失败// TODO 重复调用问题(接口幂等性)}}
其中调用了ES模块的保存接口:
controller:
@RequestMapping("/search/save")@RestControllerpublic class ElasticSaveController {@Autowiredprivate ElasticSaveService elasticSaveService;/*** 保存商品到ES中** @return*/@PostMapping("/product")public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModelList) {boolean flag = false;try {flag = elasticSaveService.productStatusUp(skuEsModelList);} catch (IOException e) {// 抛出异常return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());}// false表示没有错误return flag ? R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg()) : R.ok();}}
service接口保存信息再ES中:
@Slf4j@Servicepublic class ElasticSaveServiceImpl implements ElasticSaveService {@AutowiredRestHighLevelClient restHighLevelClient;@Overridepublic boolean productStatusUp(List<SkuEsModel> skuEsModelList) throws IOException {// 给ES中保存数据BulkRequest bulkRequest = new BulkRequest();for (SkuEsModel model : skuEsModelList) {// 创建索引对应的保存请求IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);indexRequest.id(model.getSkuId().toString());indexRequest.source(JSONObject.toJSONString(model), XContentType.JSON);bulkRequest.add(indexRequest);}BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, GulimallSearchConfig.COMMON_OPTIONS);// TODO 如果批量出现错误boolean flag = bulkResponse.hasFailures(); // 响应体中检查是否出现错误List<String> idList = Arrays.stream(bulkResponse.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList());log.info("商品上架完成:{}" + idList);return flag;}}
注意这里再调用远程接口在ware中查询库存时,需要返回数据在R中,并解析出来,此时方法如下:

最终采用了fastJson提供的转换,自己封装的方式(TypeReference封装返回类型)
在R中添加getData()和setData()方法
// 转换数据,利用fastjson进行逆转public <T> T getData(TypeReference<T> typeReference) {Object data = get("data"); // 默认是mapString jsonString = JSON.toJSONString(data);T t = JSON.parseObject(jsonString, typeReference);return t;}// feign返回对象时有用public R setData(Object data) {put("data", data);return this;}
在远程调用后进行转换
// 远程调用查询库存信息Map<Long, Boolean> stockMap = new HashMap<>();try {R r = wareFeignService.getSkuHasStock(skuIdList);// TypeReference构造器失手保护,需要以内部类构造TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {};stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, skuHasStockTo -> skuHasStockTo.getHasStock()));} catch (Exception e) {log.error("远程调用出现异常:" + e);}
其他细节可以通过SpuInfoController的这个接口查看
/*** 商品上架*/@PostMapping("/{spuId}/up")public R up(@PathVariable("spuId") Long spuId) {spuInfoService.up(spuId);return R.ok();}
