商品上架

接口功能详情

  • 修改spu状态为已上架
  • 保存sku信息到es中,以skuId作为文档ID

    1. sku默认图片
    2. sku标题
    3. sku价格
    4. 销量
    5. spu允许被检索的基本规格(尺寸、CPU、分辨率)

商品json文档格式分析

方式一:

  1. 格式1:(商品索引保存sku+attr信息)【浪费空间节省时间】
  2. {
  3. skuId:1
  4. spuId:11
  5. skuTitle:华为xx
  6. price:998
  7. attrs:[
  8. {尺寸:5寸},
  9. {CPU:高通945},
  10. {分辨率:全高清}
  11. ]
  12. }

每个商品都需要保存一个属性值,缺点是浪费空间,但检索效率更高。

  1. 格式2:(skuattr分开保存)【节省空间浪费时间】
  2. sku索引 {
  3. skuId:1
  4. spuId:11
  5. skuTitle:华为xx
  6. price:998
  7. }
  8. attr索引 {
  9. spuId:11
  10. attrs:[
  11. {尺寸:5寸},
  12. {CPU:高通945},
  13. {分辨率:全高清}
  14. ]
  15. }

将商品和索引分开,优势:数据不冗余,劣势:网络带宽大
例:查询小米,会搜到粮食、手机、电器,假设会查到10000个商品,包含4000个spu,因为要汇总所有商品的属性并列出,所以需要向后台传4000个spuId查询attr索引,spuId是Long类型,假设8byte,8byte*4000=32000byte=32Kb如果此时1000,000人检索,数据传输达到32GB

最终采用方式一。

商品文档格式(nested、doc_values、analyzer)

  1. 解析:
  2. "type": "text",// 全文检索字段
  3. "type": "keyword",// 非全文检索字段
  4. "index": false,// 不参与检索
  5. "doc_values": false// 不参与聚合统计(term)
  6. "analyzer": "ik_smart"// 使用ik分词器
  1. PUT product
  2. {
  3. "mappings": {
  4. "properties": {
  5. "skuId": {
  6. "type": "long"
  7. },
  8. "spuId": {
  9. "type": "keyword" # 关键词检索
  10. },
  11. "skuTitle": {
  12. "type": "text",
  13. "analyzer": "ik_smart"
  14. },
  15. "skuPrice": {
  16. "type": "keyword"
  17. },
  18. "skuImg": {
  19. "type": "keyword",
  20. "index": false,
  21. "doc_values": false
  22. },
  23. "saleCount": {
  24. "type": "long"
  25. },
  26. "hasStock": {
  27. "type": "boolean"
  28. },
  29. "hotScore": {
  30. "type": "long"
  31. },
  32. "brandId": {
  33. "type": "long"
  34. },
  35. "catalogId": {
  36. "type": "long"
  37. },
  38. "brandName": {
  39. "type": "keyword",
  40. "index": false,
  41. "doc_values": false
  42. },
  43. "brandImg": {
  44. "type": "keyword",
  45. "index": false,
  46. "doc_values": false
  47. },
  48. "catalogName": {
  49. "type": "keyword",
  50. "index": false,
  51. "doc_values": false
  52. },
  53. "attrs": {
  54. "type": "nested",
  55. "properties": {
  56. "attrId": {
  57. "type": "long"
  58. },
  59. "attrName": {
  60. "type": "keyword",
  61. "index": false,
  62. "doc_values": false
  63. },
  64. "attrValue": {
  65. "type": "keyword"
  66. }
  67. }
  68. }
  69. }
  70. }
  71. }

解释attr的 “type”: “nested”的nested值:

nested数据类型分析

未加nested数据类型的数组类型数据保存时会被es扁平化处理,导致查询的时候回出现错误,查看下例未加nested数据类型导致扁平化的案例。

商品上架 - 图1

  1. 案例:(存储一条id=1的数据)
  2. PUT my_index/_doc/1
  3. {
  4. "group": "fans",
  5. "user": [
  6. {
  7. "first": "John",
  8. "last": "Smith"
  9. },{
  10. "first": "Alice",
  11. "last": "White"
  12. }
  13. ]
  14. }
  15. es会处理成:
  16. {
  17. "group": "fans",
  18. "user.first": ["Alice", "John"],
  19. "user.last": ["Smith", "White"]
  20. }
  21. 查询my_index索引映射
  22. GET my_index/_mapping
  23. {
  24. "my_index" : {
  25. "mappings" : {
  26. "properties" : {
  27. "group" : {
  28. "type" : "text",
  29. "fields" : {
  30. "keyword" : {
  31. "type" : "keyword",
  32. "ignore_above" : 256
  33. }
  34. }
  35. },
  36. "user" : {
  37. "properties" : {
  38. "first" : {
  39. "type" : "text",
  40. "fields" : {
  41. "keyword" : {
  42. "type" : "keyword",
  43. "ignore_above" : 256
  44. }
  45. }
  46. },
  47. "last" : {
  48. "type" : "text",
  49. "fields" : {
  50. "keyword" : {
  51. "type" : "keyword",
  52. "ignore_above" : 256
  53. }
  54. }
  55. }
  56. }
  57. }
  58. }
  59. }
  60. }
  61. }
  62. 检索:(查询Alice+Smith也可以查到,实际应该是没有这条数据的)
  63. GET my_index/_search
  64. {
  65. "query": {
  66. "bool": {
  67. "must": [
  68. {"match": {"user.first": "Alice"}},
  69. {"match": {"user.last": "Smith"}}
  70. ]
  71. }
  72. }
  73. }

