进入商品列表,点击新增商品
出现一个弹窗
里面把商品的数据分为了 4 部分来填写:
基本信息:主要是一些简单的文本数据,包含了 SPU 和 SpuDetail 的部分数据
商品描述:是 SpuDetail 中的 description 属性,数据较多,所以单独放一个页面
规格参数:商品规格信息,对应 SpuDetail 中的 genericSpec 属性
SKU 属性:SPU 下的所有 SKU 信息
商品分类的前端
商品分类的查询之前做过了,所以这里点击就能选择商品分类
品牌选择前端
选择分类后,我们需要这个分类下的所有品牌,所以会发送一个根据分类 id 查询品牌的请求
我们找到前端请求品牌数据的代码
由此可以得知:
后台接口
在 BrandController 中添加 queryBrandsByCid 方法
/**
* 根据分类 id 查询品牌信息
* @param cid
* @return
*/
@GetMapping("/cid/{cid}")
public ResponseEntity<List<Brand>> queryBrandsByCid(@PathVariable("cid") Long cid) {
List<Brand> brands = brandService.queryBrandsByCid(cid);
if(CollectionUtils.isEmpty(brands)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(brands);
}
在 BrandService 中添加 queryBrandsByCid 方法
/**
* 根据分类 id 查询品牌信息
* @param cid
* @return
*/
public List<Brand> queryBrandsByCid(Long cid) {
List<Brand> brands = brandMapper.queryBrandsByCid(cid);
return brands;
}
在 BrandMapper 中添加 queryBrandsByCid 方法
/**
* 根据分类 id 查询品牌信息
* @param cid
* @return
*/
@Select("SELECT * FROM tb_brand WHERE id IN (SELECT brand_id FROM tb_category_brand WHERE category_id = #{cid})")
List<Brand> queryBrandsByCid(Long cid);
测试
成功得到手机分类下的所有品牌
剩下的都是普通的文本框,直接填写即可
商品描述经常需要图文并茂,甚至视频,所以这里使用了一个富文本编辑器 Vue-Quill-Editor,它支持文字、图片、视频等功能。
规格参数前端
选择分类后,我们需要这个分类下的所有规格参数,所以会发送一个根据分类 id 查询规格参数的请求
我们找到前端请求规格参数数据的代码
由此可以得知:
后台接口
前面在 SpecificationController 中已经写过一个 querySpecParams 方法,路径为 spec/params,参数为规格组 id,如下
/**
* 根据条件查询规格参数
*
* @param gid
* @return
*/
@GetMapping("/params")
public ResponseEntity<List<SpecParam>> querySpecParams(@RequestParam("gid") Long gid) {
List<SpecParam> params = specificationService.querySpecParams(gid);
if (CollectionUtils.isEmpty(params)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(params);
}
所以我们就不用再写新的方法了,直接在 querySpecParams 方法中增加分类 id 参数。考虑到以后可能还会根据是否搜索、是否为通用属性等条件过滤,我们多添加几个过滤条件
/**
* 根据条件查询规格参数
*
* @param gid
* @return
*/
@GetMapping("/params")
public ResponseEntity<List<SpecParam>> querySpecParams(
@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 = specificationService.querySpecParams(gid, cid, generic, searching);
if (CollectionUtils.isEmpty(params)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(params);
}
修改 SpecificationService 中的 querySpecParams 方法
/**
* 根据条件查询规格参数
* @param gid
* @return
*/
public List<SpecParam> querySpecParams(Long gid,Long cid, Boolean generic, Boolean searching) {
SpecParam specParam = new SpecParam();
specParam.setGroupId(gid);
specParam.setCid(cid);
specParam.setGeneric(generic);
specParam.setSearching(searching);
List<SpecParam> params = specParamMapper.select(specParam);
return params;
}
测试
成功得到规格参数,这部分规格参数是 sku 通用的属性,即 tb_spec_param 中 generic 为 1 的字段。
成功得到 sku 属性,这部分规格参数是 sku 特有的属性,即 tb_spec_param 中 generic 为 0 的字段。
当我们填写一些 sku 属性后,会在页面下方生成一个 sku 表格,可以添加价格、库存、是否启用等
填写完所有商品信息后,只剩下最后一步了,那就是提交商品信息
点击提交商品信息后,可以看到发送给了一个提交商品信息的请求
再来看看请求参数
整体是一个 JSON 格式数据,包含了 tb_spu、tb_spu_detail、tb_sku、tb_stock 四张表的数据
我们找到前端提交商品信息请求的代码
由此可以得知:
实体类
在 leyou-item-interface 项目中添加实体类 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;// 库存
// getter、setter、toString 方法省略
}
在 leyou-item-interface 项目中添加实体类 Stock
@Table(name = "tb_stock")
public class Stock {
@Id
private Long skuId;
private Integer seckillStock;// 秒杀可用库存
private Integer seckillTotal;// 已秒杀数量
private Integer stock;// 正常库存
// getter、setter、toString 方法省略
}
在实体类 SpuBo 中添加 spuDetail、skus 属性,用来封装前端提交过来的商品信息
public class SpuBo extends Spu{
private String cname;
private String bname;
private SpuDetail spuDetail;
private List<Sku> skus;
// getter、setter、toString 方法省略
}
Mapper
在 leyou-item-service 项目中添加 SkuMapper
public interface SkuMapper extends Mapper<Sku> {
}
在 leyou-item-service 项目中添加 StockMapper
public interface StockMapper extends Mapper<Stock> {
}
Controller
在 SpuController 中添加 saveSpu 方法
/**
* 新增商品
* @param spuBo
* @return
*/
@PostMapping
public ResponseEntity<Void> saveSpu(@RequestBody SpuBo spuBo) {
spuService.saveSpu(spuBo);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
Service
在 SpuService 中添加 saveSpu 方法
/**
* 新增商品
* @param spuBo
* @return
*/
@Transactional
public void saveSpu(SpuBo spuBo) {
// 先新增 Spu
spuBo.setId(null);
spuBo.setSaleable(true);
spuBo.setValid(true);
spuBo.setCreateTime(new Date());
spuBo.setLastUpdateTime(spuBo.getCreateTime());
spuMapper.insertSelective(spuBo);
// 再新增 SpuDetail
SpuDetail spuDetail = spuBo.getSpuDetail();
spuDetail.setSpuId(spuBo.getId());
spuDetailMapper.insertSelective(spuDetail);
// 再新增 Sku
List<Sku> skus = spuBo.getSkus();
for (Sku sku : skus) {
sku.setId(null);
sku.setSpuId(spuBo.getId());
sku.setCreateTime(new Date());
sku.setLastUpdateTime(sku.getCreateTime());
skuMapper.insertSelective(sku);
// 新增 Stock
Stock stock = new Stock();
stock.setSkuId(sku.getId());
stock.setStock(sku.getStock());
stockMapper.insertSelective(stock);
}
}
看到新增的商品了
商品修改需要以下步骤:
点击修改按钮就会把商品信息回显到页面上
我们找到前端商品信息回显请求的代码
由此可知,发送了两个请求:
Controller
在 SpuController 中添加 querySpuDetailBySpuId 方法
/**
* 通过 spuId 查询 SpuDetail
* @param spuId
* @return
*/
@GetMapping("/detail/{spuId}")
public ResponseEntity<SpuDetail> querySpuDetailBySpuId(@PathVariable("spuId") Long spuId) {
SpuDetail spuDetail = spuService.querySpuDetailBySpuId(spuId);
if (spuDetail == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(spuDetail);
}
在 SpuController 中添加 querySkusBySpuId 方法
/**
* 通过 spuId 查询 Sku 集合
* @param spuId
* @return
*/
@GetMapping("/sku/list")
public ResponseEntity<List<Sku>> querySkusBySpuId(@RequestParam("id") Long spuId) {
List<Sku> skus = spuService.querySkusBySpuId(spuId);
if(CollectionUtils.isEmpty(skus)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(skus);
}
Service
在 SpuService 中添加 querySpuDetailBySpuId 方法
/**
* 通过 spuId 查询 SpuDetail
* @param spuId
* @return
*/
public SpuDetail querySpuDetailBySpuId(Long spuId) {
SpuDetail spuDetail = spuDetailMapper.selectByPrimaryKey(spuId);
return spuDetail;
}
在 SpuService 中添加 querySkusBySpuId 方法
/**
* 通过 spuId 查询 Sku 集合
* @param spuId
* @return
*/
public List<Sku> querySkusBySpuId(Long spuId) {
Sku sku = new Sku();
sku.setSpuId(spuId);
List<Sku> skus = skuMapper.select(sku);
for (Sku sku1 : skus) {
Stock stock = stockMapper.selectByPrimaryKey(sku1.getId());
sku1.setStock(stock.getStock());
}
return skus;
}
商品信息回显成功
我们找到前端提交商品信息请求的代码,商品新增时是 POST 请求,商品修改是 PUT 请求
由此可以得知:
Controller
在 SpuController 中添加 updateSpu 方法
/**
* 更新商品
* @param spuBo
* @return
*/
@PutMapping
public ResponseEntity<Void> updateSpu(@RequestBody SpuBo spuBo) {
spuService.updateSpu(spuBo);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
Service
在 SpuService 中添加 updateSpu 方法
/**
* 更新商品
* @param spuBo
* @return
*/
@Transactional
public void updateSpu(SpuBo spuBo) {
// 获取要删除的 Sku
Sku sku = new Sku();
sku.setSpuId(spuBo.getId());
List<Sku> skus = skuMapper.select(sku);
// 删除原来的 Stock
for (Sku sku1 : skus) {
Stock stock = new Stock();
stock.setSkuId(sku1.getId());
stockMapper.delete(stock);
}
// 删除原来的 Sku
skuMapper.delete(sku);
// 新增 Sku 和 Stock
saveSkuAndStock(spuBo);
// 更新 Spu
spuBo.setCreateTime(null);
spuBo.setLastUpdateTime(new Date());
spuBo.setValid(null);
spuBo.setSaleable(null);
spuMapper.updateByPrimaryKeySelective(spuBo);
// 更新 SpuDetail
spuDetailMapper.updateByPrimaryKeySelective(spuBo.getSpuDetail());
}
新增 Sku 和 Stock 的代码之前写过了,为了避免代码冗余,所以抽取成 saveSkuAndStock 方法
/**
* 新增 Sku 和 Stock
* @param spuBo
*/
private void saveSkuAndStock(SpuBo spuBo) {
// 再新增 Sku
List<Sku> skus = spuBo.getSkus();
for (Sku sku : skus) {
sku.setId(null);
sku.setSpuId(spuBo.getId());
sku.setCreateTime(new Date());
sku.setLastUpdateTime(sku.getCreateTime());
skuMapper.insertSelective(sku);
// 新增 Stock
Stock stock = new Stock();
stock.setSkuId(sku.getId());
stock.setStock(sku.getStock());
stockMapper.insertSelective(stock);
}
}
把小米 10 修改为小米 11
后台系统的内容暂时告一段落,有了商品,接下来我们就要在页面展示商品,给用户提供浏览和购买的入口,那就是我们的门户系统。
门户系统面向的是用户,安全性很重要,而且搜索引擎对于单页应用并不友好。因此我们的门户系统不再采用与后台系统类似的 SPA(单页应用),而是采用多页应用,不过依旧是前后端分离。
将 leyou-portal 解压并复制到工程中
webpack 打包多页应用配置比较繁琐,所以我们使用另一种热部署的方式:live-server
live-server 是一款带有热加载功能的小型开发服务器。用它来展示你的 HTML / JavaScript / CSS,但不能用于部署最终的网站。
在 terminal 中输入以下命令:
npm install -g live-server
在 terminal 中输入以下命令:
live-server --port=9002
启动成功
现在访问:http://127.0.0.1:9002/
域名访问:http://www.leyou.com
接下来我们就来实现域名访问:
127.0.0.1 www.leyou.com
2.在 nginx.conf 添加以下配置,实现将 www.leyou.com 反向代理到 http://127.0.0.1:9002
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;
}
}
重新加载 nginx
nginx.exe -s reload
成功通过域名访问