本文共 13048 字,大约阅读时间需要 43 分钟。
接下来,我们实现商品管理的页面,先看下我们要实现的效果:
可以看出整体是一个table,然后有新增按钮。是不是跟昨天写品牌管理很像?
先看整体页面结构(Goods.vue):
并且在Vue实例挂载后就会发起查询(mounted调用getDataFromServer方法初始化数据):
我们刷新页面,可以看到浏览器发起已经发起了查询商品数据的请求:
页面已经准备好,接下来在后台提供分页查询SPU的功能。
实体类
在leyou-item-interface工程中添加实体类:
SPU
@Table(name = "tb_spu")public class Spu { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long brandId; private Long cid1;// 1级类目 private Long cid2;// 2级类目 private Long cid3;// 3级类目 private String title;// 标题 private String subTitle;// 子标题 private Boolean saleable;// 是否上架 private Boolean valid;// 是否有效,逻辑删除用 private Date createTime;// 创建时间 private Date lastUpdateTime;// 最后修改时间 // 省略getter和setter}
SPU详情
@Table(name="tb_spu_detail")public class SpuDetail { @Id private Long spuId;// 对应的SPU的id private String description;// 商品描述 private String specialSpec;// 商品特殊规格的名称及可选值模板 private String genericSpec;// 商品的全局规格属性 private String packingList;// 包装清单 private String afterService;// 售后服务 // 省略getter和setter}
mapper
public interface SpuMapper extends Mapper{ }
controller
先分析:
请求方式:GET
请求路径:/spu/page
请求参数:
返回结果:商品SPU的分页信息。
要注意,页面展示的是商品分类和品牌名称,而数据库中保存的是id,怎么办?
我们可以新建一个类,继承SPU,并且拓展cname和bname属性,写到leyou-item-interface
public class SpuBo extends Spu { String cname;// 商品分类名称 String bname;// 品牌名称 // 略 。。}
编写controller代码:
我们把与商品相关的一切业务接口都放到一起,起名为GoodsController,业务层也是这样
@Controllerpublic class GoodsController { @Autowired private GoodsService goodsService; @GetMapping("spu/page") public ResponseEntity> querySpuBoByPage( @RequestParam(value = "key", required = false)String key, @RequestParam(value = "saleable", required = false)Boolean saleable, @RequestParam(value = "page", defaultValue = "1")Integer page, @RequestParam(value = "rows", defaultValue = "5")Integer rows ){ PageResult pageResult = this.goodsService.querySpuBoByPage(key, saleable, page, rows); if(CollectionUtils.isEmpty(pageResult.getItems())){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(pageResult); }}
service
所有商品相关的业务(包括SPU和SKU)放到一个业务下:GoodsService。
@Servicepublic class GoodsService { @Autowired private SpuMapper spuMapper; @Autowired private CategoryService categoryService; @Autowired private BrandMapper brandMapper; public PageResultquerySpuBoByPage(String key, Boolean saleable, Integer page, Integer rows) { Example example = new Example(Spu.class); Example.Criteria criteria = example.createCriteria(); // 搜索条件 if (StringUtils.isNotBlank(key)) { criteria.andLike("title", "%" + key + "%"); } if (saleable != null) { criteria.andEqualTo("saleable", saleable); } // 分页条件 PageHelper.startPage(page, rows); // 执行查询 List spus = this.spuMapper.selectByExample(example); PageInfo pageInfo = new PageInfo<>(spus); List spuBos = new ArrayList<>(); spus.forEach(spu->{ SpuBo spuBo = new SpuBo(); // copy共同属性的值到新的对象 BeanUtils.copyProperties(spu, spuBo); // 查询分类名称 List names = this.categoryService.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3())); spuBo.setCname(StringUtils.join(names, "/")); // 查询品牌的名称 spuBo.setBname(this.brandMapper.selectByPrimaryKey(spu.getBrandId()).getName()); spuBos.add(spuBo); }); return new PageResult<>(pageInfo.getTotal(), spuBos); }}
页面需要商品的分类名称需要在这里查询,因此要额外提供查询分类名称的功能,
在CategoryService中添加功能:
public ListqueryNamesByIds(List ids) { List list = this.categoryMapper.selectByIdList(ids); List names = new ArrayList<>(); for (Category category : list) { names.add(category.getName()); } return names; // return list.stream().map(category -> category.getName()).collect(Collectors.toList());}
mapper的selectByIdList方法是来自于通用mapper。不过需要我们在mapper上继承一个通用mapper接口:
public interface CategoryMapper extends Mapper, SelectByIdListMapper { }
刷新页面,查看效果:
基本与预览的效果一致,OK!
cid1
,cid2
,cid3
属性brandId
属性title
属性subTitle
属性afterService
属性packingList
属性description
属性,数据较多,所以单独放一个页面genericSpec
属性对应到页面中的四个stepper-content
:
页面需要去后台查询品牌信息,我们自然需要提供:
请求方式:GET
请求路径:/brand/cid/{cid}
请求参数:cid
响应数据:品牌集合
BrandController
@GetMapping("cid/{cid}")public ResponseEntity
> queryBrandsByCid(@PathVariable("cid")Long cid){ List brands = this.brandService.queryBrandsByCid(cid); if (CollectionUtils.isEmpty(brands)) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(brands);}
BrandService
public ListqueryBrandsByCid(Long cid) { return this.brandMapper.selectBrandByCid(cid);}
BrandMapper
根据分类查询品牌有中间表,需要自己编写Sql:
@Select("SELECT b.* from tb_brand b INNER JOIN tb_category_brand cb on b.id=cb.brand_id where cb.category_id=#{cid}")ListselectBrandByCid(Long cid);
商品描述信息比较复杂,而且图文并茂,甚至包括视频。
这样的内容,一般都会使用富文本编辑器一款支持Vue的富文本编辑器:vue-quill-editor
。
Vue-Quill-Editor是一个基于Quill的富文本编辑器:
使用指南
使用非常简单:已经在项目中集成。以下步骤不需操作,仅供参考
第一步:安装,使用npm命令:
npm install vue-quill-editor --save
第二步:加载,在js中引入:
全局引入:
import Vue from 'vue'import VueQuillEditor from 'vue-quill-editor'const options = { }; /* { default global options } */Vue.use(VueQuillEditor, options); // options可选
局部引入:
import 'quill/dist/quill.core.css'import 'quill/dist/quill.snow.css'import 'quill/dist/quill.bubble.css'import { quillEditor} from 'vue-quill-editor'var vm = new Vue({ components:{ quillEditor }})
我们这里采用局部引用:
第三步:页面使用:
自定义的富文本编辑器
不过这个组件有个小问题,就是图片上传的无法直接上传到后台,因此我们对其进行了封装,支持了图片的上传。
可以看到这里是根据商品分类id查询规格参数:SpecParam。我们之前写过一个根据gid(分组id)来查询规格参数的接口,我们接下来完成根据分类id查询规格参数。
改造查询规格参数接口
我们在原来的根据 gid(规格组id)查询规格参数的接口上,添加一个参数:cid,即商品分类id。
等一下, 考虑到以后可能还会根据是否搜索、是否为通用属性等条件过滤,我们多添加几个过滤条件:
@GetMapping("params") public ResponseEntity
> 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 params = this.specificationService.queryParams(gid, cid, generic, searching); if (CollectionUtils.isEmpty(params)){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(params); }
改造SpecificationService:
/** * 根据gid查询规格参数 * @param gid * @return */ public ListqueryParams(Long gid, Long cid, Boolean generic, Boolean searching) { SpecParam record = new SpecParam(); record.setGroupId(gid); record.setCid(cid); record.setGeneric(generic); record.setSearching(searching); return this.specParamMapper.select(record); }
如果param中有属性为null,则不会把属性作为查询条件,因此该方法具备通用性,即可根据gid查询,也可根据cid查询。
实体类
SPU和SpuDetail实体类已经添加过,添加Sku和Stock(库存)对象:![]()
Sku
@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 private Integer stock;// 库存}
注意:这里保存了一个库存字段,在数据库中是另外一张表保存的,方便查询。
Stock
@Table(name = "tb_stock")public class Stock { @Id private Long skuId; private Integer seckillStock;// 秒杀可用库存 private Integer seckillTotal;// 已秒杀数量 private Integer stock;// 正常库存}
GoodsController
结合浏览器页面控制台,可以发现:
请求方式:POST
请求路径:/goods
请求参数:Spu的json格式的对象,spu中包含spuDetail和Sku集合。这里我们该怎么接收?我们之前定义了一个SpuBo对象,作为业务对象。这里也可以用它,不过需要再扩展spuDetail和skus字段:
public class SpuBo extends Spu { String cname;// 商品分类名称 String bname;// 品牌名称 SpuDetail spuDetail;// 商品详情 Listskus;// sku列表}
在GoodsController中添加新增商品的代码:
@PostMapping("goods")public ResponseEntitysaveGoods(@RequestBody SpuBo spuBo){ this.goodsService.saveGoods(spuBo); return ResponseEntity.status(HttpStatus.CREATED).build();}
注意:通过@RequestBody注解来接收Json请求
GoodsService
这里的逻辑比较复杂,我们除了要对SPU新增以外,还要对SpuDetail、Sku、Stock进行保存
/** * 新增商品 * @param spuBo */@Transactionalpublic void saveGoods(SpuBo spuBo) { // 新增spu // 设置默认字段 spuBo.setId(null); spuBo.setSaleable(true); spuBo.setValid(true); spuBo.setCreateTime(new Date()); spuBo.setLastUpdateTime(spuBo.getCreateTime()); this.spuMapper.insertSelective(spuBo); // 新增spuDetail SpuDetail spuDetail = spuBo.getSpuDetail(); spuDetail.setSpuId(spuBo.getId()); this.spuDetailMapper.insertSelective(spuDetail); saveSkuAndStock(spuBo);}private void saveSkuAndStock(SpuBo spuBo) { spuBo.getSkus().forEach(sku -> { // 新增sku sku.setSpuId(spuBo.getId()); sku.setCreateTime(new Date()); sku.setLastUpdateTime(sku.getCreateTime()); this.skuMapper.insertSelective(sku); // 新增库存 Stock stock = new Stock(); stock.setSkuId(sku.getId()); stock.setStock(sku.getStock()); this.stockMapper.insertSelective(stock); });}
Mapper
都是通用Mapper,略
目录结构:
GoodsController
需要分析的内容:
@GetMapping("spu/detail/{spuId}")public ResponseEntityquerySpuDetailBySpuId(@PathVariable("spuId")Long spuId){ SpuDetail spuDetail = this.goodsService.querySpuDetailBySpuId(spuId); if (spuDetail == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(spuDetail);}
GoodsService
/** * 根据spuId查询spuDetail * @param spuId * @return */public SpuDetail querySpuDetailBySpuId(Long spuId) { return this.spuDetailMapper.selectByPrimaryKey(spuId);}
分析
GoodsController
@GetMapping("sku/list")public ResponseEntity
> querySkusBySpuId(@RequestParam("id")Long spuId){ List skus = this.goodsService.querySkusBySpuId(spuId); if (CollectionUtils.isEmpty(skus)) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(skus);}
GoodsService
需要注意的是,为了页面回显方便,我们一并把sku的库存stock也查询出来
/** * 根据spuId查询sku的集合 * @param spuId * @return */public ListquerySkusBySpuId(Long spuId) { Sku sku = new Sku(); sku.setSpuId(spuId); List skus = this.skuMapper.select(sku); skus.forEach(s -> { Stock stock = this.stockMapper.selectByPrimaryKey(s.getId()); s.setStock(stock.getStock()); }); return skus;}
随便点击一个编辑按钮,发现数据回显完成:
这里的保存按钮与新增其实是同一个,因此提交的逻辑也是一样的,这里不再赘述。
随便修改点数据,然后点击保存,可以看到浏览器已经发出请求:
接下来,我们编写后台,实现修改商品接口。
@PutMapping("goods")public ResponseEntityupdateGoods(@RequestBody SpuBo spuBo){ this.goodsService.updateGoods(spuBo); return ResponseEntity.status(HttpStatus.NO_CONTENT).build();}
spu数据可以修改,但是SKU数据无法修改,因为有可能之前存在的SKU现在已经不存在了,或者以前的sku属性都不存在了。比如以前内存有4G,现在没了。
因此这里直接删除以前的SKU,然后新增即可。
代码:
@Transactionalpublic void updateGoods(SpuBo spu) { // 查询以前sku Listskus = this.querySkuBySpuId(spu.getId()); // 如果以前存在,则删除 if(!CollectionUtils.isEmpty(skus)) { List ids = skus.stream().map(s -> s.getId()).collect(Collectors.toList()); // 删除以前库存 Example example = new Example(Stock.class); example.createCriteria().andIn("skuId", ids); this.stockMapper.deleteByExample(example); // 删除以前的sku Sku record = new Sku(); record.setSpuId(spu.getId()); this.skuMapper.delete(record); } // 新增sku和库存 saveSkuAndStock(spuBo); // 更新spu spu.setLastUpdateTime(new Date()); spu.setCreateTime(null); spu.setValid(null); spu.setSaleable(null); this.spuMapper.updateByPrimaryKeySelective(spu); // 更新spu详情 this.spuDetailMapper.updateByPrimaryKeySelective(spu.getSpuDetail());}
转载地址:http://bxxab.baihongyu.com/