电商品台新增商品的过程大致如下:

另外为了测试,我还手动新增了一些测试数据:

一、新增会员、仓储、订单、优惠券等模块前端页面;
1、将会员、仓储、订单、优惠券等模块注册进nacos并正常启动

此时nacos中的服务列表:

2、使用网关服务,对会员、仓储、订单、优惠券等模块进行路由转发:
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://zidanmall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>/?.*), /$\{segment}
- id: coupon_route
uri: lb://zidanmall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?<segment>/?.*), /$\{segment}
- id: member_route
uri: lb://zidanmall-member
predicates:
- Path=/api/member/**
filters:
- RewritePath=/api/(?<segment>/?.*), /$\{segment}
- id: order_route
uri: lb://zidanmall-order
predicates:
- Path=/api/order/**
filters:
- RewritePath=/api/(?<segment>/?.*), /$\{segment}
- id: ware_route
uri: lb://zidanmall-ware
predicates:
- Path=/api/ware/**
filters:
- RewritePath=/api/(?<segment>/?.*), /$\{segment}
- id: third_party_route
uri: lb://zidanmall-third-party
predicates:
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>/?.*), /$\{segment}
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
二、新增商品——1——基本信息
1、在用户系统——会员等级下,添加几条会员等级数据:

2、新增商品基本信息第一步:需要“根据所选分类查询品牌列表”接口:
GET /product/categorybrandrelation/brands/list
Controller层接口:
@GetMapping("/brands/list")
public R relationBrandsList(@RequestParam(value = "catId", required = true) Long catId){
List<BrandEntity> brandEntities = categoryBrandRelationService.getBrandsByCatId(catId);
List<BrandVo> data = null;
if (!CollectionUtils.isEmpty(brandEntities)){
data = brandEntities.stream().map(b -> {
BrandVo brandVo = new BrandVo();
brandVo.setBrandId(b.getBrandId());
brandVo.setBrandName(b.getName());
return brandVo;
}).collect(Collectors.toList());
}
return R.ok().put("data", data);
}
到这,第一步“基本信息”的添加就通顺了;

三、新增商品——2——规格参数
获取制定三级分类下的所有“属性分组”以及“关联的基本属性”
GET /product/attrgroup/{catelogId}/withattr
需要相应的数据结构如下:
{
"msg": "success",
"code": 0,
"data": [{
"attrGroupId": 1,
"attrGroupName": "主体",
"sort": 0,
"descript": "主体",
"icon": "dd",
"catelogId": 225,
"attrs": [{
"attrId": 7,
"attrName": "入网型号",
"searchType": 1,
"valueType": 0,
"icon": "xxx",
"valueSelect": "aaa;bb",
"attrType": 1,
"enable": 1,
"catelogId": 225,
"showDesc": 1,
"attrGroupId": null
}, {
"attrId": 8,
"attrName": "上市年份",
"searchType": 0,
"valueType": 0,
"icon": "xxx",
"valueSelect": "2018;2019",
"attrType": 1,
"enable": 1,
"catelogId": 225,
"showDesc": 0,
"attrGroupId": null
}]
}]
}
1、Service层核心代码实现:
@Override
public List<AttrGroupWithAttrVo> getAttrGroupWithAttrsByCatelogId(Long catelogId) {
//获取catelogId对于的所有分组列表
List<AttrGroupEntity> groups = this.baseMapper.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
if (CollectionUtils.isEmpty(groups)){
return null;
}
//填充对应的attrs
return groups.stream().map(g -> {
AttrGroupWithAttrVo attrGroupWithAttrVo = new AttrGroupWithAttrVo();
BeanUtils.copyProperties(g, attrGroupWithAttrVo);
List<AttrAttrgroupRelationEntity> relations = attrAttrgroupRelationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", g.getAttrGroupId()));
if (!CollectionUtils.isEmpty(relations)) {
List<Long> collect = relations.stream().map(AttrAttrgroupRelationEntity::getAttrId).collect(Collectors.toList());
List<AttrEntity> attrs = attrDao.selectList(new QueryWrapper<AttrEntity>().in("attr_id", collect));
attrGroupWithAttrVo.setAttrs(attrs);
}
return attrGroupWithAttrVo;
}).collect(Collectors.toList());
}
2、页面效果:

其中的快速展示选择框,会继承“规格参数”创建时候的默认值,但是在这里我们还可以去掉;“去掉的话,只影响当前商品”;
四、新增商品——完整过程
1、添加基本信息:

2、添加规格参数:

3、添加销售属性:

4、添加SKU信息(笛卡尔积列表)

5、点击提交:被提交的数据会以一个“超级JSON”提交到后台,后台需要花一定的时间去储存这部分数据:

对于这些复杂数据,我们需要专门准备一个大步骤描述;
五、新增商品——后端接口存储复杂商品数据
1、分析提交的数据:我们使用在线json工具格式化,并自动生成java类:
https://www.bejson.com/?src=xiaof
https://www.bejson.com/json2javapojo/new/

下载自动生成的java类,并将对于的Id的类型修改为Long,价格类的类型修改为BigDecimal
2、商品新增业务逻辑分析——分析如下:
|
//1、保存Spu基本信息; pms_spu_info //2、保存Spu的描述图片; pms_spu_info_desc //3、保存Spu的图片集; pms_spu_images //4、保存Spu的规格参数; pms_product_attr_value //5、保存Spu的积分信息;——跨服务调用:zidanmall-coupon:sms_spu_bounds //6、保存当前Spu对应的所有Sku信息; //6.1、保存Sku的基本信息; pms_sku_info //6.2、保存Sku的图片信息; pms_sku_images //6.3、保存Sku的销售属性信息; pms_sku_sale_attr_value //6.4、保存Sku的优惠、满减等信息;——跨服务调用:zidanmall-coupon:sms_sku_ladder(打折表)–>sms_sku_full_reduction(满减表)–>sms_member_price(会员价格表) |
3、开始编写新增逻辑:
SpuInfoController.java:
@RequestMapping("/save")
public R save(@RequestBody SpuSaveVo vo){
spuInfoService.saveSpuInfo(vo);
return R.ok();
}
SpuInfoServiceImpl.java:
@Transactional
@Override
public void saveSpuInfo(SpuSaveVo vo) {
//1、保存spu基本信息 pms_spu_info
SpuInfoEntity infoEntity = new SpuInfoEntity();
BeanUtils.copyProperties(vo,infoEntity);
infoEntity.setCreateTime(new Date());
infoEntity.setUpdateTime(new Date());
this.baseMapper.insert(infoEntity);
//2、保存Spu的描述图片 pms_spu_info_desc
List<String> decript = vo.getDecript();
SpuInfoDescEntity descEntity = new SpuInfoDescEntity();
descEntity.setSpuId(infoEntity.getId());
descEntity.setDecript(String.join(",",decript));
spuInfoDescService.saveSpuInfoDesc(descEntity);
//3、保存spu的图片集 pms_spu_images
List<String> images = vo.getImages();
imagesService.saveImages(infoEntity.getId(),images);
//4、保存spu的规格参数;pms_product_attr_value
List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
List<ProductAttrValueEntity> collect = baseAttrs.stream().map(attr -> {
ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();
valueEntity.setAttrId(attr.getAttrId());
AttrEntity id = attrService.getById(attr.getAttrId());
valueEntity.setAttrName(id.getAttrName());
valueEntity.setAttrValue(attr.getAttrValues());
valueEntity.setQuickShow(attr.getShowDesc());
valueEntity.setSpuId(infoEntity.getId());
return valueEntity;
}).collect(Collectors.toList());
attrValueService.saveProductAttr(collect);
//5、保存spu的积分信息;zidanmall_sms->sms_spu_bounds
Bounds bounds = vo.getBounds();
SpuBoundTo spuBoundTo = new SpuBoundTo();
BeanUtils.copyProperties(bounds,spuBoundTo);
spuBoundTo.setSpuId(infoEntity.getId());
R r = couponFeignService.saveSpuBounds(spuBoundTo);
if(r.getCode() != 0){
log.error("远程保存spu积分信息失败");
}
//6、保存当前spu对应的所有sku信息;
List<Skus> skus = vo.getSkus();
if(skus!=null && skus.size()>0){
skus.forEach(item->{
String defaultImg = "";
for (Images image : item.getImages()) {
if(image.getDefaultImg() == 1){
defaultImg = image.getImgUrl();
}
}
SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
BeanUtils.copyProperties(item,skuInfoEntity);
skuInfoEntity.setBrandId(infoEntity.getBrandId());
skuInfoEntity.setCatalogId(infoEntity.getCatalogId());
skuInfoEntity.setSaleCount(0L);
skuInfoEntity.setSpuId(infoEntity.getId());
skuInfoEntity.setSkuDefaultImg(defaultImg);
//6.1)、sku的基本信息;pms_sku_info
skuInfoService.saveSkuInfo(skuInfoEntity);
Long skuId = skuInfoEntity.getSkuId();
List<SkuImagesEntity> imagesEntities = item.getImages().stream().map(img -> {
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(img.getImgUrl());
skuImagesEntity.setDefaultImg(img.getDefaultImg());
return skuImagesEntity;
}).filter(entity->StringUtils.isNotBlank(entity.getImgUrl())).collect(Collectors.toList());
//6.2)、sku的图片信息;pms_sku_image
skuImagesService.saveBatch(imagesEntities);
//TODO 没有图片路径的无需保存
List<Attr> attr = item.getAttr();
List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attr.stream().map(a -> {
SkuSaleAttrValueEntity attrValueEntity = new SkuSaleAttrValueEntity();
BeanUtils.copyProperties(a, attrValueEntity);
attrValueEntity.setSkuId(skuId);
return attrValueEntity;
}).collect(Collectors.toList());
//6.3)、sku的销售属性信息:pms_sku_sale_attr_value
skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
//6.4)、sku的优惠、满减等信息;zidanmall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
SkuReductionTo skuReductionTo = new SkuReductionTo();
BeanUtils.copyProperties(item,skuReductionTo);
skuReductionTo.setSkuId(skuId);
if(skuReductionTo.getFullCount() >0 || skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){
R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
if(r1.getCode() != 0){
log.error("远程保存sku优惠信息失败");
}
}
});
}
}
其中:5.0、6.4两节都需要调用远程服务 zidanmall_sms ,相关的 Feign 接口如下:
CouponFeignService.java:
@FeignClient("zidanmall-coupon")
public interface CouponFeignService {
/**
* 1、CouponFeignService.saveSpuBounds(spuBoundTo);
* 1)、@RequestBody将这个对象转为json。
* 2)、找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。
* 将上一步转的json放在请求体位置,发送请求;
* 3)、对方服务收到请求。请求体里有json数据。
* (@RequestBody SpuBoundsEntity spuBounds);将请求体的json转为SpuBoundsEntity;
* 只要json数据模型是兼容的。双方服务无需使用同一个to
* @param spuBoundTo
* @return
*/
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
@PostMapping("/coupon/skufullreduction/saveinfo")
R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
4、其中zidanmall_sms服务中的相关service层代码如下:
SkuFullReductionServiceImpl.java:
@Transactional
@Override
public void saveSkuReduction(SkuReductionTo reductionTo) {
//1、// //6.4)、sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
//sms_sku_ladder
SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
skuLadderEntity.setSkuId(reductionTo.getSkuId());
skuLadderEntity.setFullCount(reductionTo.getFullCount());
skuLadderEntity.setDiscount(reductionTo.getDiscount());
skuLadderEntity.setAddOther(reductionTo.getCountStatus());
if(reductionTo.getFullCount() > 0){
skuLadderService.save(skuLadderEntity);
}
//2、sms_sku_full_reduction
SkuFullReductionEntity reductionEntity = new SkuFullReductionEntity();
BeanUtils.copyProperties(reductionTo,reductionEntity);
if(reductionEntity.getFullPrice().compareTo(new BigDecimal("0"))==1){
this.save(reductionEntity);
}
//3、sms_member_price
List<MemberPrice> memberPrice = reductionTo.getMemberPrice();
List<MemberPriceEntity> collect = memberPrice.stream().map(item -> {
MemberPriceEntity priceEntity = new MemberPriceEntity();
priceEntity.setSkuId(reductionTo.getSkuId());
priceEntity.setMemberLevelId(item.getId());
priceEntity.setMemberLevelName(item.getName());
priceEntity.setMemberPrice(item.getPrice());
priceEntity.setAddOther(1);
return priceEntity;
}).filter(item-> item.getMemberPrice().compareTo(new BigDecimal("0")) == 1).collect(Collectors.toList());
memberPriceService.saveBatch(collect);
}
其实此处的ServiceImpl等代码我们虽然添加了事务,但是都是本地事务;
在后续的环节中,我将会加上 seata,以实现分布式事务;
六、新增商品——添加两条数据:
Spu管理

