商品维护架构文档
概述
商品维护包括:新增、修改、查询、上下架、审核、删除等操作,商家和平台拥有不同的操作权限,如下:
客户端 操作权限 商家端 新增、修改、查询、上下架、删除 平台端 查询、上下架、审核 商品核心数据包含SPU和SKU,二者关系为一对多(数据结构可参考商品核心数据说明)
商品分为普通商品和虚拟商品两种,还可以在发布商品时开启积分兑换功能。
核心API
实体类参数可参考下方的类图展示
商家端
请求方式和链接 | 参数说明 | 作用 |
---|---|---|
GET - https://{seller-api-domain}/seller/goods | GoodsQueryParam param | 获取商品分页列表数据 |
POST - https://{seller-api-domain}/seller/goods | GoodsDTO goods | 新增商品信息 |
PUT - https://{seller-api-domain}/seller/goods/{id} | GoodsDTO goods、Long id:商品主键ID | 编辑商品信息 |
GET - https://{seller-api-domain}/seller/goods/{id} | Long id:商品主键ID | 根据主键ID获取商品信息 |
PUT - https://{seller-api-domain}/seller/goods/{goods_ids}/up | Long[] goodsIds:商品主键ID组 | 批量上架商品 |
PUT - https://{seller-api-domain}/seller/goods/{goods_ids}/under | Long[] goodsIds:商品主键ID组,String reason:下架原因 | 批量下架商品 |
PUT - https://{seller-api-domain}/seller/goods/{goods_ids}/putInRecycle | Long[] goodsIds:商品主键ID组 | 将商品批量放入回收站 |
PUT - https://{seller-api-domain}/seller/goods/{goods_ids}/revert | Long[] goodsIds:商品主键ID组 | 将商品批量从回收站还原至商品列表 |
DELETE - https://{seller-api-domain}/seller/goods/{goods_ids} | Long[] goodsIds:商品主键ID组 | 批量删除商品 |
管理端
请求方式和链接 | 参数说明 | 作用 |
---|---|---|
GET - https://{manager-api-domain}/admin/goods | GoodsQueryParam param | 获取商品分页列表数据 |
PUT - https://{manager-api-domain}/admin/goods/{goods_ids}/under | Long[] goodsIds:商品主键ID组,String reason:下架原因 | 批量下架商品 |
PUT - https://{manager-api-domain}/admin/goods/{goods_ids}/up | Long[] goodsIds:商品主键ID组 | 批量上架商品 |
POST - https://{manager-api-domain}/admin/goods/batch/audit | GoodsAuditParam param | 批量审核商品 |
GET - https://{manager-api-domain}/admin/goods/{id} | Long id:商品主键ID | 根据主键ID获取商品信息 |
商品发布流程
类图
商家端
管理端
实体类
DO类
DTO类
VO类
枚举类
代码展示
新增商品
/**
* 新增商品
* 全量添加商品
* @param goodsDTO 商品信息
* @return 商品对象
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public GoodsDO addWithDTO(GoodsDTO goodsDTO) {
Seller seller = UserContext.getSeller();
//检查商品sn是否重复
this.checkSnDuplicate(goodsDTO, seller.getSellerId());
//调用 通过dto接口添加商品,并返回DO
GoodsDO goods = this.addWithDTO(goodsDTO, seller);
// 获取添加商品的商品ID
Long goodsId = goods.getGoodsId();
// 添加商品参数
this.goodsParamsManager.addParams(goodsDTO.getGoodsParamsList(), goodsId);
// 添加相册
goodsDTO.getGoodsGalleryList()
.forEach(goodsGallery -> goodsGallery.setGalleryType(GalleryType.GOODS.name()));
this.goodsGalleryManager.add(goodsDTO.getGoodsGalleryList(), goodsId);
// 发送增加商品消息,店铺增加自身商品数量,静态页使用
GoodsChangeMsg goodsChangeMsg = new GoodsChangeMsg(new Long[]{goods.getGoodsId()},
GoodsChangeMsg.ADD_OPERATION);
this.messageSender.send(goodsChangeMsg);
return goods;
}
/**
* 通过DTO添加商品
*
* @param goodsDTO
* @param seller
* @return
*/
private GoodsDO addWithDTO(GoodsDTO goodsDTO, Seller seller) {
//通过dto构建DO
GoodsDO goods = new GoodsDO(goodsDTO).init();
// 向goods加入图片,第一个图片为默认图
GoodsGalleryDO goodsGalley = goodsGalleryManager
.getGoodsGallery(goodsDTO.getGoodsGalleryList().get(0).getOriginal());
goods.setOriginal(goodsGalley.getOriginal());
goods.setBig(goodsGalley.getBig());
goods.setSmall(goodsGalley.getSmall());
goods.setThumbnail(goodsGalley.getThumbnail());
goods.setSellerId(seller.getSellerId());
//调用通过do的接口添加商品,并返回DO
goods = this.add(seller, goods);
return goods;
}
/**
* 新增商品
* 直接面向表添加
* @param seller 上架信息
* @param goods 商品信息
* @return
*/
@Override
@NotNull
@Transactional(value = "goodsTransactionManager", rollbackFor = Exception.class)
public GoodsDO add(Seller seller, GoodsDO goods) {
//对商品进行敏感词过滤
String goodsName = SensitiveFilter.filter(goods.getGoodsName(), CharacterConstant.WILDCARD_STAR);
goods.setGoodsName(goodsName);
// 商品的店铺名称
goods.setSellerId(seller.getSellerId());
goods.setSellerName(seller.getSellerName());
// 判断是否是自营店铺
goods.setSelfOperated(seller.getSelfOperated() == 1 ? 1 : 0);
// 设置商品审核状态
this.setAuthStatus(goods);
// 添加商品
this.goodsMapper.insert(goods);
// 添加商品sku信息,此时商品和sku为一对一
this.goodsSkuManager.addNoSpecSku(goods);
return goods;
}
编辑商品
/**
* 修改商品
* @param goodsDTO 商品
* @param id 商品主键
* @return Goods 商品
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public GoodsDO edit(GoodsDTO goodsDTO, Long id) {
//获取原商品信息
GoodsDO goodsDO = goodsQueryManager.getModel(id);
//设置商品ID
goodsDTO.setGoodsId(id);
//对商品名字进行敏感词检测
goodsDTO.setGoodsName(SensitiveFilter.filter(goodsDTO.getGoodsName(), CharacterConstant.WILDCARD_STAR));
GoodsDO goods = new GoodsDO(goodsDTO);
// 设置商品审核状态
this.setAuthStatus(goods);
// 修改相册信息
List<GoodsGalleryDO> goodsGalleys = goodsDTO.getGoodsGalleryList();
goodsGalleys.forEach(goodsGallery -> goodsGallery.setGalleryType(GalleryType.GOODS.name()));
this.goodsGalleryManager.edit(goodsGalleys, goodsDTO.getGoodsId());
// 向goods加入图片
goods.setOriginal(goodsGalleys.get(0).getOriginal());
goods.setBig(goodsGalleys.get(0).getBig());
goods.setSmall(goodsGalleys.get(0).getSmall());
goods.setThumbnail(goodsGalleys.get(0).getThumbnail());
goods.setSellerId(goodsDO.getSellerId());
goods.setSellerName(goodsDO.getSellerName());
//查询商品的规格信息,则将规格中最低的价格赋值到商品价格中 update by liuyulei 2019-05-21
if (goodsDO.getHaveSpec() != null && goodsDO.getHaveSpec() == 1) {
//有规格则商品价格等不变,以规格的为主
goods.setPrice(goodsDO.getPrice());
goods.setCost(goodsDO.getCost());
goods.setWeight(goodsDO.getWeight());
}
// 更新商品
goods.setGoodsId(id);
UpdateWrapper wrapper = new UpdateWrapper<GoodsDO>();
wrapper.eq("goods_id", id);
// 如果商品品牌为空,更新商品品牌为空
if (goods.getBrandId() == null) {
wrapper.set("brand_id", null);
}
goods.setLockVersion(goodsDO.getLockVersion());
int i = goodsMapper.update(goods, wrapper);
if (i == 0) {
throw new ServiceException(GoodsErrorCode.E301.code(), "商品信息修改失败请重试");
}
// 处理参数信息
this.goodsParamsManager.addParams(goodsDTO.getGoodsParamsList(), id);
// 处理规格信息
this.goodsSkuManager.updateSkuByGoods(id);
//清除该商品关联的东西
this.cleanGoodsCache(id);
// 发送增加商品消息,店铺增加自身商品数量,静态页使用
GoodsChangeMsg goodsChangeMsg = new GoodsChangeMsg(new Long[]{id}, GoodsChangeMsg.UPDATE_OPERATION);
//修改商品时需要删除商品参与的促销活动
goodsChangeMsg.setDelPromotion(true);
//判断商品名称是否变化
if (!goodsDO.getGoodsName().equals(goodsDTO.getGoodsName())) {
goodsChangeMsg.setNameChange(true);
}
this.messageSender.send(goodsChangeMsg);
return goods;
}
查询分页列表数据
- 商家端和管理端的商品列表调用的是同一个方法
- ProductQueryWrapper类封装了查询商品的条件,查询条件可以根据商品基本信息(如名称、编号的)进行查询,也支持分类及店铺分组查询
- 查询的结果是按照商品搜索优先级和创建时间倒序排序
/**
* 查询商品列表
*
* @param goodsQueryParam 查询参数
* @return
*/
@Override
public WebPage list(GoodsQueryParam goodsQueryParam) {
//构建查询条件
QueryWrapper<GoodsDO> queryWrapper = new ProductQueryWrapper(ProductWrapperType.spu, shopCatClient, categoryMapper).build(goodsQueryParam);
//排序规则构建 按商品搜索优先级和创建时间倒序排序
queryWrapper.orderByDesc("priority", "create_time");
//查询分页数据
IPage<GoodsDO> page = goodsMapper.selectPage(new Page<>(goodsQueryParam.getPageNo(), goodsQueryParam.getPageSize()), queryWrapper);
return PageConvert.convert(page);
}
上架和下架
/**
* 批量下架商品
* @param goodsIds 商品id数组
* @param reason 下架理由
* @param permission 权限
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void batchUnder(Long[] goodsIds, String reason, Permission permission) {
//下架原因长度不能超过500个字符
if (reason.length() > 500) {
throw new ServiceException(PromotionErrorCode.E400.code(), "下架原因长度不能超过500个字符");
}
//验证不同客户端的操作
if (Permission.SELLER.equals(permission)) {
//获取当前登录商家信息
Seller seller = UserContext.getSeller();
//校验商家操作权限
this.checkOperaPermission(goodsIds, GoodsOperate.UNDER, seller.getSellerId());
//构建下架原因
reason = "店员[" + seller.getUsername() + "]操作下架,原因为:" + (StringUtil.notEmpty(reason) ? reason : "无原因");
} else {
//校验平台操作权限
this.checkOperaPermission(goodsIds, GoodsOperate.UNDER, null);
//平台管理员下架商品,下架原因必须填写
if (StringUtil.isEmpty(reason)) {
throw new ServiceException(PromotionErrorCode.E400.code(), "请填写下架原因");
}
reason = "平台管理员操作下架,原因为:" + reason;
}
//将商品标记为下架状态
new LambdaUpdateChainWrapper<>(goodsMapper)
//设置上下架状态为已下架 0:下架,1:上架
.set(GoodsDO::getMarketEnable, 0)
//设置下架原因
.set(GoodsDO::getUnderMessage, reason)
//设置商品最后修改时间
.set(GoodsDO::getLastModify, DateUtil.getDateline())
.in(GoodsDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//将SKU标记为下架状态
new LambdaUpdateChainWrapper<>(goodsSkuMapper)
//设置上下架状态为已下架 0:下架,1:上架
.set(GoodsSkuDO::getMarketEnable, 0)
.in(GoodsSkuDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//清除商品相关的缓存信息
for (long goodsId : goodsIds) {
this.cleanGoodsCache(goodsId);
}
//发送商品变化的消息
GoodsChangeMsg goodsChangeMsg = new GoodsChangeMsg(goodsIds, GoodsChangeMsg.UNDER_OPERATION, reason);
this.messageSender.send(goodsChangeMsg);
}
/**
* 批量上架商品
* @param goodsIds 商品id数组
* @param permission 权限
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void batchUp(Long[] goodsIds, Permission permission) {
//商品上架是否需要审核 默认不需要
boolean isAuth = false;
//验证不同客户端的操作
if (Permission.SELLER.equals(permission)) {
//获取当前登录商家信息
Seller seller = UserContext.getSeller();
//校验商家操作权限
this.checkOperaPermission(goodsIds, GoodsOperate.MARKET, seller.getSellerId());
//获取系统中对商品的设置信息
String goodsSettingJson = settingClient.get(SettingGroup.GOODS);
GoodsSettingVO goodsSetting = JsonUtil.jsonToObject(goodsSettingJson, GoodsSettingVO.class);
//判断平台是否开启了商品审核功能 0:否,1:是
if (goodsSetting.getUpdateAuth() == 1) {
isAuth = true;
}
} else {
//校验平台操作权限
this.checkOperaPermission(goodsIds, GoodsOperate.MARKET, null);
}
//修改商品状态为上架
new LambdaUpdateChainWrapper<>(goodsMapper)
//设置上下架状态为已上架 0:下架,1:上架
.set(GoodsDO::getMarketEnable, 1)
//设置商品最后修改时间
.set(GoodsDO::getLastModify, DateUtil.getDateline())
//设置商品是否需要审核
.set(isAuth, GoodsDO::getIsAuth, 0)
.in(GoodsDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//修改sku状态为上架
new LambdaUpdateChainWrapper<>(goodsSkuMapper)
//设置上下架状态为已上架 0:下架,1:上架
.set(GoodsSkuDO::getMarketEnable, 1)
//设置商品是否需要审核
.set(isAuth, GoodsSkuDO::getIsAuth, 0)
.in(GoodsSkuDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//清除相关的关联
for (long goodsId : goodsIds) {
this.cleanGoodsCache(goodsId);
}
//发送商品变化的消息
GoodsChangeMsg goodsChangeMsg = new GoodsChangeMsg(goodsIds, GoodsChangeMsg.UPDATE_OPERATION);
this.messageSender.send(goodsChangeMsg);
}
删除和还原
/**
* 将商品批量放入回收站
* 此方法主要针对商家店铺端
* @param goodsIds 商品id数组
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void batchPutInRecycle(Long[] goodsIds) {
//校验是否有操作权限
this.checkOperaPermission(goodsIds, GoodsOperate.RECYCLE, UserContext.getSeller().getSellerId());
//将商品状态设置为已删除(这里的删除代表的不是物理删除)
new LambdaUpdateChainWrapper<>(goodsMapper)
//设置上下架状态为已下架 0:下架,1:上架
.set(GoodsDO::getMarketEnable, 0)
//设置删除状态为已删除 0:已删除,1:未删除,-1:彻底删除
.set(GoodsDO::getDisabled, 0)
//设置商品最后修改时间
.set(GoodsDO::getLastModify, DateUtil.getDateline())
.in(GoodsDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//将SKU状态设置为已删除(这里的删除代表的不是物理删除)
new LambdaUpdateChainWrapper<>(goodsSkuMapper)
//设置上下架状态为已下架 0:下架,1:上架
.set(GoodsSkuDO::getMarketEnable, 0)
//设置删除状态为已删除 0:已删除,1:未删除,-1:彻底删除
.set(GoodsSkuDO::getDisabled, 0)
.in(GoodsSkuDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//清除相关的关联
for (long goodsId : goodsIds) {
this.cleanGoodsCache(goodsId);
}
//发送商品变化的消息
GoodsChangeMsg goodsChangeMsg = new GoodsChangeMsg(goodsIds, GoodsChangeMsg.INRECYCLE_OPERATION);
this.messageSender.send(goodsChangeMsg);
}
/**
* 将商品批量从回收站还原至商品列表
* 此方法主要针对商家店铺端
* @param goodsIds 商品id数组
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void batchRevertFromRecycle(Long[] goodsIds) {
//校验是否有操作权限
this.checkOperaPermission(goodsIds, GoodsOperate.REVRET, UserContext.getSeller().getSellerId());
//将商品状态设置为未删除
new LambdaUpdateChainWrapper<>(goodsMapper)
//设置删除状态为未删除 0:已删除,1:未删除,-1:彻底删除
.set(GoodsDO::getDisabled, 1)
//设置商品最后修改时间
.set(GoodsDO::getLastModify, DateUtil.getDateline())
.in(GoodsDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//将SKU状态设置为未删除
new LambdaUpdateChainWrapper<>(goodsSkuMapper)
//设置删除状态为未删除 0:已删除,1:未删除,-1:彻底删除
.set(GoodsSkuDO::getDisabled, 1)
.in(GoodsSkuDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//发送商品变化消息
GoodsChangeMsg goodsChangeMsg = new GoodsChangeMsg(goodsIds, GoodsChangeMsg.REVERT_OPERATION);
this.messageSender.send(goodsChangeMsg);
}
/**
* 批量删除商品(非物理删除)
* 此方法主要针对商家店铺端使用
* @param goodsIds 商品id数组
*/
@Override
@Transactional(value = "goodsTransactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void batchDelete(Long[] goodsIds) {
//校验是否有操作权限
this.checkOperaPermission(goodsIds, GoodsOperate.DELETE, UserContext.getSeller().getSellerId());
//设置商品状态为彻底删除
new LambdaUpdateChainWrapper<>(goodsMapper)
//设置删除状态为未删除 0:已删除,1:未删除,-1:彻底删除
.set(GoodsDO::getDisabled, -1)
//设置商品最后修改时间
.set(GoodsDO::getLastModify, DateUtil.getDateline())
.in(GoodsDO::getGoodsId, Arrays.asList(goodsIds))
.update();
//发送商品变化的消息
GoodsChangeMsg goodsChangeMsg = new GoodsChangeMsg(goodsIds, GoodsChangeMsg.DEL_OPERATION);
this.messageSender.send(goodsChangeMsg);
}
商品缓存
商品为高频率读取数据,频繁读取数据库,会导致数据库压力过大,所以使用缓存机制,常用数据存入redis缓存中。
缓存类
CacheGoods为商品缓存信息,内如如下:
核心代码
com.enation.app.javashop.service.goods.impl.GoodsQueryManagerImpl#getFromCache
/**
* 查询存放在缓存中的商品信息
*
* @param goodsId 商品主键ID
* @return
*/
@Override
public CacheGoods getFromCache(Long goodsId) {
//从缓存中取出商品信息
CacheGoods goods = (CacheGoods) cache.get(CachePrefix.GOODS.getPrefix() + goodsId);
logger.debug("由缓存中读出商品:\n{}", 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("由缓存中读出商品为空,由数据库中返回商品:", goods);
return goods;
} else {
//填充库存数据
fillStock(goods);
}
logger.debug("最终返回商品:\n{}", goods);
return goods;
}