1.SPU和SKU数据结构
规格确定以后,就可以添加商品了,先看下数据库表
1.1.SPU表
1.1.1.表结构
SPU表:
CREATE TABLE `tb_spu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
`sub_title` varchar(255) DEFAULT '' COMMENT '子标题',
`cid1` bigint(20) NOT NULL COMMENT '1级类目id',
`cid2` bigint(20) NOT NULL COMMENT '2级类目id',
`cid3` bigint(20) NOT NULL COMMENT '3级类目id',
`brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
`saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
`valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已删除,1有效',
`create_time` datetime DEFAULT NULL COMMENT '添加时间',
`last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象的商品,比如 iphone8';
与我们前面分析的基本类似,但是似乎少了一些字段,比如商品描述。
我们做了表的垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail
CREATE TABLE `tb_spu_detail` (
`spu_id` bigint(20) NOT NULL,
`description` text COMMENT '商品描述信息',
`specifications` varchar(3000) NOT NULL DEFAULT '' COMMENT '全部规格参数数据',
`spec_template` varchar(1000) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
`packing_list` varchar(1000) DEFAULT '' COMMENT '包装清单',
`after_service` varchar(1000) DEFAULT '' COMMENT '售后服务',
PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这张表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。
需要注意的是这两个字段:specifications和spec_template。
1.1.2.spu中的规格参数
前面讲过规格参数与商品分类绑定,一个分类下的所有SPU具有类似的规格参数。SPU下的SKU可能会有不同的规格参数,因此我们计划是这样:
- SPU中保存全局的规格参数信息。
- SKU中保存特有规格参数。
以手机为例,品牌、操作系统等肯定是全局属性,内存、颜色等肯定是特有属性。
当你确定了一个SPU,比如小米的:红米4X
全局属性举例:
品牌:小米
型号:红米4X
特有属性举例:
颜色:[香槟金, 樱花粉, 磨砂黑]
内存:[2G, 3G]
机身存储:[16GB, 32GB]
来看下我们的 表如何存储这些信息:
1.1.2.1.specifications字段
首先是specifications,其中保存全部规格参数信息,因此也是一个json格式:
整体来看:
展开一组来看
可以看到,与规格参数表中的模板相比,最大的区别就是,这里指定了具体的值,因为商品确定了,其参数值肯定也确定了。
特有属性
刚才看到的是全局属性,那么特有属性在这个字段中如何存储呢?
我们发现特有属性也是有的,但是,注意看这里是不确定具体值的,因为特有属性只有在SKU中才能确定。这里只是保存了options,所有SKU属性的可选项。
在哪里会用到这个字段的值呢,商品详情页的规格参数信息中:
1.1.2.2.spec_template字段
既然specifications已经包含了所有的规格参数,那么为什么又多出了一个spec_template呢?
里面又有哪些内容呢?
来看数据格式:
可以看出,里面只保存了规格参数中的特有属性,而且格式进行了大大的简化,只有属性的key,和待选项。
为什么要冗余保存一份?
因为很多场景下我们只需要查询特有规格属性,如果放在一起,每次查询再去分离比较麻烦。
比如,商品详情页展示可选的规格参数时:
2.页面实现
2.1.页面实现代码
<template>
<v-card>
<v-card-title>
<v-btn color="primary" @click="addGoods">新增商品</v-btn>
<!--搜索框,与search属性关联-->
<v-spacer/>
<v-text-field label="输入关键字搜索" v-model.lazy="search" append-icon="search" hide-details/>
</v-card-title>
<v-divider/>
<v-data-table
:headers="headers"
:items="goodsList"
:search="search"
:pagination.sync="pagination"
:total-items="totalGoods"
:loading="loading"
class="elevation-1"
>
<template slot="items" slot-scope="props">
<td>{{ props.item.id }}</td>
<td class="text-xs-center">{{ props.item.title }}</td>
<td class="text-xs-center">{{props.item.cname}}</td>
<td class="text-xs-center">{{ props.item.bname }}</td>
<td class="justify-center layout">
<v-btn color="info" @click="editGoods(props.item)">编辑</v-btn>
<v-btn color="warning">删除</v-btn>
<v-btn >下架</v-btn>
</td>
</template>
</v-data-table>
<!--弹出的对话框-->
<v-dialog max-width="500" v-model="show" persistent>
<v-card>
<!--对话框的标题-->
<v-toolbar dense dark color="primary">
<v-toolbar-title>{{isEdit ? '修改' : '新增'}}商品</v-toolbar-title>
<v-spacer/>
<!--关闭窗口的按钮-->
<v-btn icon @click="closeWindow"><v-icon>close</v-icon></v-btn>
</v-toolbar>
<!--对话框的内容,表单-->
<v-card-text class="px-5">
<my-goods-form :oldGoods="oldGoods" />
</v-card-text>
</v-card>
</v-dialog>
</v-card>
</template>
<script>
// 导入自定义的表单组件
import MyGoodsForm from './MyGoodsForm'
export default {
name: "my-goods",
data() {
return {
search: '', // 搜索过滤字段
totalGoods: 0, // 总条数
goodsList: [], // 当前页品牌数据
loading: true, // 是否在加载中
pagination: {}, // 分页信息
headers: [
{text: 'id', align: 'center', value: 'id'},
{text: '标题', align: 'center', sortable: false, value: 'title'},
{text: '商品分类', align: 'center', sortable: false, value: 'cname'},
{text: '品牌', align: 'center', value: 'bname', sortable: false,},
{text: '操作', align: 'center', sortable: false}
],
show: false,// 控制对话框的显示
oldGoods: {}, // 即将被编辑的商品信息
isEdit: false, // 是否是编辑
}
},
mounted() { // 渲染后执行
// 查询数据
this.getDataFromServer();
},
watch: {
pagination: { // 监视pagination属性的变化
deep: true, // deep为true,会监视pagination的属性及属性中的对象属性变化
handler() {
// 变化后的回调函数,这里我们再次调用getDataFromServer即可
this.getDataFromServer();
}
},
search: { // 监视搜索字段
handler() {
this.getDataFromServer();
}
}
},
methods: {
getDataFromServer() { // 从服务的加载数的方法。
// 发起请求
this.$http.get("/item/spu/page", {
params: {
key: this.search, // 搜索条件
page: this.pagination.page,// 当前页
rows: this.pagination.rowsPerPage,// 每页大小
sortBy: this.pagination.sortBy,// 排序字段
desc: this.pagination.descending// 是否降序
}
}).then(resp => { // 这里使用箭头函数
this.goodsList = resp.data.items;
this.totalGoods = resp.data.total;
// 完成赋值后,把加载状态赋值为false
this.loading = false;
})
},
addGoods() {
// 修改标记
this.isEdit = false;
// 控制弹窗可见:
this.show = true;
// 把oldBrand变为null
this.oldBrand = null;
},
editGoods(oldGoods){
// 修改标记
this.isEdit = true;
// 控制弹窗可见:
this.show = true;
// 获取要编辑的brand
this.oldGoods = oldGoods;
},
closeWindow(){
// 重新加载数据
this.getDataFromServer();
// 关闭窗口
this.show = false;
}
},
components:{
MyGoodsForm
}
}
</script>
<style scoped>
</style>
主要的改动点:
-
页面的
v-data-table
中的属性绑定修改。items指向goodsList,totalItems指向totalGoods -
页面渲染的字段名修改:字段改成商品的SPU字段:id、title,cname(商品分类名称),bname(品牌名称)
-
data属性修改了以下属性:
- goodsList:当前页商品数据
- totalGoods:商品总数
- headers:头信息,需要修改头显示名称
- oldGoods:准备要修改的商品
-
加载数据的函数:getDataFromServer,请求的路径进行了修改,另外去除了跟排序相关的查询。SPU查询不排序
-
新增商品的事件函数:清除了一些数据查询接口,只保留弹窗
查看效果:
2.3.后台提供接口
页面已经准备好,接下来在后台提供分页查询SPU的功能:
实体类
spu
@Data
@Table(name = "tb_spu")
public class Spu {
@Id
@KeySql(useGeneratedKeys = true)
// @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;// 最后修改时间
@Transient
private String cname;
@Transient
private String bname;
}
spu详情表
@Table(name="tb_spu_detail")
public class SpuDetail {
@Id
private Long spuId;// 对应的SPU的id
private String description;// 商品描述
private String specTemplate;// 商品特殊规格的名称及可选值模板
private String specifications;// 商品的全局规格属性
private String packingList;// 包装清单
private String afterService;// 售后服务
// 省略getter和setter
}
controller
先分析:
-
请求方式:GET
-
请求路径:/spu/page
-
请求参数:
- page:当前页
- rows:每页大小
- key:过滤条件
- saleable:上架或下架
-
返回结果:商品SPU的分页信息。
-
要注意,页面展示的是商品分类和品牌名称,而数据库中保存的是id,怎么办?
我们可以在spu类,拓展cname和bname属性,
@Data @Table(name = "tb_spu") public class Spu { @Transient private String cname; @Transient private String bname; }
-
代码
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
@GetMapping("/spu/page")
public ResponseEntity<PageResult<Spu>> querySpuByPage(
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "sortBy", required = false) String sortBy,
@RequestParam(value = "desc", defaultValue = "false") Boolean desc,
@RequestParam(value = "key",required = false) String key,
@RequestParam(value = "saleable",defaultValue = "true") Boolean saleable
){
return ResponseEntity.ok(goodsService.querySpuByPage(page,rows,saleable,key));
}
}
service
所有商品相关的业务(包括SPU和SKU)放到一个业务下:GoodsService。
@Service
public class GoodsService {
@Autowired
private SpuMapper spuMapper;
@Autowired
private SpuDetailMapper spuDetailMapper;
@Autowired
private CategoryService categoryService;
@Autowired
private BrandService brandService;
public PageResult<Spu> querySpuByPage(Integer page, Integer rows, Boolean saleable, String key) {
//分页
PageHelper.startPage(page,rows);
//过滤
Example example = new Example(Spu.class);
Example.Criteria criterion = example.createCriteria();
//搜索字段过滤
if (StringUtils.isNotBlank(key)){
criterion.andLike("title","%"+key+"%");
}
//上下架过滤
if (saleable != null){
criterion.orEqualTo("saleable",saleable);
}
//排序
example.setOrderByClause("last_update_time DESC");
//查询
List<Spu> spus = spuMapper.selectByExample(example);
//判断
if (CollectionUtils.isEmpty(spus)){
throw new LyException(ExceptionEnum.GOODS_NOT_FOOD);
}
//解析分类和品牌的名称
loadCategoryAndBrandName(spus);
//解析分页结果
PageInfo<Spu> info = new PageInfo<>(spus);
return new PageResult<>(info.getTotal(),spus);
}
private void loadCategoryAndBrandName(List<Spu> spus) {
for (Spu spu:spus){
//处理分类名称
List<String> names = categoryService.queryByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()))
.stream().map(Category::getName).collect(Collectors.toList());
spu.setCname(StringUtils.join(names,"/"));
//处理品牌信息
spu.setBname(brandService.queryById(spu.getBrandId()).getName());
}
}
}
mapper
public interface SpuMapper extends Mapper<Spu> {
}
Category中拓展查询名称的功能
页面需要商品的分类名称需要在这里查询,因此要额外提供查询分类名称的功能,
在CategoryService中添加功能:
public List<Category> queryByIds(List<Long> ids){
List<Category> categories = categoryMapper.selectByIdList(ids);
if (CollectionUtils.isEmpty(categories)){
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
}
return categories;
}
mapper的selectByIDList方法是来自于通用mapper。不过需要我们在mapper上继承一个通用mapper接口:
public interface CategoryMapper extends Mapper<Category>, IdListMapper <Category,Long>{
}