当我们点击新增商品按钮:
就会出现一个弹窗:
-
基本信息:主要是一些简单的文本数据,包含了SPU和SpuDetail的部分数据,如
-
商品分类:是SPU中的
cid1
,cid2
,cid3
属性 -
品牌:是spu中的
brandId
属性 -
标题:是spu中的
title
属性 -
子标题:是spu中的
subTitle
属性 -
售后服务:是SpuDetail中的
afterService
属性 -
包装列表:是SpuDetail中的
packingList
属性
-
-
商品描述:是SpuDetail中的
description
属性,数据较多,所以单独放一个页面 -
规格参数:商品规格信息,对应SpuDetail中的
genericSpec
属性 -
SKU属性:spu下的所有Sku信息
对应到页面中的四个stepper-content
1.弹窗事件
弹窗是一个独立组件:
并且在Goods组件中已经引用它:
并且在页面中渲染:
2.基本信息栏
我们先来看下基本信息栏:
(1)商品分类
商品分类信息查询我们之前已经做过,所以这里的级联选框已经实现完成:
刷新页面,可以看到请求已经发出:
效果图:
(2)品牌选择
<1>页面
选择商品分类后,可以看到请求发起:
<2>后台接口
根据商品分类id,查询对应品牌即可。
页面需要去后台查询品牌信息,我们自然需要提供:
请求路径:/brand/cid/{cid}
请求参数:cid
/** * 注意:@GetMapping(path = "/cid/{cid}") 中的{cid}为占位符 对应url中的 /cid/77中的77 * @param cid * @return */ @GetMapping(path = "/cid/{cid}") public ResponseEntity<List<Brand>> queryBrandsByCid(@PathVariable("cid")Long cid){ List<Brand> brands = this.brandService.queryBrandsByCid(cid); if (CollectionUtils.isEmpty(brands)) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(brands); }
(2)BrandServiceImpl中添加方法
/** * 根据分类id查询品牌 * @param cid * @return */ @Override public List<Brand> queryBrandsByCid(Long cid) { return this.brandMapper.selectBrandByCid(cid); }
(3)BrandMapper中添加方法(需要人为的编写sql语句)
/** * 根据分类cid,筛选出该分类下所有的品牌信息 * sql语句解读:INNER JOIN 内连接 将tb_brand表和tb_category_brand关联起来,on为条件语句 * @param cid * @return */ @Select("SELECT b.* from tb_brand b INNER JOIN tb_category_brand cb on b.id=cb.brand_id where cb.category_id=#{cid}") List<Brand> selectBrandByCid(Long cid);
<3>效果图
3.商品描述栏
富文本编辑器有很多,例如:KindEditor、Ueditor。但并不原生支持vue
但是我们今天要说的,是一款支持Vue的富文本编辑器:
(2)Vue-Quill-Editor
npm install vue-quill-editor --save
<2>加载,在js中引入:
<3>页面使用:
<quill-editor v-model="goods.spuDetail.description" :options="editorOption"/>
(4)
使用也非常简单:
<v-stepper-content step="2"> <v-editor v-model="goods.spuDetail.description" upload-url="/upload/image"/> </v-stepper-content>
范例(GoodsForm.vue):
(5)效果图
4.规格参数栏
规格参数的查询我们之前也已经编写过接口,因为商品规格参数也是与商品分类绑定,所以需要在商品分类变化后去查询,我们也是通过watch监控来实现:
可以看到这里是根据商品分类id查询规格参数:SpecParam。我们之前写过一个根据gid(分组id)来查询规格参数的接口,我们接下来完成根据分类id查询规格参数。
(1)改造规格参数代码
<1>SpecificationController.java类中的queryParams方法
/** * 根据条件查询规格参数 * @param gid * @return */ @GetMapping(path = "/params") public ResponseEntity<List<SpecParam>> queryParams( @RequestParam(value = "gid", required = false)Long gid, @RequestParam(value = "cid", required = false)Long cid, @RequestParam(value = "generic", required = false)Boolean generic, @RequestParam(value = "searching", required = false)Boolean searching ){ List<SpecParam> params = this.specificationService.queryParams(gid, cid, generic, searching); if (CollectionUtils.isEmpty(params)){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(params); }
<2>改造SpecificationService:
/** * 根据条件查询规格参数 * * @param cid * @param gid * @param generic * @param searching * @return */ @Override public List<SpecParam> queryParams(Long gid, Long cid, Boolean generic, Boolean searching) { SpecParam param = new SpecParam(); param.setGroupId(gid); param.setCid(cid); param.setGeneric(generic); param.setSearching(searching); return this.specParamMapper.select(param); }
如果param中有属性为null,则不会把属性作为查询条件,因此该方法具备通用性,即可根据gid查询,也可根据cid查询。
(2)效果图
5.SKU属性栏
Sku属性是SPU下的每个商品的不同特征,如图:
当你选择了上图中的这些选项时:
-
颜色共2种:迷夜黑,勃艮第红,绚丽蓝
-
内存共2种:4GB,6GB
-
机身存储1种:64GB,128GB
此时会产生多少种SKU呢? 应该是 3 * 2 * 2 = 12种,这其实就是在求笛卡尔积。
6.页面表单提交
在sku列表的下方,有一个提交按钮:
并且绑定了点击事件:
点击后会组织数据并向后台提交:
submit() { // 表单校验。 if(!this.$refs.basic.validate){ this.$message.error("请先完成表单内容!"); } // 先处理goods,用结构表达式接收,除了categories外,都接收到goodsParams中 const { categories: [{ id: cid1 }, { id: cid2 }, { id: cid3 }], ...goodsParams } = this.goods; // 处理规格参数 const specs = {}; this.specs.forEach(({ id,v }) => { specs[id] = v; }); // 处理特有规格参数模板 const specTemplate = {}; this.specialSpecs.forEach(({ id, options }) => { specTemplate[id] = options; }); // 处理sku const skus = this.skus .filter(s => s.enable) .map(({ price, stock, enable, images, indexes, ...rest }) => { // 标题,在spu的title基础上,拼接特有规格属性值 const title = goodsParams.title + " " + Object.values(rest).map(v => v.v).join(" "); const obj = {}; Object.values(rest).forEach(v => { obj[v.id] = v.v; }); return { price: this.$format(price), // 价格需要格式化 stock, indexes, enable, title, // 基本属性 images: images ? images.join(",") : '', // 图片 ownSpec: JSON.stringify(obj) // 特有规格参数 }; }); Object.assign(goodsParams, { cid1, cid2, cid3, // 商品分类 skus // sku列表 }); goodsParams.spuDetail.genericSpec = JSON.stringify(specs); goodsParams.spuDetail.specialSpec = JSON.stringify(specTemplate); // 提交到后台 this.$http({ method: this.isEdit ? "put" : "post", url: "/item/goods", data: goodsParams }) .then(() => { // 成功,关闭窗口 this.$emit("close"); // 提示成功 this.$message.success("保存成功了"); }) .catch(() => { this.$message.error("保存失败!"); }); }
点击提交,查看控制台提交的数据格式:
-
brandId:品牌id
-
cid1、cid2、cid3:商品分类id
-
subTitle:副标题
-
title:标题
-
spuDetail:是一个json对象,代表商品详情表数据
-
afterService:售后服务
-
description:商品描述
-
packingList:包装列表
-
specialSpec:sku规格属性模板
-
genericSpec:通用规格参数
-
-
skus:spu下的所有sku数组,元素是每个sku对象:
-
title:标题
-
images:图片
-
price:价格
-
stock:库存
-
ownSpec:特有规格参数
-
-
7.后台实现
(1)实体类
SPU和SpuDetail实体类已经添加过,添加Sku和Stock对象:
<1>Sku
package lucky.leyou.item.domain; import javax.persistence.*; import java.util.Date; @Table(name = "tb_sku") public class Sku { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long spuId; private String title; private String images; private Long price; private String ownSpec;// 商品特殊规格的键值对 private String indexes;// 商品特殊规格的下标 private Boolean enable;// 是否有效,逻辑删除用 private Date createTime;// 创建时间 private Date lastUpdateTime;// 最后修改时间 // @Transient表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性. 如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient, @Transient private Integer stock;// 库存 public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getSpuId() { return spuId; } public void setSpuId(Long spuId) { this.spuId = spuId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getImages() { return images; } public void setImages(String images) { this.images = images; } public Long getPrice() { return price; } public void setPrice(Long price) { this.price = price; } public String getOwnSpec() { return ownSpec; } public void setOwnSpec(String ownSpec) { this.ownSpec = ownSpec; } public String getIndexes() { return indexes; } public void setIndexes(String indexes) { this.indexes = indexes; } public Boolean getEnable() { return enable; } public void setEnable(Boolean enable) { this.enable = enable; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getLastUpdateTime() { return lastUpdateTime; } public void setLastUpdateTime(Date lastUpdateTime) { this.lastUpdateTime = lastUpdateTime; } public Integer getStock() { return stock; } public void setStock(Integer stock) { this.stock = stock; } }
<2>Stock
package lucky.leyou.item.domain; import javax.persistence.Id; import javax.persistence.Table; @Table(name = "tb_stock") public class Stock { @Id private Long skuId; private Integer seckillStock;// 秒杀可用库存 private Integer seckillTotal;// 已秒杀数量 private Integer stock;// 正常库存 public Long getSkuId() { return skuId; } public void setSkuId(Long skuId) { this.skuId = skuId; } public Integer getSeckillStock() { return seckillStock; } public void setSeckillStock(Integer seckillStock) { this.seckillStock = seckillStock; } public Integer getSeckillTotal() { return seckillTotal; } public void setSeckillTotal(Integer seckillTotal) { this.seckillTotal = seckillTotal; } public Integer getStock() { return stock; } public void setStock(Integer stock) { this.stock = stock; } }
(2)Mapper
利用通用mapper,可以直接调用通用mapper工具包封装的方法直接操作数据库,避免了sql语句的编写。
注意:SkuMapper、StockMapper继承通用Mapper后,也可以自定义方法用来操作数据库表
<1>SkuMapper
package lucky.leyou.item.mapper; import lucky.leyou.item.domain.Sku; import tk.mybatis.mapper.common.Mapper; public interface SkuMapper extends Mapper<Sku> { }
<2>StockMapper
package lucky.leyou.item.mapper; import lucky.leyou.item.domain.Stock; import tk.mybatis.mapper.common.Mapper; public interface StockMapper extends Mapper<Stock> { }
(3)Controller
<1>修改SpuBo类
Spu的json格式的对象,spu中包含spuDetail和Sku集合。这里我们该怎么接收?我们之前定义了一个SpuBo对象,作为业务对象。这里也可以用它,不过需要再扩展spuDetail和skus字段:
package lucky.leyou.item.bo; import lucky.leyou.item.domain.Sku; import lucky.leyou.item.domain.Spu; import lucky.leyou.item.domain.SpuDetail; import java.util.List; /** * Bo为business object 业务对象 * SpuBo这个类用来封装分页查询商品的结果集,继承了spu这个类,并扩展出了cname,和bname属性 */ public class SpuBo extends Spu { private String cname; private String bname; private SpuDetail spuDetail; private List<Sku> skus; public SpuDetail getSpuDetail() { return spuDetail; } public void setSpuDetail(SpuDetail spuDetail) { this.spuDetail = spuDetail; } public List<Sku> getSkus() { return skus; } public void setSkus(List<Sku> skus) { this.skus = skus; } public String getCname() { return cname; } public void setCname(String cname) { this.cname = cname; } public String getBname() { return bname; } public void setBname(String bname) { this.bname = bname; } }
<2>在GoodsController中添加新增商品的方法
/** * 商品保存 * @param spuBo 注意:利用@RequestBody注解接收json数据 * @return */ @PostMapping("goods") public ResponseEntity<Void> saveGoods(@RequestBody SpuBo spuBo){ this.goodsService.saveGoods(spuBo); return ResponseEntity.status(HttpStatus.CREATED).build(); }
注意:通过@RequestBody注解来接收Json请求
(4)service
这里的逻辑比较复杂,我们除了要对SPU新增以外,还要对SpuDetail、Sku、Stock进行保存
package lucky.leyou.item.service.impl; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import lucky.leyou.common.domain.PageResult; import lucky.leyou.item.bo.SpuBo; import lucky.leyou.item.domain.Spu; import lucky.leyou.item.domain.SpuDetail; import lucky.leyou.item.domain.Stock; import lucky.leyou.item.mapper.*; import lucky.leyou.item.service.ICategoryService; import lucky.leyou.item.service.IGoodsService; import org.apache.commons.lang.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Example; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; @Service public class GoodsServiceImpl implements IGoodsService { @Autowired private SpuMapper spuMapper; @Autowired private SpuDetailMapper spuDetailMapper; @Autowired private BrandMapper brandMapper; @Autowired private ICategoryService categoryService; @Autowired private SkuMapper skuMapper; @Autowired private StockMapper stockMapper; @Override public PageResult<SpuBo> querySpuBoByPage(String key, Boolean saleable, Integer page, Integer rows) { Example example = new Example(Spu.class); Example.Criteria criteria = example.createCriteria(); //查询条件 // 01 添加文本框中用户输入的搜索条件 if (StringUtils.isNotBlank(key)) { //注意:criteria.andLike该方法的参数1是数据库表的字段名,参数2为模糊查询的表达式 criteria.andLike("title", "%" + key + "%"); } //02 添加上下架的过滤条件 if (saleable != null) { criteria.andEqualTo("saleable", saleable); } // 03 分页条件 PageHelper.startPage(page, rows); // 04 执行查询,获取spu集合 List<Spu> spus = this.spuMapper.selectByExample(example); PageInfo<Spu> pageInfo = new PageInfo<>(spus); List<SpuBo> spuBos = new ArrayList<>(); //05 spu集合转化为spubo集合 //java8 foreach循环 spus.forEach(spu->{ SpuBo spuBo = new SpuBo(); // copy共同属性的值到新的对象 BeanUtils.copyProperties(spu, spuBo); // 查询分类名称 List<String> names = this.categoryService.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3())); spuBo.setCname(StringUtils.join(names, "/")); //StringUtils.join将集合元素用指定的分隔符连接成字符串 // 查询品牌的名称 spuBo.setBname(this.brandMapper.selectByPrimaryKey(spu.getBrandId()).getName()); spuBos.add(spuBo); }); //06 利用PageResult的构造方法返回PageResult对象 return new PageResult<>(pageInfo.getTotal(), spuBos); } /** * 新增商品 * @param spuBo */ @Override @Transactional //添加事务 public void saveGoods(SpuBo spuBo) { // 01 新增spu // 设置默认字段 spuBo.setId(null); spuBo.setSaleable(true); //设置是否可售 spuBo.setValid(true); spuBo.setCreateTime(new Date()); //设置创建时间 spuBo.setLastUpdateTime(spuBo.getCreateTime()); //设置更新时间 this.spuMapper.insertSelective(spuBo); // 02 新增spuDetail SpuDetail spuDetail = spuBo.getSpuDetail(); spuDetail.setSpuId(spuBo.getId()); this.spuDetailMapper.insertSelective(spuDetail); saveSkuAndStock(spuBo); } private void saveSkuAndStock(SpuBo spuBo) { spuBo.getSkus().forEach(sku -> { // 03 新增sku sku.setSpuId(spuBo.getId()); sku.setCreateTime(new Date()); sku.setLastUpdateTime(sku.getCreateTime()); this.skuMapper.insertSelective(sku); // 04 新增库存 Stock stock = new Stock(); stock.setSkuId(sku.getId()); stock.setStock(sku.getStock()); this.stockMapper.insertSelective(stock); }); } }
8.最终效果图
数据库表数据:
spu表
spu_detail表