(10)商品管理
新增商品
- 初始化数据
- 根据cid查询品牌
- 根据cid查询规格参数
- 新增spu
- 新增spuDetail
- sku
- stock
1.初始化数据
1.1.根据cid查询品牌
1.1.1.前端页面
所以页面编写了watch函数,监控商品分类的变化,每当商品分类值有变化,就会发起请求,查询品牌列表:
选择商品分类后,可以看到请求发起:
接下来,我们只要编写后台接口,根据商品分类id,查询对应品牌即可。
1.1.2.后台接口
页面需要去后台查询品牌信息,我们自然需要提供:
请求方式:GET
请求路径:/brand/cid/{cid}
请求参数:cid
响应数据:品牌集合
BrandController
@GetMapping("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);
}
BrandService
public List<Brand> queryBrandsByCid(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}")
List<Brand> selectBrandByCid(Long cid);
1.1.3.效果
1.2.根据cid查询规格参数
1.2.1.Vue的Watch商品分类变化
规格参数的查询我们之前也已经编写过接口,因为商品规格参数也是与商品分类绑定,所以需要在商品分类变化后去查询,我们也是通过watch监控来实现:
可以看到这里是根据商品分类id查询规格参数:SpecParam。我们之前写过一个根据gid(分组id)来查询规格参数的接口,我们接下来完成根据分类id查询规格参数。
我们在原来的根据 gid(规格组id)查询规格参数的接口上,添加一个参数:cid,即商品分类id。
等一下, 考虑到以后可能还会根据是否搜索、是否为通用属性等条件过滤,我们多添加几个过滤条件:
/** * 根据gid查询规格参数 * @param gid * @return */ public List<SpecParam> queryParams(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查询。
测试:
刷新页面测试:
2.提交保存(新增spu、新增spuDetail、新增sku、新增stock)
2.1.前端页面的数据处理
在sku列表的下方,有一个提交按钮:
并且绑定了点击事件:
点击提交,查看控制台提交的数据格式:
整体是一个json格式数据,包含Spu表所有数据:
-
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:特有规格参数
-
indexes:特有规格参数的下标
-
2.2.
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;// 正常库存 }
2.2.2.
结合浏览器页面控制台,可以发现:
请求方式:POST
请求路径:/goods
请求参数:Spu的json格式的对象,spu中包含spuDetail和Sku集合。这里我们该怎么接收?我们之前定义了一个SpuBo对象,作为业务对象。这里也可以用它,不过需要再扩展spuDetail和skus字段:
public class SpuBo extends Spu { String cname;// 商品分类名称 String bname;// 品牌名称 SpuDetail spuDetail;// 商品详情 List<Sku> skus;// sku列表 }
返回类型:无
在GoodsController中添加新增商品的代码:
@PostMapping("goods") public ResponseEntity<Void> saveGoods(@RequestBody SpuBo spuBo){ this.goodsService.saveGoods(spuBo); return ResponseEntity.status(HttpStatus.CREATED).build(); }
注意:通过@RequestBody注解来接收Json请求
2.2.3.
/** * 新增商品 * @param spuBo */ @Transactional public 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); }); }
2.2.4.
更新商品
- 回显
- 根据spuId查询spuDetail
- 根据spuId查询skus
- 先删除stock
- 然后删除sku
- 新增sku和stock
- 更新spu和spuDetail
1.回显
1.1.
点击这个按钮,就会打开一个商品编辑窗口,我们看下它所绑定的点击事件:(在item/Goods.vue)
对应的方法:
可以看到这里发起了两个请求,在查询商品详情和sku信息。
因为在商品列表页面,只有spu的基本信息:id、标题、品牌、商品分类等。比较复杂的商品详情(spuDetail)和sku信息都没有,编辑页面要回显数据,就需要查询这些内容。
因此,接下来我们就编写后台接口,提供查询服务接口。
1.2.根据spuId查询spuDetail
GoodsController
需要分析的内容:
-
请求方式:GET
-
请求路径:/spu/detail/{id}
-
请求参数:id,应该是spu的id
-
返回结果:SpuDetail对象
@GetMapping("spu/detail/{spuId}") public ResponseEntity<SpuDetail> querySpuDetailBySpuId(@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); }
测试
1.3.根据spuId查询skus
分析
-
-
请求路径:/sku/list
-
请求参数:id,应该是spu的id
-
返回结果:sku的集合
GoodsController
@GetMapping("sku/list") public ResponseEntity<List<Sku>> querySkusBySpuId(@RequestParam("id")Long spuId){ List<Sku> 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 List<Sku> querySkusBySpuId(Long spuId) { Sku sku = new Sku(); sku.setSpuId(spuId); List<Sku> skus = this.skuMapper.select(sku); skus.forEach(s -> { Stock stock = this.stockMapper.selectByPrimaryKey(s.getId()); s.setStock(stock.getStock()); }); return skus; }
测试:
页面回显
随便点击一个编辑按钮,发现数据回显完成:
2.页面提交(先删除stock、然后删除sku、新增sku和stock、更新spu和spuDetail)
2.1.前端页面发送请求
这里的保存按钮与新增其实是同一个,因此提交的逻辑也是一样的,这里不再赘述。
随便修改点数据,然后点击保存,可以看到浏览器已经发出请求:
2.2.后台页面实现接口
-
-
请求路径:/
-
请求参数:Spu对象
-
返回结果:无
@PutMapping("goods") public ResponseEntity<Void> updateGoods(@RequestBody SpuBo spuBo){ this.goodsService.updateGoods(spuBo); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); }
spu数据可以修改,但是SKU数据无法修改,因为有可能之前存在的SKU现在已经不存在了,或者以前的sku属性都不存在了。比如以前内存有4G,现在没了。
因此这里直接删除以前的SKU,然后新增即可。
代码:
@Transactional public void update(SpuBo spu) { // 查询以前sku List<Sku> skus = this.querySkuBySpuId(spu.getId()); // 如果以前存在,则删除 if(!CollectionUtils.isEmpty(skus)) { List<Long> 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(spu); // 更新spu spu.setLastUpdateTime(new Date()); spu.setCreateTime(null); spu.setValid(null); spu.setSaleable(null); this.spuMapper.updateByPrimaryKeySelective(spu); // 更新spu详情 this.spuDetailMapper.updateByPrimaryKeySelective(spu.getSpuDetail()); }
搭建前台系统
- live-server --port=9002
- 配置host文件
- 配置nginx
- commonjs:getUrlParam、formatprice、formatDate、stringify、parse
live-server --port=9002
live-server --port=9002
配置host文件
127.0.0.1 www.leyou.com
配置nginx
server { listen 80; server_name www.leyou.com; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { proxy_pass http://127.0.0.1:9002; proxy_connect_timeout 600; proxy_read_timeout 600; } }
commonjs:getUrlParam、formatprice、formatDate、stringify、parse
为了方便后续的开发,我们在前台系统中定义了一些工具,放在了common.js中:
部分代码截图:
首先对axios进行了一些全局配置,请求超时时间,请求的基础路径,是否允许跨域操作cookie等
定义了对象 ly ,也叫leyou,包含了下面的属性:
-
getUrlParam(key):获取url路径中的参数
-
http:axios对象的别名。以后发起ajax请求,可以用ly.http.get()
-
store:localstorage便捷操作,后面用到再详细说明
-
formatPrice:格式化价格,如果传入的是字符串,则扩大100被并转为数字,如果传入是数字,则缩小100倍并转为字符串
-
formatDate(val, pattern):对日期对象val按照指定的pattern模板进行格式化
-
stringify:将对象转为参数字符串
-
parse:将参数字符串变为js对象