目录
后台功能——品牌管理(后端)
二、后端接口实现
主要就是对数据库的抽插,难点在于和前端页面的联调,接口本身不复杂。
2.1 品牌查询
2.1.1 数据库表
CREATE TABLE `tb_brand` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
`name` varchar(50) NOT NULL COMMENT '品牌名称',
`image` varchar(200) DEFAULT '' COMMENT '品牌图片地址',
`letter` char(1) DEFAULT '' COMMENT '品牌的首字母',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=325400 DEFAULT CHARSET=utf8 COMMENT='品牌表,一个品牌下有多个商品(spu),一对多关系';
品牌和商品分类之间是多对多关系。因此我们有一张中间表,来维护两者间关系:
CREATE TABLE `tb_category_brand` (
`category_id` bigint(20) NOT NULL COMMENT '商品类目id',
`brand_id` bigint(20) NOT NULL COMMENT '品牌id',
PRIMARY KEY (`category_id`,`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分类和品牌的中间表,两者是多对多关系';
但是,你可能会发现,这张表中并没有设置外键约束,似乎与数据库的设计范式不符。为什么这么做?
-
外键会严重影响数据库读写的效率
-
数据删除时会比较麻烦
在电商行业,性能是非常重要的。我们宁可在代码中通过逻辑来维护表关系,也不设置外键。
2.1.2 实体类
@Table(name = "tb_brand")
/**
* @author:li
*
*/
public class Brand implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 品牌名称
*/
private String name;
/**
* 品牌图片
*/
private String image;
private Character letter;
//省略get和set
}
2.1.3 Mapper
/**
* @Author: 98050
* @Time: 2018-08-07 19:15
* @Feature:
*/
@org.apache.ibatis.annotations.Mapper
public interface BrandMapper extends Mapper<Brand> {
}
2.1.4 Controller
编写controller先思考四个问题:
-
请求方式:查询,肯定是Get
-
请求路径:分页查询,/brand/page
-
请求参数:根据我们刚才编写的页面,有分页功能,有排序功能,有搜索过滤功能,因此至少要有5个参数:
-
page:当前页,int
-
rows:每页大小,int
-
sortBy:排序字段,String
-
desc:是否为降序,boolean
-
key:搜索关键词,String
-
-
响应结果:分页结果一般至少需要两个数据
-
total:总条数
-
items:当前页数据
-
totalPage:有些还需要总页数
-
为了方便,需要封装一个类,表示分页结果:
package com.leyou.common.pojo;
import java.util.List;
/**
* @author li
* @param <T>
*/
public class PageResult<T> {
/**
* 总条数
*/
private Long total;
/**
* 总页数
*/
private Long totalPage;
/**
* 当前页数据
*/
private List<T> items;
public PageResult() {
}
public PageResult(Long total, List<T> items) {
this.total = total;
this.items = items;
}
public PageResult(Long total, Long totalPage, List<T> items) {
this.total = total;
this.totalPage = totalPage;
this.items = items;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
public List<T> getItems() {
return items;
}
public void setItems(List<T> items) {
this.items = items;
}
public Long getTotalPage() {
return totalPage;
}
public void setTotalPage(Long totalPage) {
this.totalPage = totalPage;
}
}
并且这个封装类在其他微服务中也会使用,所以将其抽取到ly-common中,提高复用性:
因为传递的参数比较多,所以专门封装一个参数类:
package com.leyou.parameter.pojo;
/**
* @Author: 98050
* Time: 2018-08-08 11:38
* Feature:
*/
public class BrandQueryByPageParameter {
/*
* - page:当前页,int
- rows:每页大小,int
- sortBy:排序字段,String
- desc:是否为降序,boolean
- key:搜索关键词,String
* */
private Integer page;
private Integer rows;
private String sortBy;
private Boolean desc;
private String key;
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getRows() {
return rows;
}
public void setRows(Integer rows) {
this.rows = rows;
}
public String getSortBy() {
return sortBy;
}
public void setSortBy(String sortBy) {
this.sortBy = sortBy;
}
public Boolean getDesc() {
return desc;
}
public void setDesc(Boolean desc) {
this.desc = desc;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public BrandQueryByPageParameter(Integer page, Integer rows, String sortBy, Boolean desc, String key) {
this.page = page;
this.rows = rows;
this.sortBy = sortBy;
this.desc = desc;
this.key = key;
}
public BrandQueryByPageParameter(){
super();
}
@Override
public String toString() {
return "BrandQueryByPageParameter{" +
"page=" + page +
", rows=" + rows +
", sortBy='" + sortBy + '\'' +
", desc=" + desc +
", key='" + key + '\'' +
'}';
}
}
编写Controller
/**
* @Author: 98050
* Time: 2018-08-07 19:18
* Feature:
*/
@RestController
@RequestMapping("brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 分页查询品牌
* @param page
* @param rows
* @param sortBy
* @param desc
* @param key
* @return
*/
@GetMapping("page")
public ResponseEntity<PageResult<Brand>> queryBrandByPage( @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){
BrandQueryByPageParameter brandQueryByPageParameter=new BrandQueryByPageParameter(page,rows,sortBy,desc,key);
PageResult<Brand> result = this.brandService.queryBrandByPage(brandQueryByPageParameter);
if(result == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
return ResponseEntity.ok(result);
}
}
2.1.5 Service
接口
/**
* 分页查询
* @param brandQueryByPageParameter
* @return
*/
PageResult<Brand> queryBrandByPage(BrandQueryByPageParameter brandQueryByPageParameter);
实现类
@Override
public PageResult<Brand> queryBrandByPage(BrandQueryByPageParameter brandQueryByPageParameter) {
/**
* 1.分页
*/
PageHelper.startPage(brandQueryByPageParameter.getPage(),brandQueryByPageParameter.getRows());
/**
* 2.排序
*/
Example example = new Example(Brand.class);
if (StringUtils.isNotBlank(brandQueryByPageParameter.getSortBy())){
example.setOrderByClause(brandQueryByPageParameter.getSortBy()+(brandQueryByPageParameter.getDesc()? " DESC":" ASC"));
}
/**
* 3.查询
*/
if(StringUtils.isNotBlank(brandQueryByPageParameter.getKey())) {
example.createCriteria().orLike("name", brandQueryByPageParameter.getKey()+"%").orEqualTo("letter", brandQueryByPageParameter.getKey().toUpperCase());
}
List<Brand> list=this.brandMapper.selectByExample(example);
/**
* 4.创建PageInfo
*/
PageInfo<Brand> pageInfo = new PageInfo<>(list);
/**
* 5.返回分页结果
*/
return new PageResult<>(pageInfo.getTotal(),pageInfo.getList());
}
2.1.6 测试
访问http://api.leyou.com/api/item/brand/page
2.1.7 前端请求
在页面创建的时候需要加载数据,所以将数据请求单独放在一个函数里面,方便以后实时刷新数据。
getDataFromServer(){
// 开启进度条
this.loading = true;
//发起ajax请求
// 分页查询page,rows,key,sortBy,desc
this.$http.get("/item/brand/page",{
params:{
page:this.pagination.page,
rows:this.pagination.rowsPerPage,
sortBy:this.pagination.sortBy,
desc:this.pagination.descending,
key:this.search,
}
}).then(resp =>{
console.log(resp)
this.brands=resp.data.items;
this.totalBrands = resp.data.total;
//关闭进度条
this.loading = false;
})
}
2.2 品牌增加
2.2.1 Controller
还是一样,先分析四个内容:
-
请求方式:刚才看到了是POST
-
请求路径:/brand
-
请求参数:brand对象,外加商品分类的id(最后一级id)数组cids
-
返回值:无
代码:
/**
* 品牌新增
* @param brand
* @param categories
* @return
*/
@PostMapping
public ResponseEntity<Void> saveBrand(Brand brand, @RequestParam("categories") List<Long> categories){
this.brandService.saveBrand(brand, categories);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
2.2.2 Service
接口:
/**
* 新增brand,并且维护中间表
* @param brand
* @param cids
*/
void saveBrand(Brand brand, List<Long> cids);
实现类:
@Override
@Transactional(rollbackFor = Exception.class)
public void saveBrand(Brand brand, List<Long> categories) {
System.out.println(brand);
// 新增品牌信息
this.brandMapper.insertSelective(brand);
// 新增品牌和分类中间表
for (Long cid : categories) {
this.brandMapper.insertCategoryBrand(cid, brand.getId());
}
}
这里调用了brandMapper中的一个自定义方法insertCategoryBrand,来实现中间表的数据新增 。
2.2.3 Mapper
通用Mapper只能处理单表,也就是Brand的数据,因此我们手动编写一个方法及sql,实现中间表的新增
/**
* @Author: 98050
* @Time: 2018-08-07 19:15
* @Feature:
*/
@org.apache.ibatis.annotations.Mapper
public interface BrandMapper extends Mapper<Brand> {
/**
* 新增商品分类和品牌中间表数据
* @param cid 商品分类id
* @param bid 品牌id
* @return
*/
@Insert("INSERT INTO tb_category_brand (category_id, brand_id) VALUES (#{cid},#{bid})")
void insertCategoryBrand(@Param("cid") Long cid, @Param("bid") Long bid);
}
2.2.4 前端的细节问题
新增完成后关闭当前窗口(Vue组件之间的通信),控制窗口关闭是在父组件MyBrand.vue中。
- 第一步,在父组件中定义一个函数,用来关闭窗口,不过之前已经定义过了,我们优化一下,关闭的同时重新加载数据:
reload(){
//关闭对话框
this.show=false;
//刷新页面
this.getDataFromServer();
},
- 第二步,父组件在使用子组件时,绑定事件,关联到这个函数
- 第三步,子组件通过
this.$emit
调用父组件的函数:
2.2.5 图片的上传
刚才的新增实现中,并没有上传图片。由于文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此需要创建一个独立的微服务,专门处理各种上传。最终目的是做一个分布式文件系统,具体在下一篇介绍。
2.3 品牌修改
2.3.1 点击编辑出现弹窗
给编辑按钮绑定一个事件即可,并且把当前brand的信息传递给editBrand方法。
2.3.2 数据回显
回显数据,就是把当前点击的品牌数据传递到子组件(MyBrandForm)。而父组件给子组件传递数据,通过props属性。
- 第一步:在编辑时获取当前选中的品牌信息,并且记录到oldBrand中。
在data中定义oldBrand属性,用来接收要编辑的brand数据:
- 第二步:在触发编辑事件时,把当前的brand传递给editBrand方法方法,然后赋值给oldBrand。
editBrand(oldBrand){
// 控制弹窗可见:
this.show = true;
// 获取要编辑的brand
this.oldBrand = oldBrand;
},
- 第三步:把获取的brand数据 传递给子组件
- 第四步:在子组件中通过props接收要编辑的brand数据,Vue会自动完成回显
接收数据:
- 第五步:通过watch函数监控oldBrand的变化,把值copy到本地的brand。
watch:{
oldBrand:{
deep:true,
handler(val){
if(val){
this.brand=Object.deepCopy(val);
}else{
this.clear();
}
}
}
},
Object.deepCopy 自定义的对对象进行深度复制的方法。
需要判断监听到的是否为空,如果为空,应该进行初始化,初始化用到了一个函数clear:
- 第六步:测试。除了商品分类以外,其他数据都回显了。
2.3.4 商品分类回显
商品分类信息在tb_brand中是没有的,需要通过中间表tb_category_brand和tb_category联合查询得到。
2.3.4.1 后台接口
Controller
/**
* 用于修改品牌信息时,商品分类信息的回显
* @param bid
* @return
*/
@GetMapping("bid/{bid}")
public ResponseEntity<List<Category>> queryByBrandId(@PathVariable("bid") Long bid){
List<Category> list = this.categoryService.queryByBrandId(bid);
if(list == null || list.size() < 1){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(list);
}
Service
接口:
/**
* 根据brand id查询分类信息
* @param bid
* @return
*/
List<Category> queryByBrandId(Long bid);
实现类:
/**
* 根据品牌id查询分类
* @param bid
* @return
*/
@Override
public List<Category> queryByBrandId(Long bid) {
return this.categoryMapper.queryByBrandId(bid);
}
Mapper
/**
* 根据品牌id查询商品分类
* @param bid
* @return
*/
@Select("SELECT * FROM tb_category WHERE id IN (SELECT category_id FROM tb_category_brand WHERE brand_id = #{bid}) ")
List<Category> queryByBrandId(@Param("bid") Long bid);
2.3.4.2 前台查询分类并渲染
在编辑页打开前,就要进行商品分类的查询,查询成功后再回显其他数据。
最终代码:
editBrand(oldBrand){
//根据品牌信息查询商品分类
this.$http.get("/item/category/bid/"+oldBrand.id).then(
({data}) => {
this.isEdit=true;
//显示弹窗
this.show=true;
//获取要编辑的brand
this.oldBrand=oldBrand;
this.oldBrand.categories = data;
}
).catch();
},
测试:
2.3.4.3 新增窗口数据干扰
但是,此时却产生了新问题:新增窗口竟然也有数据。
原因:如果之前打开过编辑,那么在父组件中记录的oldBrand会保留。下次再打开窗口,如果是编辑窗口到没问题,但是新增的话,就会再次显示上次打开的品牌信息了。
解决: 新增窗口打开前,把数据置空。
2.3.4.4 提交表单时要判断是新增还是修改
新增和修改是同一个页面,我们该如何判断?
父组件中点击按钮弹出新增或修改的窗口,因此父组件非常清楚接下来是新增还是修改。
因此,最简单的方案就是,在父组件中定义变量,记录新增或修改状态,当弹出页面时,把这个状态也传递给子组件。
- 第一步:在父组件中记录状态
- 第二步:在新增和修改前更改状态
- 第三步:传递给子组件
- 第四步:子组件接收标记
- 第五步:动态化处理
标题动态化:
表单提交动态:
submit(){
//提交表单
if(this.$refs.BrandForm.validate()){
/**
* 使用解构表达式获取数据,除categories以外的数据都放入rest中,然后对categories使用map进行处理,得到id后重新赋值给
* rest里面的categories数组
*/
const {categories, ... rest}=this.brand;
rest.categories=categories.map(c => c.id).join(",");
console.log(rest)
if(this.isEdit) {
this.$http.delete("/item/brand/cid_bid/" + this.oldBrand.id).then().catch();
}
this.$http({
method:this.isEdit ? 'put' :'post',
url:"/item/brand",
data:this.$qs.stringify(rest),
}).then(
() =>{
//关闭对话框
this.$emit('reload');
this.$message.success("保存成功!");
this.clear();
}
).catch(
()=>{
this.$message.success("保存失败!");
}
);
}
},
2.4 品牌删除
删除分为两种:单个和多个。
2.4.1 逻辑
单个删除传入后端的是被删数据的id,多个删除则是将全部id用“-”连接成字符串传入后端,而后端通过判断传入的数据是否包含“-”来决定是单个删除还是删除多个。
2.4.2 后端接口实现
选中后点击删除即可。删除的时候先从tb_brand中删除数据,然后维护中间表tb_category_brand。
Controller
/**
* 删除tb_brand中的数据,单个删除、多个删除二合一
* @param bid
* @return
*/
@DeleteMapping("bid/{bid}")
public ResponseEntity<Void> deleteBrand(@PathVariable("bid") String bid){
String separator="-";
if(bid.contains(separator)){
String[] ids=bid.split(separator);
for (String id:ids){
this.brandService.deleteBrand(Long.parseLong(id));
}
}
else {
this.brandService.deleteBrand(Long.parseLong(bid));
}
return ResponseEntity.status(HttpStatus.OK).build();
}
Service
接口
/**
* 删除brand,并且维护中间表
* @param id
*/
void deleteBrand(Long id);
实现类
/**
* 品牌删除
* @param id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteBrand(Long id) {
//删除品牌信息
this.brandMapper.deleteByPrimaryKey(id);
//维护中间表
this.brandMapper.deleteByBrandIdInCategoryBrand(id);
}
Mapper
维护中间表时需要自己写sql
/**
* 根据brand id删除中间表相关数据
* @param bid
*/
@Delete("DELETE FROM tb_category_brand WHERE brand_id = #{bid}")
void deleteByBrandIdInCategoryBrand(@Param("bid") Long bid);
2.4.3 前端请求
2.4.3.1 单个删除
deleteBrand(oldBrand){
if (this.selected.length === 1 && this.selected[0].id === oldBrand.id) {
this.$message.confirm('此操作将永久删除该品牌, 是否继续?').then(
() => {
//发起删除请求,删除单条数据
this.$http.delete("/item/brand/bid/" + oldBrand.id).then(() => {
this.getDataFromServer();
}).catch()
}
).catch(() => {
this.$message.info("删除已取消!");
});
}
}
2.4.3.2 多个删除
deleteAllBrand(){
//拼接id数组
/**
* 加了{}就必须有return
* @type {any[]}
*/
const ids = this.selected.map( s => s.id);
if (selected.length>0) {
this.$message.confirm('此操作将永久删除所选品牌,是否继续?').then(
() => {
this.$http.delete("/item/brand/bid/" + ids.join("-")).then(() => {
this.getDataFromServer();
}).catch();
}
).catch(() => {
this.$message.info("删除已取消!");
});
}
}
三、功能演示