电商品台新增商品的过程大致如下:
另外为了测试,我还手动新增了一些测试数据:
一、新增会员、仓储、订单、优惠券等模块前端页面;
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); }