商品上架
接口功能详情
- 修改spu状态为已上架
保存sku信息到es中,以skuId作为文档ID
sku默认图片
sku标题
sku价格
销量
spu允许被检索的基本规格(尺寸、CPU、分辨率)
商品json文档格式分析
方式一:
格式1:(商品索引保存sku+attr信息)【浪费空间节省时间】
{
skuId:1
spuId:11
skuTitle:华为xx
price:998
attrs:[
{尺寸:5寸},
{CPU:高通945},
{分辨率:全高清}
]
}
每个商品都需要保存一个属性值,缺点是浪费空间,但检索效率更高。
格式2:(sku与attr分开保存)【节省空间浪费时间】
sku索引 {
skuId:1
spuId:11
skuTitle:华为xx
price:998
}
attr索引 {
spuId:11
attrs:[
{尺寸: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对象
@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;
// 存储为es数据时应该作为nested类型存储
@Data
public static class Attrs {
private Long attrId;
private String attrName;
private String attrValue;
}
}
商品上架的接口:
@Override
public void up(Long spuId) {
// 先查出spu对应的sku信息
List<SkuInfoEntity> skuInfoEntityList = skuInfoService.getSkuBySpuId(spuId);
// 获取所有的skuId
List<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 热度评分,先默认设置为0
skuEsModel.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());
// 将数据保存到ES
R r = searchFeignService.productStatusUp(skuEsModelList);
if (r.getCode() == 0) {
// 远程调用成功,修改当前spu状态
baseMapper.updateSpuUpStatus(spuId, SpuConstant.PublishStatusEnum.SPU_UP.getCode());
} else {
// 远程调用失败
// TODO 重复调用问题(接口幂等性)
}
}
其中调用了ES模块的保存接口:
controller:
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
@Autowired
private 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
@Service
public class ElasticSaveServiceImpl implements ElasticSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public 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"); // 默认是map
String 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();
}