樂優商城(五)品牌管理(後端)
目錄
後臺功能——品牌管理(後端)
二、後端介面實現
主要就是對資料庫的抽插,難點在於和前端頁面的聯調,介面本身不復雜。
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 測試
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("刪除已取消!");
});
}
}
三、功能演示