商品管理:——即Sku管理

七、商品管理——Spu检索:
GET /product/spuinfo/list
1、根据上面的截图,我们可以看到,Spu检索,需要支持额外的几个字段搜索:
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为',//检索关键字 ——额外
catelogId: 6,//三级分类id ——额外
brandId: 1,//品牌id ——额外
status: 0,//商品状态 ——额外
}
SpuInfoServiceImpl.java:
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (StringUtils.isNotBlank(key)){
wrapper.and(obj -> obj.eq("id", key).or().like("spu_name", key));
}
String catelogId = (String) params.get("catelogId");
if (StringUtils.isNotBlank(catelogId) && !"0".equals(catelogId)){
wrapper.eq("catalog_id", catelogId);
}
String brandId = (String) params.get("brandId");
if (StringUtils.isNotBlank(brandId) && !"0".equals(brandId)){
wrapper.eq("brand_id", brandId);
}
String status = (String)params.get("status");
if (StringUtils.isNotBlank(status)){
wrapper.eq("publish_status", status);
}
IPage<SpuInfoEntity> page = this.page(new Query<SpuInfoEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
2、另外,我们得时间格式不好看,我们希望统一为:
yyyy-MM-dd HH:mm:ss
只需要增加一个配置项:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss
效果如下:

八、商品管理——Sku检索:
GET /product/skuinfo/list
由上面的截图可以看到,Sku检索,需要支持额外的几个字段搜索:
{
page: 1,//当前页码
limit: 10,//每页记录数
sidx: 'id',//排序字段
order: 'asc/desc',//排序方式
key: '华为',//检索关键字
catelogId: 0,
brandId: 0,
min: 0,
max: 0
}
SkuInfoServiceImpl.java:
@Override
public PageUtils queryPageByCondition(Map<String, Object> params) {
QueryWrapper<SkuInfoEntity> wrapper = new QueryWrapper<>();
String key = (String) params.get("key");
if (StringUtils.isNotBlank(key)){
wrapper.and(obj -> obj.eq("sku_id", key).or().like("sku_name", key));
}
String catelogId = (String) params.get("catelogId");
if (StringUtils.isNotBlank(catelogId) && !"0".equals(catelogId)){
wrapper.eq("catalog_id", catelogId);
}
String brandId = (String) params.get("brandId");
if (StringUtils.isNotBlank(brandId) && !"0".equals(brandId)){
wrapper.eq("brand_id", brandId);
}
String min = (String) params.get("min");
if (StringUtils.isNotBlank(min)){
wrapper.ge("price", min);
}
String max = (String) params.get("max");
if (StringUtils.isNotBlank(max)){
try {
BigDecimal bigDecimal = new BigDecimal(max);
if (bigDecimal.compareTo(BigDecimal.ZERO) == 1){
wrapper.le("price", max);
}
}catch (Exception e){
e.printStackTrace();
}
}
IPage<SkuInfoEntity> page = this.page(new Query<SkuInfoEntity>().getPage(params), wrapper);
return new PageUtils(page);
}
效果如下:

到这里,商品新增(Spu、Sku管理)的内容就都完成了;
九、Spu规格维护
1、在Spu管理界面,我们点击具体的Spu的规格按钮,可以重新修改Spu的规格参数

首先获取当前的Spu规则属性信息:
GET /product/attr/base/listforspu/{spuId}
AttrController.java:
@GetMapping("/base/listforspu/{spuId}")
public R baseAttrListForSpu(@PathVariable("spuId") Long spuId){
List<ProductAttrValueEntity> data = productAttrValueService.baseAttrListForSpu(spuId);
return R.ok().put("data", data);
}
回显效果:

2、修改后保存更新:
POST /product/attr/update/{spuId}
[
{
"attrId":7,
"attrName":"入网型号",
"attrValue":"LIO-AL00",
"quickShow":1
},
{
"attrId":14,
"attrName":"机身材质工艺",
"attrValue":"玻璃",
"quickShow":0
},
{
"attrId":16,
"attrName":"CPU型号",
"attrValue":"HUAWEI Kirin 980",
"quickShow":1
}
]
ProductAttrValueServiceImpl.java:
@Transactional
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
//我们不需要去判断哪些值发生了变化,哪些值没有变化,直接将spuId对应的记录全部删除后,再新增即可;
this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
//插入操作
entities.forEach(e -> e.setSpuId(spuId));
this.saveBatch(entities);
}



