上架的商品才可以在网站展示
上架的商品需要可以检索
分析:商品上架在 es 中是存 sku 还是 spu ?
检索商品的名字,如“手机”,对应的 spu 有很多,我们要分析出这些 spu 的所有关联属性,再做一次查询,就必须将所有 spu_id 都发出去。假设有 1 万个数据,数据传输一次就10000*4=4MB;并发情况下假设 1000 检索请求,那就是 4GB 的数据,,传输阻塞时间会很长,业务更加无法继续。
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数据库范式
PUT product
{
"mappings":{
"properties":{
"skuId":{
"type":"long"
},
"spuId":{
"type":"keyword"
},
"skuTitle":{
"type":"text",
"analyzer": "ik_smart"
},
"skuPrice":{
"type":"keyword"
},
"skuImg":{
"type":"text",
"analyzer": "ik_smart"
},
"saleCount":{
"type":"long"
},
"hasStock":{
"type":"boolean"
},
"hotScore":{
"type":"long"
},
"brandId":{
"type":"long"
},
"catelogId":{
"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 来让字段不能被搜索但可以用于排序、聚合以及脚本操作
nested :
表示数组数据是嵌入式的,默认不是嵌入式,ES默认数组数据是扁平化处理
上架是将后台的商品放在 es 中可以提供检索和查询功能
商品上架步骤:

因为数据需要存入 ES,在 common 模块里面新建实体类
SkuEsModel
//上架商品信息
@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 catelogId;
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;
}
}
SpuInfoController
//商品上架
@RequestMapping("/{spuId}/up")
public R up(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
SpuInfoServiceImpl
//商品上架:查出当前 spuid 对应的所有信息封装为 SkuEsModel,发送给 es保存
@Override
public void up(Long spuId) {
//1、查询sku信息(一个spu对应多个sku)
List<SkuInfoEntity> skus = skuInfoService.getSkuBySpuId(spuId);
//2、sku信息封装为 SkuEsModel
//skuPrice skuImg hasStock hotScore brandName brandImg catalogName attrs
//TODO 2.1、发送远程调用,库存系统查询是否有库存(hasStock)
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//将list转换为map
Map<Long, Boolean> skuHasStock = null;
try {
R r = wareFeignService.getSkuHasStock(skuIdList);
TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {};
skuHasStock = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
} catch (Exception e) {
log.error("库存服务异常:原因:{}",e);
e.printStackTrace();
}
//TODO 2.4、查询当前sku的所有可以用来被检索的基本规格属性(attrs)
//基本属性是跟着spu走,销售属性是跟着sku,所以对于同一个spu下的sku,是同一类基本属性
//属性名、值是在pms_product_attr_value表,是否被检索是在pms_attr表的search_type字段
List<ProductAttrValueEntity> attrValues = productAttrValueService.list(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
//筛选出可检索的属性
List<Long> attrIds = attrValues.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
//再次筛选,过滤掉不包含在 searchAttrIds集合中的元素
HashSet<Long> searchAttrIdsHashSet = new HashSet<>(searchAttrIds);
//封装可被检索的规格属性
List<SkuEsModel.Attrs> attrs = attrValues.stream().filter(item -> {
//过滤掉不包含在 searchAttrIds集合中的元素
return searchAttrIds.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attr = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item,attr);
return attr;
}).collect(Collectors.toList());
Map<Long, Boolean> finalSkuHasStock = skuHasStock;
List<SkuEsModel> upProducts = skus.stream().map(sku -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku,skuEsModel);
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
//2.1 设置库存信息(防止此处多次调用远程服务,所以在循环外部查询)
skuEsModel.setHasStock(finalSkuHasStock != null && finalSkuHasStock.get(sku.getSkuId()));
//TODO 2.2、热度评分默认为 0
skuEsModel.setHotScore(0L);
//TODO 2.3、查询品牌和商品分类的信息
BrandEntity brand = brandService.getById(sku.getBrandId());
skuEsModel.setBrandImg(brand.getName());
skuEsModel.setBrandImg(brand.getLogo());
CategoryEntity category = categoryService.getById(sku.getCatalogId());
skuEsModel.setCatalogName(category.getName());
//2.4 设置属性
skuEsModel.setAttrs(attrs);
return skuEsModel;
}).collect(Collectors.toList());
//TODO 3、将数据发送给 es保存,直接发送给 search服务
R r = searchFeignService.productStatusUp(upProducts);
if (r.getCode() == 0) {
// 远程调用成功
// TODO 3.1、修改当前 spu 的状态
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
// 远程调用失败
//TODO 4、重复调用?接口冥等性、重试机制
/**
* feign源码分析:
* 1、构造请求数据,将对象转成json
* RequestTemplate template = buildTemplateFromArgs.create(argv);
* 2、发送请求进行执行(执行成功进行解码)
* executeAndDecode(template);
* 3、执行请求会有重试机制
* while (true) {
* try {
* return executeAndDecode(template);
* } catch (RetryableException e) {
* try {
* retryer.continueOrPropagate(e);
* } catch (RetryableException th) {
* throw cause;
* }
* continute
*/
}
}
发送远程调用,库存系统查询是否有库存
common 模块里面新建实体类用于传输数据
SkuHasStockTo
@Data
public class SkuHasStockTo {
private Long skuId;
private Boolean hasStock;
}
gulimall-ware 服务里的接口
WareSkuController
/**
* 查询指定sku是否有库存
*/
@PostMapping("/hasStock")
public R getSkuHasStock(@RequestBody List<Long> skuIds){
List<SkuHasStockTo> tos = wareSkuService.getSkuStock(skuIds);
R r = R.ok().setData(tos);
return r;
}
为了方便传递数据,修改了 R 的代码,添加了泛型方法,存取数据
public class R extends HashMap<String, Object> {
//利用fastjson进行逆转,泛型方法:调用时指定要拿到的数据类型
//注意是 com.alibaba.fastjson.TypeReference
public <T> T getData(TypeReference<T> typeReference){
Object data = get("data");
//此处不能直接将data强转为指定要拿到的数据类型
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s,typeReference);
return t;
}
public R setData(Object object){
put("data",object);
return this;
}
...
}
为什么上面不能直接将data强转为指定要拿到的数据类型?
注意:
因为 map 中的 value 为一个对象,在 springmvc 中取出这个对象时会将这个对象默认转为 map
SpringMVC 对于 Object 转 json 的时候,会变成 key-value 的形式
示例:
当方法返回 HashMap——value为List——List泛型为自定义类对象
以下是正常情况下方法执行结果
以下是远程调用返回结果
会将 map 值value 里面的集合 list 里面的对象转化为 map键值对
示例:如果直接返回 List 集合也会有上面的问题
但是如果直接返回自定义类对象,不会被转化为键值对
gulimall-product 服务里接收数据
//TODO 2.1、发送远程调用,库存系统查询是否有库存(hasStock)
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//将list转换为map
Map<Long, Boolean> skuHasStock = null;
try {
R r = wareFeignService.getSkuHasStock(skuIdList);
TypeReference<List<SkuHasStockTo>> typeReference = new TypeReference<List<SkuHasStockTo>>() {};
skuHasStock = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
} catch (Exception e) {
log.error("库存服务异常:原因:{}",e);
e.printStackTrace();
}
注意:以下写法有误
gulimall-product 服务里的商品上架 SpuInfoServiceImpl
R<List<SkuHasStockTo>> r = wareFeignService.getSkuHasStock(skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList())); //将list转换为map Map<Long, Boolean> skuHasStock = r.getData().stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
- 1
- 2
- 3
gulimall-ware 服务里的接口 WareSkuController
R<List<SkuHasStockTo>> getSkuHasStock(@RequestBody List<Long> skuIds){ List<SkuHasStockTo> tos = wareSkuService.getSkuStock(skuIds); R r = R.ok(); r.setData(tos); return r; }
- 1
- 2
- 3
- 4
- 5
- 6
此处为了方便传递数据,修改了
R的代码,添加了泛型属性public class R<T> extends HashMap<String, Object> { private T data; public T getData() {return data;} public void setData(T data) {this.data = data;} ... }
- 1
- 2
- 3
- 4
- 5
- 6
错误原因:
因为Jackson对于HashMap类型会有特殊的处理方式,具体来说就是会对类进行向上转型为Map,导致子类的私有属性消失
就会导致在 gulimall-product 服务里 r.getData() 拿不到属性值数据
所以将 R 修改为泛型类—— pass
WareSkuService
//查询指定skuid列表是否由库存
List<SkuHasStockTo> getSkuStock(List<Long> skuIds);
@Override
public List<SkuHasStockTo> getSkuStock(List<Long> skuIds) {
return skuIds.stream().map(id -> {
SkuHasStockTo to = new SkuHasStockTo();
//SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` where sku_id = ?
//注意这里接收 count 的类型是 Long,因为查询出来的结果可能是null,需要用包装类
Long count = baseMapper.getSkuStockById(id);
to.setSkuId(id);
to.setHasStock(count != null && count > 0);
return to;
}).collect(Collectors.toList());
}
<select id="getSkuStockById" resultType="java.lang.Long">
SELECT SUM(stock-stock_locked) FROM `wms_ware_sku`
where sku_id = #{id}
select>
gulimall-product 服务里的接口
WareFeignService
@FeignClient("gulimall-ware")
public interface WareFeignService {
//查询指定sku是否有库存
@PostMapping("/ware/waresku/hasStock")
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
AttrService
给定属性id列表,从中筛选出可检索属性列表
//给定属性id列表,从中筛选出可检索属性列表
List<Long> selectSearchAttrs(List<Long> attrIds);
@Override
public List<Long> selectSearchAttrs(List<Long> attrIds) {
return attrDao.selectSearchAttrs(attrIds);
}
<select id="selectSearchAttrs" resultType="java.lang.Long">
select attr_id from pms_attr where attr_id in
<foreach collection="attrIds" item="id" separator="," open="(" close=")">
#{id}
foreach>
and search_type = 1
select>
gulimall-search 模块中编写保存数据接口
ElasticSaveController
@Slf4j
@RequestMapping("search/save")
@RestController
public class ElasticSaveController {
@Autowired
ProductSaveService productSaveService;
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> list) throws IOException {
boolean b = false;
try {
b = productSaveService.productStatusUp(list);
} catch (IOException e) {
log.error("ElasticSaveController商品上架错误",e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if(b) return R.ok();
else return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
BizCodeEnume :common 模块里面的异常常量类
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败"),
PRODUCT_UP_EXCEPTION(11000,"商品上架异常");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
ProductSaveService
public interface ProductSaveService{
/**
* @param list
* @return false 批量保存错误;true 批量保存成功
* @throws IOException
*/
Boolean productStatusUp(List<SkuEsModel> list) throws IOException;
}
ProductSaveServiceImpl
@Service("productSaveServiceImpl")
public class ProductSaveServiceImpl implements ProductSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public Boolean productStatusUp(List<SkuEsModel> list) throws IOException {
//先要在es中建立索引,再在es中保存数据;因为此处数据较多,使用批量保存
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : list) {
//指定存储的索引
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString()); //指定唯一id
String s = JSON.toJSONString(skuEsModel);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
//参数:BulkRequest bulkRequest, RequestOptions options
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
//TODO 如果批量错误
//false 批量保存错误;true 批量保存成功
boolean b = bulk.hasFailures();
return !b;
}
}
EsConstant 常量类
public class EsConstant {
//sku数据在es中的索引
public static final String PRODUCT_INDEX = "product";
}