商品核心逻辑说明
一、商品sku
1、表结构设计
商品表(es_goods)
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
goods_id | bigint(20) | 商品ID | 是 |
goods_name | varchar(255) | 商品名称 | 是 |
sn | varchar(255) | 商品编号 | 是 |
brand_id | bigint(20) | 品牌id | 是 |
category_id | bigint(20) | 分类id | 是 |
goods_type | varchar(50) | 商品类型 | 是 |
weight | decimal(10) | 重量 | 否 |
market_enable | int(1) | 上架状态 | 是 |
intro | longtext | 详情 | 否 |
price | decimal(20) | 商品价格 | 否 |
cost | decimal(20) | 成本价格 | 否 |
mktprice | decimal(20) | 市场价格 | 否 |
have_spec | int(1) | 是否有规格 | 否 |
create_time | bigint(20) | 创建时间 | 否 |
last_modify | bigint(20) | 最后修改时间 | 否 |
view_count | int(10) | 浏览数量 | 否 |
buy_count | int(10) | 购买数量 | 否 |
disabled | int(10) | 是否被删除 | 是 |
quantity | int(10) | 实际库存 | 否 |
enable_quantity | int(10) | 可用库存 | 否 |
point | int(10) | 积分 | 否 |
page_title | varchar(255) | seo标题 | 否 |
meta_keywords | varchar(255) | seo关键字 | 否 |
meta_description | varchar(255) | seo描述 | 否 |
grade | decimal(20) | 商品好评率 | 否 |
thumbnail | varchar(255) | 缩略图路径 | 否 |
big | varchar(255) | 大图路径 | 否 |
small | varchar(255) | 小图路径 | 否 |
original | varchar(255) | 原图路径 | 否 |
seller_id | bigint(20) | 卖家id | 否 |
shop_cat_id | bigint(20) | 店铺分类id | 否 |
comment_num | int(10) | 评论数量 | 否 |
template_id | bigint(20) | 运费模板id | 否 |
goods_transfee_charge | int(1) | 谁承担运费 | 否 |
seller_name | varchar(255) | 卖家名字 | 否 |
is_auth | int(1) | 审核状态 | 否 |
auth_message | varchar(255) | 审核信息 | 否 |
self_operated | int(1) | 是否自营 | 否 |
under_message | varchar(500) | 下架原因 | 否 |
priority | int(1) | 搜索优先级 | 否 |
mobile_intro | longtext | 商品移动端详情 | 否 |
goods_video | varchar(255) | 商品视频 | 否 |
商品sku表(es_goods_sku)
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
sku_id | bigint(20) | 主键ID | 是 |
goods_id | bigint(20) | 商品ID | 是 |
goods_name | varchar(255) | 商品名称 | 否 |
sn | varchar(50) | 商品编号 | 是 |
quantity | int(10) | 库存 | 否 |
enable_quantity | int(10) | 可用库存 | 否 |
price | decimal(20) | 商品价格 | 否 |
specs | longtext | 规格信息json | 否 |
cost | decimal(20) | 成本价格 | 否 |
weight | decimal(20) | 重量 | 否 |
seller_id | bigint(20) | 卖家id | 否 |
seller_name | varchar(255) | 卖家名称 | 否 |
category_id | bigint(20) | 分类id | 否 |
thumbnail | varchar(255) | 缩略图 | 否 |
hash_code | int(10) | 哈希码 | 否 |
2、商品结构
二、商品规格值
1、表结构设计
规格项(es_specification) 所有规格名称和描述的表
列名 | 类型 | 描述 | 是否索引 |
---|---|---|---|
spec_id | bigint(20) | 主键 | 是 |
spec_name | varchar(100) | 规格项名称 | 是 |
disabled | int(1) | 是否被删除 0 删除 1 没有删除 | 否 |
spec_memo | varchar(50) | 规格描述 | 否 |
seller_id | bigint(20) | 所属卖家 | 是 |
规格值(es_spec_values) 规格值的表
列名 | 类型 | 描述 | 是否索引 |
---|---|---|---|
spec_value_id | bigint(20) | 主键 | 否 |
spec_id | bigint(20) | 规格项id | 否 |
spec_value | varchar(100) | 规格值名字 | 否 |
seller_id | bigint(20) | 所属卖家 | 否 |
spec_name | varchar(100) | 规格名字 | 否 |
2、规格值结构
当前商城平台商品规格结构如下:
3、核心代码展示
商家添加规格值实现类(SpecValuesManagerImpl)—以添加规格为例:
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public List<SpecValuesDO> saveSpecValue(Long specId, String[] valueList) {
//查询规格是否存在
SpecificationDO spec = specificationManager.getModel(specId);
if (spec == null) {
throw new ServiceException(GoodsErrorCode.E306.code(), "所属规格不存在");
}
this.specValuesMapper.delete(new QueryWrapper<SpecValuesDO>()
.eq("spec_id",specId)
.eq("seller_id",0));
List<SpecValuesDO> res = new ArrayList<>();
for (String value : valueList) {
if (value.length() > 50) {
throw new ServiceException(GoodsErrorCode.E305.code(), "规格值为1到50个字符之间");
}
SpecValuesDO specValue = new SpecValuesDO(specId, value, 0L);
specValue.setSpecName(spec.getSpecName());
this.specValuesMapper.insert(specValue);
res.add(specValue);
}
return res;
}
三、 商品参数组
1、表结构设计
商品关联参数表(es_goods_params) 关联了商品表(es_goods),商品参数组表(es_parameter)
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
id | bigint(20) | 主键ID | 是 |
goods_id | bigint(20) | 商品id | 是 |
param_id | bigint(20) | 参数id | 是 |
param_name | varchar(100) | 参数名字 | 否 |
param_value | varchar(255) | 参数值 | 否 |
参数表(es_parameters)
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
param_id | int(10) | 主键 | 否 |
param_name | varchar(100) | 参数名称 | 否 |
param_type | int(1) | 参数类型1 输入项 2 选择项 | 否 |
options | varchar(255) | 选择值,当参数类型是选择项2时,必填,逗号分隔 | 否 |
is_index | int(1) | 是否可索引,0 不显示 1 显示 | 否 |
required | int(1) | 是否必填是 1 否 0 | 否 |
group_id | int(10) | 参数分组id | 否 |
category_id | int(10) | 分类id | 否 |
sort | int(2) | 排序 | 否 |
参数组(es_parameter_group)
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
group_id | int(10) | 主键 | 否 |
group_name | varchar(100) | 参数组名称 | 否 |
category_id | int(10) | 关联分类id | 否 |
sort | int(2) | 排序 | 否 |
2、商品参数结构
四、 商品缓存
1、设计说明
商品为高频率读取数据,频繁读取数据库,会导致数据库压力过大,所以使用缓存机制,常用数据存入redis 中。
2、数据结构设计
商品缓存类:CacheGoods
/** 缓存商品对象 */
@ApiModel
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class CacheGoods implements Serializable {
private static final long serialVersionUID = -3642358108471082387L;
@ApiModelProperty(name = "goods_id", value = "商品id")
@Column(name = "goods_id")
private Long goodsId;
@ApiModelProperty(name = "category_id", value = "分类id")
private Long categoryId;
@ApiModelProperty(name = "goods_name", value = "商品名称")
@Column(name = "goods_name")
private String goodsName;
@ApiModelProperty(name = "sn", value = "商品编号")
@Column(name = "sn")
private String sn;
@ApiModelProperty(name = "price", value = "商品价格")
@Column(name = "price")
private Double price;
@ApiModelProperty(name = "weight", value = "重量")
@Column(name = "weight")
private Double weight;
@ApiModelProperty(name = "intro", value = "详情")
private String intro;
@ApiModelProperty(name = "goods_transfee_charge", value = "谁承担运费0:买家承担,1:卖家承担")
@Column(name = "goods_transfee_charge")
private Integer goodsTransfeeCharge;
@ApiModelProperty(name = "template_id", value = "运费模板id,不需要运费模板时值是0")
@Column(name = "template_id")
private Long templateId;
@ApiModelProperty(name = "market_enable", value = "是否上架,1上架 0下架")
@Column(name = "market_enable")
private Integer marketEnable;
@ApiModelProperty(name = "disabled", value = "是否放入回收站 0 删除 1未删除")
@Column(name = "disabled")
private Integer disabled;
@ApiModelProperty(name = "is_auth", value = "是否审核通过 0 未审核 1 通过 2 不通过")
@Column(name = "is_auth")
private Integer isAuth;
@ApiModelProperty(value = "可用库存")
@Column(name = "enable_quantity")
private Integer enableQuantity;
@ApiModelProperty(name = "quantity", value = "库存")
private Integer quantity;
@ApiModelProperty(name = "seller_id", value = "卖家")
private Long sellerId;
@ApiModelProperty(name = "seller_name", value = "卖家名称")
private String sellerName;
@ApiModelProperty(name = "sku_list", value = "sku列表")
private List<GoodsSkuVO> skuList;
@ApiModelProperty(name = "thumbnail", value = "商品缩略图")
private String thumbnail;
@ApiModelProperty(name = "last_modify", value = "商品最后修改时间")
private Long lastModify;
@ApiModelProperty(name = "comment_num", value = "评论数量")
private Integer commentNum;
@ApiModelProperty(name = "grade", value = "商品好评率")
private Double grade;
@ApiModelProperty(name = "mobile_intro", value = "商品移动端详情")
private String mobileIntro;
@ApiModelProperty(name = "goods_video", value = "商品视频")
private String goodsVideo;
.....
SKU缓存类:GoodsSkuVO
@ApiModel
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GoodsSkuVO extends GoodsSkuDO {
private static final long serialVersionUID = -666090547834195127L;
@ApiModelProperty(name = "spec_list", value = "规格列表", required = false)
private List<SpecValueVO> specList;
@ApiModelProperty(name = "goods_transfee_charge", value = "谁承担运费0:买家承担,1:卖家承担", hidden = true)
private Integer goodsTransfeeCharge;
@ApiModelProperty(value = "是否被删除 0 删除 1 未删除", hidden = true)
private Integer disabled;
@ApiModelProperty(value = "上架状态 0下架 1上架", hidden = true)
private Integer marketEnable;
@ApiModelProperty(name = "goods_type", value = "商品类型NORMAL普通POINT积分")
private String goodsType;
@ApiModelProperty(value = "最后修改时间", hidden = true)
private Long lastModify;
@ApiModelProperty(value = "运费脚本", hidden = true)
private List<String> scripts;
GoodsSkuDO
@TableName("es_goods_sku")
@ApiModel
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GoodsSkuDO implements Serializable {
private static final long serialVersionUID = 5102510694003249L;
/**
* 主键
*/
@TableId(type= IdType.ASSIGN_ID)
@ApiModelProperty(hidden = true)
private Long skuId;
/**
* 商品id
*/
@ApiModelProperty(name = "goods_id", value = "商品id", hidden = true)
private Long goodsId;
/**
* 商品名称
*/
@ApiModelProperty(name = "goods_name", value = "商品名称", hidden = true)
private String goodsName;
/**
* 商品编号
*/
@ApiModelProperty(name = "sn", value = "商品编号", required = false)
@Length(max = 30,message = "商品规格编号太长,不能超过30个字符")
private String sn;
/**
* 库存
*/
@ApiModelProperty(name = "quantity", value = "库存", required = false)
@Max(value = 99999999, message = "库存不能超过99999999")
private Integer quantity;
/**
* 可用库存
*/
@ApiModelProperty(name = "enable_quantity", value = "可用库存")
private Integer enableQuantity;
/**
* 商品价格
*/
@ApiModelProperty(name = "price", value = "商品价格", required = false)
@Max(value = 99999999, message = "价格不能超过99999999")
private Double price;
/**
* 规格信息json
*/
@ApiModelProperty(name = "specs", value = "规格信息json", hidden = true)
private String specs;
/**
* 成本价格
*/
@ApiModelProperty(name = "cost", value = "成本价格", required = true)
@Max(value = 99999999, message = "成本价格不能超过99999999")
private Double cost;
/**
* 重量
*/
@ApiModelProperty(name = "weight", value = "重量", required = true)
@Max(value = 99999999, message = "重量不能超过99999999")
private Double weight;
/**
* 卖家id
*/
@ApiModelProperty(name = "seller_id", value = "卖家id", hidden = true)
private Long sellerId;
/**
* 卖家名称
*/
@ApiModelProperty(name = "seller_name", value = "卖家名称", hidden = true)
private String sellerName;
/**
* 分类id
*/
@ApiModelProperty(name = "category_id", value = "分类id", hidden = true)
private Long categoryId;
/**
* 缩略图
*/
@ApiModelProperty(name = "thumbnail", value = "缩略图", hidden = true)
private String thumbnail;
@ApiModelProperty(name = "hash_code", value = "hash_code", hidden = true)
private Integer hashCode;
/**
* 运费模板ID(非数据库字段)
*/
@TableField(exist = false)
private Long templateId;
3、核心代码展示
缓存商品的查询和写入
/**
* 缓存中查询商品的信息
*
* @param goodsId 商品主键
* @return
*/
@Override
public CacheGoods getFromCache(Long goodsId) {
CacheGoods goods = (CacheGoods) cache.get(CachePrefix.GOODS.getPrefix() + goodsId);
logger.debug("由缓存中读出商品:");
logger.debug(goods);
if (goods == null) {
GoodsDO goodsDB = this.getModel(goodsId);
if (goodsDB == null) {
throw new ServiceException(GoodsErrorCode.E301.code(), "该商品已被彻底删除");
}
// GoodsVo的对象返回,GoodsVo中的skuList是要必须填充好的
List<GoodsSkuVO> skuList = goodsSkuManager.listByGoodsId(goodsId);
goods = new CacheGoods();
BeanUtils.copyProperties(goodsDB, goods);
goods.setSkuList(skuList);
//填充库存数据
fillStock(goods);
cache.put(CachePrefix.GOODS.getPrefix() + goodsId, goods);
logger.debug("由缓存中读出商品为空,由数据库中返回商品:");
logger.debug(goods);
return goods;
} else {
//填充库存数据
fillStock(goods);
}
return goods;
}
五、 商品详情页
1、设计说明
商品详情页分为服务器端渲染和前端异步渲染部分,服务器端渲染通过静态页功能静态化展示
服务器端渲染:商品名称、商品价格、分类名称、规格参数、商品详情、商品相册、商品好评率、店铺的信息等
浏览器异步渲染:商品的库存,商品的规格,商品的参与的活动、商品评论、商品咨询、销售记录等
2、表结构设计
商品相册表(es_goods_gallery) 存储商品图片的链接地址
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
img_id | bigint(20) | 主键ID | 是 |
goods_id | bigint(20) | 商品主键 | 是 |
thumbnail | varchar(255) | 缩略图路径 | 否 |
small | varchar(255) | 小图路径 | 否 |
big | varchar(255) | 大图路径 | 否 |
original | varchar(255) | 原图路径 | 否 |
tiny | varchar(255) | 极小图路径 | 否 |
isdefault | int(1) | 是否是默认图片 | 否 |
sort | int(2) | 排序 | 否 |
交易快照表(es_goods_snapshot) 订单生成时,把下单的商品数据写入此表,留作凭证。
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
snapshot_id | bigint(20) | 主键ID | 是 |
goods_id | bigint(20) | 商品id | 是 |
name | varchar(255) | 商品名称 | 否 |
sn | varchar(255) | 商品编号 | 是 |
brand_name | varchar(255) | 品牌名称 | 否 |
category_name | varchar(255) | 分类名称 | 否 |
goods_type | varchar(20) | 商品类型 | 是 |
weight | decimal(10) | 重量 | 否 |
intro | longtext | 商品详情 | 否 |
mobile_intro | longtext | 商品移动端详情 | 否 |
price | decimal(10) | 商品价格 | 否 |
cost | decimal(10) | 商品成本价 | 否 |
mktprice | decimal(10) | 商品市场价 | 否 |
have_spec | smallint(1) | 商品是否开启规格 | 否 |
params_json | longtext | 参数json | 否 |
img_json | longtext | 图片json | 否 |
create_time | bigint(20) | 快照时间 | 否 |
point | int(10) | 商品使用积分 | 否 |
seller_id | bigint(20) | 所属卖家 | 否 |
promotion_json | longtext | 促销json | 否 |
coupon_json | longtext | 优惠券json | 否 |
member_id | bigint(20) | 所属会员 | 否 |
3、核心代码展示
静态部分
@ApiOperation(value = "浏览商品的详情,静态部分使用")
@ApiImplicitParam(name = "goods_id", value = "分类id,顶级为0", required = true, dataType = "long", paramType = "path")
@GetMapping("/{goods_id}")
public GoodsShowDetail getGoodsDetail(@PathVariable("goods_id") Long goodsId) {
GoodsDO goods = goodsQueryManager.getModel(goodsId);
GoodsShowDetail detail = new GoodsShowDetail();
if (goods == null){
throw new ResourceNotFoundException("不存在此商品");
}
BeanUtils.copyProperties(goods,detail);
Integer goodsOff = 0;
//商品不存在,直接返回
if(goods == null){
detail.setGoodsOff(goodsOff);
return detail;
}
//分类
CategoryDO category = categoryManager.getModel(goods.getCategoryId());
detail.setCategoryName(category == null ? "":category.getName());
//上架状态
if(goods.getMarketEnable()==1){
goodsOff = 1;
}
detail.setGoodsOff(goodsOff);
//参数
List<GoodsParamsGroupVO> list = this.goodsParamsManager.queryGoodsParams(goods.getCategoryId(),goodsId);
detail.setParamList(list);
//相册
List<GoodsGalleryDO> galleryList = goodsGalleryManager.list(goodsId);
detail.setGalleryList(galleryList);
//商品好平率
detail.setGrade(goodsQueryManager.getGoodsGrade(goodsId));
return detail;
}
动态部分
@ApiOperation(value = "获取sku信息,商品详情页动态部分")
@ApiImplicitParam(name = "goods_id", value = "商品id", required = true, dataType = "long", paramType = "path")
@GetMapping("/{goods_id}/skus")
public List<GoodsSkuVO> getGoodsSkus(@PathVariable("goods_id") Long goodsId) {
CacheGoods goods = goodsQueryManager.getFromCache(goodsId);
return goods.getSkuList();
}
@Override
CacheGoods goods = (CacheGoods) cache.get(CachePrefix.GOODS.getPrefix() + goodsId);
if (goods == null) {
GoodsDO goodsDB = this.getModel(goodsId);
if (goodsDB == null) {
throw new ServiceException(GoodsErrorCode.E301.code(), "该商品已被彻底删除");
}
// GoodsVo的对象返回,GoodsVo中的skuList是要必须填充好的
List<GoodsSkuVO> skuList = goodsSkuManager.listByGoodsId(goodsId);
//类型转换
goods = new CacheGoods();
BeanUtils.copyProperties(goodsDB, goods);
goods.setSkuList(skuList);
//填充库存数据
fillStock(goods);
cache.put(CachePrefix.GOODS.getPrefix() + goodsId, goods);
return goods;
} else {
//填充库存数据
fillStock(goods);
}
六、 库存设计
1、设计思路
添加库存:goods表的库存字段,人员不能进行编辑,而是编辑对应的SKU库存,再通过SKU总库存,写入goods表的库存字段
扣减库存:扣减reids 的库存,一定状态后统一同步到数据库。
2、实现方法
采用lua脚本执行redis中的库存扣减 数据库的更新采用非时时同步,而是建立了一个缓冲池,当达到一定条件时再同步数据库。 这样条件有:缓冲区大小,缓冲次数,缓冲时间 上述条件在配置中心可以配置,如果没有配置采用 ${@link UpdatePool} 默认值
在配置项说明:
缓冲区大小:shoptnt.pool.stock.max-pool-size
缓冲次数:shoptnt.pool.stock.max-update-time
缓冲时间(秒数):shoptnt.pool.stock.max-lazy-second
3、核心代码展示
GoodsQuantityVO:
//商品ID
private Long goodsId;
//skuID
private Long skuId;
//库存值
private Integer quantity;
//库存类型 actual: 实际的库存,包含了待发货的
enable: 可以售的库存,不包含待发货的
private QuantityType quantityType;
GoodsQueryManagerImpl
/**
* 为商品填充库存信息<br/>
* 库存的信息存储在单独的缓存key中<br/>
* 由缓存中读取出所有sku的库存,并分别为goods.skuList中的sku设置库存,以保证库存的时时正确性<br/>
* 同时还会将所有的sku库存累加设置为商品的库存
*
* @param goods
*/
protected void (CacheGoods goods) {
List<GoodsSkuVO> skuList = goods.getSkuList();
//由缓存中获取sku的可用库存和实际库存
//此操作为批量操作,因为是高频操作,要尽量减少和redis的交互次数
List keys = createKeys(skuList);
//将商品的可用库存和实际库存一起读
keys.add(StockCacheKeyUtil.goodsEnableKey(goods.getGoodsId()));
keys.add(StockCacheKeyUtil.goodsActualKey(goods.getGoodsId()));
List<String> quantityList = stringRedisTemplate.opsForValue().multiGet(keys);
int enableTotal = 0;
int actualTotal = 0;
int i = 0;
for (GoodsSkuVO skuVO : skuList) {
//第一个是可用库存
Integer enable = StringUtil.toInt(quantityList.get(i), null);
i++;
//第二个是真实库存
Integer actual = StringUtil.toInt(quantityList.get(i), null);
//缓存被击穿,由数据库中读取
if (enable == null || actual == null) {
Map<String, Integer> map = goodsQuantityManager.fillCacheFromDB(skuVO.getSkuId());
enable = map.get("enable_quantity");
actual = map.get("quantity");
//重置缓存中的库存
stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.skuEnableKey(skuVO.getSkuId()), "" + enable);
stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.skuActualKey(skuVO.getSkuId()), "" + actual);
}
skuVO.setEnableQuantity(enable);
skuVO.setQuantity(actual);
if (enable == null) {
enable = 0;
}
if (actual == null) {
actual = 0;
}
//累计商品的库存
enableTotal += enable;
actualTotal += actual;
i++;
}
//设置商品的库存
goods.setEnableQuantity(enableTotal);
goods.setQuantity(actualTotal);
//读取缓存中商品的库存,看是否被击穿了
//第一个是可用库存
Integer goodsEnable = StringUtil.toInt(quantityList.get(i), null);
i++;
//第二个是真实库存
Integer goodsActual = StringUtil.toInt(quantityList.get(i), null);
//商品的库存被击穿了
if (goodsEnable == null || goodsActual == null) {
//重置缓存中的库存
stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.goodsEnableKey(goods.getGoodsId()), "" + enableTotal);
stringRedisTemplate.opsForValue().set(StockCacheKeyUtil.goodsActualKey(goods.getGoodsId()), "" + actualTotal);
}
}
七、 搜索设计
1、设计思路
商品是高频率搜索功能,数据库查询压力大,所以由 elasticsearch 进行创建映射,创建索引,进行搜索。。
2、表结构设计
商品分词表(es_goods_words) 为商品搜索之前,提供一个模糊查询商品的数量
字段名 | 类型 | 备注 | 是否索引 |
---|---|---|---|
id | bigint(20) | 主键ID | 是 |
words | varchar(255) | 分词 | 否 |
goods_num | bigint(20) | 数量 | 否 |
quanpin | varchar(255) | 全拼字母 | 否 |
szm | varchar(255) | 首字母 | 否 |
type | varchar(255) | 类型 | 否 |
sort | int(10) | 排序字段 | 否 |
3、代码示例
1.创建映射(goodsMapping) EsDeployExecutor#createGoodsMapping方法中新增所需字段
创建商品mapping 类型为HashMap
/**
* 创建商品mapping
*/
private Map createGoodsMapping() {
Map goodsMap = new HashMap();
//放入商品信息到创建的map中
goodsMap.put("goodsId", new MyMap().put("type", "long").getMap());
goodsMap.put("goodsName", new MyMap().put("type", "text").put("analyzer", "ik_max_word").getMap());
goodsMap.put("thumbnail", new MyMap().put("type", "text").getMap());
goodsMap.put("small", new MyMap().put("type", "text").getMap());
goodsMap.put("buyCount", new MyMap().put("type", "integer").getMap());
goodsMap.put("sellerId", new MyMap().put("type", "long").getMap());
goodsMap.put("sellerName", new MyMap().put("type", "text").getMap());
goodsMap.put("shopCatId", new MyMap().put("type", "long").getMap());
goodsMap.put("shopCatPath", new MyMap().put("type", "text").getMap());
goodsMap.put("commentNum", new MyMap().put("type", "integer").getMap());
goodsMap.put("grade", new MyMap().put("type", "double").getMap());
goodsMap.put("price", new MyMap().put("type", "double").getMap());
goodsMap.put("brand", new MyMap().put("type", "long").getMap());
goodsMap.put("categoryId", new MyMap().put("type", "long").getMap());
goodsMap.put("categoryPath", new MyMap().put("type", "text").getMap());
goodsMap.put("disabled", new MyMap().put("type", "integer").getMap());
goodsMap.put("marketEnable", new MyMap().put("type", "integer").getMap());
goodsMap.put("priority", new MyMap().put("type", "integer").getMap());
goodsMap.put("isAuth", new MyMap().put("type", "integer").getMap());
goodsMap.put("intro", new MyMap().put("type", "text").getMap());
goodsMap.put("selfOperated", new MyMap().put("type", "integer").getMap());
//参数有多种,所以创建多个map
Map paramPro = new MyMap().put("name", new MyMap().put("type", "keyword").getMap()).put("value", new MyMap().put("type", "keyword").getMap()).getMap();
goodsMap.put("params", new MyMap().put("type", "nested").put("properties", paramPro).getMap());
return new MyMap().put("properties", goodsMap).getMap();
}
2.索引类增加对应属性
cn.shoptnt.core.goodssearch.model.GoodsIndex 这个类中也需要新增对应字段
```java
@Document(indexName = "#{esConfig.indexName}_"+ EsSettings.GOODS_INDEX_NAME, type = EsSettings.GOODS_TYPE_NAME)
public class GoodsIndex {
public GoodsIndex() {
}
@Id
private Long goodsId;
@Field(type = FieldType.text, analyzer = "ik_max_word")
private String goodsName;
@Field(type = FieldType.Long)
private Long sellerId;
....
}
3.填充商品信息 GoodsIndexManagerImpl#getSource方法中添加对应字段,将商品对应字段值信息存储到ES中
把HashMap封装成 GoodsIndex 类
/**
* 封装成内存需要格式数据
* @param goods
*/
protected GoodsIndex getSource(Map goods) {
GoodsIndex goodsIndex = new GoodsIndex();
goodsIndex.setGoodsId(StringUtil.toLong(goods.get("goods_id").toString(), 0));
goodsIndex.setGoodsName(goods.get("goods_name").toString());
goodsIndex.setThumbnail(goods.get("thumbnail") == null ? "" : goods.get("thumbnail").toString());
goodsIndex.setSmall(goods.get("small") == null ? "" : goods.get("small").toString());
Double p = goods.get("price") == null ? 0d : StringUtil.toDouble(goods.get("price").toString(), 0d);
goodsIndex.setPrice(p);
Double discountPrice = goods.get("discount_price") == null ? 0d : StringUtil.toDouble(goods.get("discount_price").toString(), 0d);
goodsIndex.setDiscountPrice(discountPrice);
goodsIndex.setBuyCount(goods.get("buy_count") == null ? 0 : StringUtil.toInt(goods.get("buy_count").toString(), 0));
goodsIndex.setSellerId(StringUtil.toLong(goods.get("seller_id").toString(), 0));
//店铺分组
goodsIndex.setShopCatId(goods.get("shop_cat_id") == null ? 0 : StringUtil.toLong(goods.get("shop_cat_id").toString(), 0));
goodsIndex.setShopCatPath("");
if (goodsIndex.getShopCatId() != 0) {
ShopCatDO shopCat = shopCatClient.getModel(goodsIndex.getShopCatId());
if (shopCat != null) {
goodsIndex.setShopCatPath(HexUtils.encode(shopCat.getCatPath()));
}
}
goodsIndex.setSellerName(goods.get("seller_name").toString());
goodsIndex.setCommentNum(goods.get("comment_num") == null ? 0 : StringUtil.toInt(goods.get("comment_num").toString(), 0));
goodsIndex.setGrade(goods.get("grade") == null ? 100 : StringUtil.toDouble(goods.get("grade").toString(), 100d));
goodsIndex.setBrand(goods.get("brand_id") == null ? 0 : StringUtil.toLong(goods.get("brand_id").toString(), 0));
goodsIndex.setCategoryId(goods.get("category_id") == null ? 0 : StringUtil.toLong(goods.get("category_id").toString(), 0));
CategoryDO cat = categoryManager.getModel(Long.valueOf(goods.get("category_id").toString()));
if (cat!=null){
goodsIndex.setCategoryPath(HexUtils.encode(cat.getCategoryPath()));
}
goodsIndex.setDisabled(StringUtil.toInt(goods.get("disabled").toString(), 0));
goodsIndex.setMarketEnable(StringUtil.toInt(goods.get("market_enable").toString(), 0));
goodsIndex.setIsAuth(StringUtil.toInt(goods.get("is_auth").toString(), 0));
goodsIndex.setIntro(goods.get("intro") == null ? "" : goods.get("intro").toString());
goodsIndex.setSelfOperated(goods.get("self_operated") == null ? 0 : StringUtil.toInt(goods.get("self_operated").toString(), 0));
//添加商品优先级维度
goodsIndex.setPriority(goods.get("priority") == null ? 1 : StringUtil.toInt(goods.get("priority").toString(), 1));
//参数维度,已填写参数
List<Map> params = (List<Map>) goods.get("params");
List<Param> paramsList = this.convertParam(params);
goodsIndex.setParams(paramsList);
return goodsIndex;
}
4.封装然后存入es
先把HashMap封装成 GoodsIndex 类,然后在封装到 indexQuery 类,最后写入 elasticsearch
/**
* 将某个商品加入索引<br>
* @param goods 商品数据
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void addIndex(Map goods) {
String goodsName = goods.get("goods_name").toString();
try {
//配置文件中定义的索引名字
String indexName = esConfig.getIndexName() + "_" + EsSettings.GOODS_INDEX_NAME;
GoodsIndex goodsIndex = this.getSource(goods);
IndexQuery indexQuery = new IndexQuery();
indexQuery.setIndexName(indexName);
indexQuery.setType(EsSettings.GOODS_TYPE_NAME);
indexQuery.setId(goodsIndex.getGoodsId().toString());
indexQuery.setObject(goodsIndex);
//审核通过且没有下架且没有删除
boolean flag = goodsIndex.getDisabled() == 1 && goodsIndex.getMarketEnable() == 1 && goodsIndex.getIsAuth() == 1;
if (flag) {
List<String> wordsList = toWordsList(goodsName);
// 分词入库
this.wordsToDb(wordsList);
}
elasticsearchOperations.index(indexQuery);
} catch (Exception e) {
e.printStackTrace();
logger.error("为商品["+goodsName+"]生成索引异常",e);
debugger.log("为商品["+goodsName+"]生成索引异常",StringUtil.getStackTrace(e));
throw new RuntimeException("为商品["+goodsName+"]生成索引异常", e);
}
}
5.商品搜索参数
>如果新增字段不做为搜索参数可忽略此步骤
GoodsSearchManagerImpl#createQuery方法中,添加对应字段的搜索表达式
```java
/**
* 构建查询条件
* @throws Exception
*/
protected SearchRequestBuilder createQuery(GoodsSearchDTO goodsSearch) throws Exception {
String keyword = goodsSearch.getKeyword();
Long cat = goodsSearch.getCategory();
Long brand = goodsSearch.getBrand();
String price = goodsSearch.getPrice();
Long sellerId = goodsSearch.getSellerId();
Long shopCatId = goodsSearch.getShopCatId();
SearchRequestBuilder searchRequestBuilder = elasticsearchTemplate.getClient().prepareSearch(esConfig.getIndexName() + "_" + EsSettings.GOODS_INDEX_NAME);
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 关键字检索
if (!StringUtil.isEmpty(keyword)) {
QueryStringQueryBuilder queryString = new QueryStringQueryBuilder(keyword).field("goodsName");
queryString.defaultOperator(Operator.AND);
//按照关键字检索 关键字无需按照最细粒度分词 update by liuyulei 2019-12-12
queryString.analyzer("ik_smart");
queryString.useDisMax(false);
boolQueryBuilder.must(queryString);
}
// 品牌搜素
if (brand != null) {
boolQueryBuilder.must(QueryBuilders.termQuery("brand", brand));
}
// 分类检索
if (cat != null) {
CategoryDO category = categoryManager.getModel(cat);
if (category == null) {
throw new ServiceException("", "该分类不存在");
}
boolQueryBuilder.must(QueryBuilders.wildcardQuery("categoryPath", HexUtils.encode(category.getCategoryPath()) + "*"));
}
//卖家搜索
if (sellerId != null) {
boolQueryBuilder.must(QueryBuilders.termQuery("sellerId", sellerId));
}
// 卖家分组搜索
if (shopCatId != null) {
ShopCatDO shopCat = shopCatClient.getModel(shopCatId);
if (shopCat == null) {
throw new ServiceException("", "该分组不存在");
}
boolQueryBuilder.must(QueryBuilders.wildcardQuery("shopCatPath", HexUtils.encode(shopCat.getCatPath()) + "*"));
}
// 参数检索
String prop = goodsSearch.getProp();
if (!StringUtil.isEmpty(prop)) {
String[] propArray = prop.split(Separator.SEPARATOR_PROP);
for (String p : propArray) {
String[] onpropAr = p.split(Separator.SEPARATOR_PROP_VLAUE);
String name = onpropAr[0];
String value = onpropAr[1];
boolQueryBuilder.must(QueryBuilders.nestedQuery("params", QueryBuilders.termQuery("params.name", name), ScoreMode.None));
boolQueryBuilder.must(QueryBuilders.nestedQuery("params", QueryBuilders.termQuery("params.value", value), ScoreMode.None));
}
}
//价格搜索
if (!StringUtil.isEmpty(price)) {
String[] pricear = price.split(Separator.SEPARATOR_PROP_VLAUE);
double min = StringUtil.toDouble(pricear[0], 0.0);
double max = Integer.MAX_VALUE;
if (pricear.length == 2) {
max = StringUtil.toDouble(pricear[1], Double.MAX_VALUE);
}
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").from(min).to(max).includeLower(true).includeUpper(true));
}
// 删除的商品不显示
boolQueryBuilder.must(QueryBuilders.termQuery("disabled", "1"));
// 未上架的商品不显示
boolQueryBuilder.must(QueryBuilders.termQuery("marketEnable", "1"));
// 待审核和审核不通过的商品不显示
boolQueryBuilder.must(QueryBuilders.termQuery("isAuth", "1"));
searchRequestBuilder.setQuery(boolQueryBuilder);
//排序
String sortField = goodsSearch.getSort();
String sortId = "priority";
SortOrder sort = SortOrder.DESC;
if (!StringUtil.isEmpty(sortField)) {
Map<String, String> sortMap = SortContainer.getSort(sortField);
sortId = sortMap.get("id");
// 如果是默认排序 --默认排序根据 商品优先级排序 add by liuyulei _2019-07-01
if ("def".equals(sortId)) {
sortId = "priority";
}
if ("buynum".equals(sortId)) {
sortId = "buyCount";
}
if ("grade".equals(sortId)) {
sortId = "commentNum";
}
if ("desc".equals(sortMap.get("def_sort"))) {
sort = SortOrder.DESC;
} else {
sort = SortOrder.ASC;
}
}
searchRequestBuilder.addSort(sortId, sort);
//好平率
if ("commentNum".equals(sortId)) {
searchRequestBuilder.addSort("buyCount", sort);
// boolQueryBuilder.must(QueryBuilders.rangeQuery("buyCount").from(1).includeLower(true));
}
//如果不是默认排序 则在原有搜索结果基础上加上商品优先级排序 add by liuyulei 2019-07-01
if (!"priority".equals(sortId)) {
//商品优先级
searchRequestBuilder.addSort("priority", SortOrder.DESC);
}
return searchRequestBuilder;
}
ES搜索新增字段之后,需要执行部署程序重新初始化ES,然后管理端手动点击生成索引,否则搜索不到新增字段数据