使用nested类型映射

  1. 上面案例的正确映射方式:(避免了es自动对数据扁平化处理)
  2. PUT my_index
  3. {
  4. "mappings": {
  5. "properties": {
  6. "user": {
  7. "type": "nested"
  8. }
  9. }
  10. }
  11. }
  12. 然后再存储数据,按照以上查询方式查询不会再查询到值
  13. 基本规格案例:
  14. "attrs": {
  15. "type": "nested",
  16. "properties": {
  17. "attrId": {
  18. "type": "long"
  19. },
  20. "attrName": {
  21. "type": "keyword",
  22. "index": false,
  23. "doc_values": false
  24. },
  25. "attrValue": {
  26. "type": "keyword"
  27. }
  28. }

商品上架接口

产品模块上架接口要调用es模块,所以在common模块新增TO对象

  1. @Data
  2. public class SkuEsModel {
  3. private Long skuId;
  4. private Long spuId;
  5. private String skuTitle;
  6. private BigDecimal skuPrice;
  7. private String skuImg;
  8. private Long saleCount;
  9. private Boolean hasStock;
  10. private Long hotScore;
  11. private Long brandId;
  12. private Long catalogId;
  13. private String brandName;
  14. private String brandImg;
  15. private String catalogName;
  16. private List<Attrs> attrs;
  17. // 存储为es数据时应该作为nested类型存储
  18. @Data
  19. public static class Attrs {
  20. private Long attrId;
  21. private String attrName;
  22. private String attrValue;
  23. }
  24. }

商品上架的接口:

  1. @Override
  2. public void up(Long spuId) {
  3. // 先查出spu对应的sku信息
  4. List<SkuInfoEntity> skuInfoEntityList = skuInfoService.getSkuBySpuId(spuId);
  5. // 获取所有的skuId
  6. List<Long> skuIdList = skuInfoEntityList.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
  7. // 设置属性
  8. List<ProductAttrValueEntity> attrValueEntityList = productAttrValueService.baseAttrlistforspu(spuId);
  9. List<Long> attrIds = attrValueEntityList.stream().map(attrValueEntity -> attrValueEntity.getAttrId()).collect(Collectors.toList());
  10. List<Long> attrSearchIds = attrService.selectSearchAttrIds(attrIds); // 查出满足检索条件的属性
  11. HashSet<Long> set = new HashSet<>(attrSearchIds);
  12. // 封装成搜索的属性
  13. List<SkuEsModel.Attrs> searchAttrs = attrValueEntityList.stream().filter(attrValueEntity -> set.contains(attrValueEntity.getAttrId())).map(attrValueEntity -> {
  14. SkuEsModel.Attrs attr = new SkuEsModel.Attrs();
  15. BeanUtils.copyProperties(attrValueEntity, attr);
  16. return attr;
  17. }).collect(Collectors.toList());
  18. // 远程调用查询库存信息
  19. Map<Long, Boolean> stockMap = new HashMap<>();
  20. try {
  21. R r = wareFeignService.getSkuHasStock(skuIdList);
  22. // TypeReference构造器失手保护,需要以内部类构造
  23. TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {
  24. };
  25. stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, skuHasStockTo -> skuHasStockTo.getHasStock()));
  26. } catch (Exception e) {
  27. log.error("远程调用出现异常:" + e);
  28. }
  29. Map<Long, Boolean> finalStockMap = stockMap;
  30. List<SkuEsModel> skuEsModelList = skuInfoEntityList.stream().map(skuInfo -> {
  31. SkuEsModel skuEsModel = new SkuEsModel();
  32. // 将一些属性进行对拷
  33. BeanUtils.copyProperties(skuInfo, skuEsModel);
  34. // 设置不对应的属性值
  35. skuEsModel.setSkuPrice(skuInfo.getPrice());
  36. skuEsModel.setSkuImg(skuInfo.getSkuDefaultImg());
  37. // 查询品牌信息进行封装
  38. BrandEntity brandById = brandService.getById(skuEsModel.getBrandId());
  39. skuEsModel.setBrandId(brandById.getBrandId());
  40. skuEsModel.setBrandName(brandById.getName());
  41. skuEsModel.setBrandImg(brandById.getLogo());
  42. // TODO 热度评分,先默认设置为0
  43. skuEsModel.setHotScore(0L);
  44. // 设置库存信息
  45. if (finalStockMap == null) {
  46. skuEsModel.setHasStock(true);
  47. } else {
  48. skuEsModel.setHasStock(finalStockMap.get(skuEsModel.getSkuId()));
  49. }
  50. // 查出分类信息进行封装
  51. CategoryEntity categoryEntityById = categoryService.getById(skuEsModel.getCatalogId());
  52. skuEsModel.setCatalogName(categoryEntityById.getName());
  53. // 设置检索属性值
  54. skuEsModel.setAttrs(searchAttrs);
  55. return skuEsModel;
  56. }).collect(Collectors.toList());
  57. // 将数据保存到ES
  58. R r = searchFeignService.productStatusUp(skuEsModelList);
  59. if (r.getCode() == 0) {
  60. // 远程调用成功,修改当前spu状态
  61. baseMapper.updateSpuUpStatus(spuId, SpuConstant.PublishStatusEnum.SPU_UP.getCode());
  62. } else {
  63. // 远程调用失败
  64. // TODO 重复调用问题(接口幂等性)
  65. }
  66. }

其中调用了ES模块的保存接口:

controller:

  1. @RequestMapping("/search/save")
  2. @RestController
  3. public class ElasticSaveController {
  4. @Autowired
  5. private ElasticSaveService elasticSaveService;
  6. /**
  7. * 保存商品到ES中
  8. *
  9. * @return
  10. */
  11. @PostMapping("/product")
  12. public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModelList) {
  13. boolean flag = false;
  14. try {
  15. flag = elasticSaveService.productStatusUp(skuEsModelList);
  16. } catch (IOException e) {
  17. // 抛出异常
  18. return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
  19. }
  20. // false表示没有错误
  21. return flag ? R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg()) : R.ok();
  22. }
  23. }

service接口保存信息再ES中:

  1. @Slf4j
  2. @Service
  3. public class ElasticSaveServiceImpl implements ElasticSaveService {
  4. @Autowired
  5. RestHighLevelClient restHighLevelClient;
  6. @Override
  7. public boolean productStatusUp(List<SkuEsModel> skuEsModelList) throws IOException {
  8. // 给ES中保存数据
  9. BulkRequest bulkRequest = new BulkRequest();
  10. for (SkuEsModel model : skuEsModelList) {
  11. // 创建索引对应的保存请求
  12. IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
  13. indexRequest.id(model.getSkuId().toString());
  14. indexRequest.source(JSONObject.toJSONString(model), XContentType.JSON);
  15. bulkRequest.add(indexRequest);
  16. }
  17. BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, GulimallSearchConfig.COMMON_OPTIONS);
  18. // TODO 如果批量出现错误
  19. boolean flag = bulkResponse.hasFailures(); // 响应体中检查是否出现错误
  20. List<String> idList = Arrays.stream(bulkResponse.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList());
  21. log.info("商品上架完成:{}" + idList);
  22. return flag;
  23. }
  24. }

注意这里再调用远程接口在ware中查询库存时,需要返回数据在R中,并解析出来,此时方法如下:

商品上架 - 图2

最终采用了fastJson提供的转换,自己封装的方式(TypeReference封装返回类型)

在R中添加getData()和setData()方法

  1. // 转换数据,利用fastjson进行逆转
  2. public <T> T getData(TypeReference<T> typeReference) {
  3. Object data = get("data"); // 默认是map
  4. String jsonString = JSON.toJSONString(data);
  5. T t = JSON.parseObject(jsonString, typeReference);
  6. return t;
  7. }
  8. // feign返回对象时有用
  9. public R setData(Object data) {
  10. put("data", data);
  11. return this;
  12. }

在远程调用后进行转换

  1. // 远程调用查询库存信息
  2. Map<Long, Boolean> stockMap = new HashMap<>();
  3. try {
  4. R r = wareFeignService.getSkuHasStock(skuIdList);
  5. // TypeReference构造器失手保护,需要以内部类构造
  6. TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {
  7. };
  8. stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, skuHasStockTo -> skuHasStockTo.getHasStock()));
  9. } catch (Exception e) {
  10. log.error("远程调用出现异常:" + e);
  11. }

其他细节可以通过SpuInfoController的这个接口查看

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