樂優商城--品牌管理(新增、圖片上傳、FastDNS、修改)
-
獨立實現品牌新增
-
實現圖片上傳
-
瞭解FastDFS的安裝
-
使用FastDFS客戶端實現上傳
1.品牌的新增
昨天我們完成了品牌的查詢,接下來就是新增功能。點選新增品牌按鈕
Brand.vue頁面有一個提交按鈕:
點選觸發addBrand方法:
把資料模型之的show置為true,而頁面中有一個彈窗與show繫結:
彈窗中有一個表單子元件,並且是一個區域性子元件,有頁面可以找到該元件:
1.1.頁面實現
1.1.1.重置表單
重置表單相對簡單,因為v-form元件已經提供了reset方法,用來清空表單資料。只要我們拿到表單元件物件,就可以呼叫方法了。
我們可以通過$refs
首先,在表單上定義ref
屬性:
然後,在頁面檢視this.$refs
屬性:
reset(){ // 重置表單 console.log(this); }
檢視如下:
看到this.$refs
中只有一個屬性,就是myBrandForm
我們在clear中來獲取表單物件並呼叫reset方法:
要注意的是,這裡我們還手動把this.categories清空了,因為我寫的級聯選擇元件並沒有跟表單結合起來。需要手動清空。
1.1.2.表單校驗
1.1.2.1.校驗規則
Vuetify的表單校驗,是通過rules屬性來指定的:
校驗規則的寫法:
說明:
-
規則是一個數組
-
陣列中的元素是一個函式,該函式接收表單項的值作為引數,函式返回值兩種情況:
-
返回true,代表成功,
-
返回錯誤提示資訊,代表失敗
-
1.1.2.2.編寫校驗
我們有四個欄位:
-
name:做非空校驗和長度校驗,長度必須大於1
-
letter:首字母,校驗長度為1,非空。
-
image:圖片,不做校驗,圖片可以為空
-
categories:非空校驗,自定義元件已經幫我們完成,不用寫了
首先,我們定義規則:
然後,在頁面標籤中指定:
<v-text-field v-model="brand.name" label="請輸入品牌名稱" hint="例如:oppo" :rules="[rules.required, rules.nameLength]"></v-text-field> <v-text-field v-model="brand.letter" label="請輸入品牌首字母" hint="例如:O" :rules="[rules.letter]"></v-text-field>
效果:
1.1.3.表單提交
在submit方法中新增表單提交的邏輯:
submit() { console.log(this.$qs); // 表單校驗 if (this.$refs.myBrandForm.validate()) { // 定義一個請求引數物件,通過解構表示式來獲取brand中的屬性{categories letter name image} const {categories, letter, ...params} = this.brand; // params:{name, image, cids, letter} // 資料庫中只要儲存分類的id即可,因此我們對categories的值進行處理,只保留id,並轉為字串 params.cids = categories.map(c => c.id).join(","); // 將字母都處理為大寫 params.letter = letter.toUpperCase(); // 將資料提交到後臺 // this.$http.post('/item/brand', this.$qs.stringify(params)) this.$http({ method: this.isEdit ? 'put' : 'post', url: '/item/brand', data: params }).then(() => { // 關閉視窗 this.$emit("close"); this.$message.success("儲存成功!"); }) .catch(() => { this.$message.error("儲存失敗!"); }); } }
-
通過
this.$refs.myBrandForm
選中表單,然後呼叫表單的validate
方法,進行表單校驗。返回boolean值,true代表校驗通過 -
通過解構表示式來獲取brand中的值,categories需要處理,單獨獲取。其它的存入params物件中
-
品牌和商品分類的中間表只儲存兩者的id,而brand.categories中儲存的是物件陣列,裡面有id和name屬性,因此這裡通過陣列的map功能轉為id陣列,然後通過join方法拼接為字串
-
發起請求
-
彈窗提示成功還是失敗,這裡用到的是我們的自定義元件功能message元件:
這個外掛把$message
物件繫結到了Vue的原型上,因此我們可以通過this.$message
來直接呼叫。
包含以下常用方法:
-
info、error、success、warning等,彈出一個帶有提示資訊的視窗,色調與為普通(灰)、錯誤(紅色)、成功(綠色)和警告(黃色)。使用方法:this.$message.info("msg")
-
confirm:確認框。用法:
this.$message.confirm("確認框的提示資訊")
,返回一個Promise。
1.2.後臺實現新增
1.2.1.controller
還是一樣,先分析四個內容:
-
請求方式:POST
-
請求路徑:/brand
-
請求引數:brand物件,外加商品分類的id陣列cids
-
返回值:無,只需要響應狀態碼
程式碼:
/** * 新增品牌 * @param brand * @param cids */ @PostMapping public ResponseEntity<Void> saveBrand(Brand brand, @RequestParam("cids") List<Long> cids){ this.brandService.saveBrand(brand, cids); return ResponseEntity.status(HttpStatus.CREATED).build(); }
1.2.2.Service
這裡要注意,我們不僅要新增品牌,還要維護品牌和商品分類的中間表。
/** * 新增品牌 * * @param brand * @param cids */ @Transactional public void saveBrand(Brand brand, List<Long> cids) { // 先新增brand this.brandMapper.insertSelective(brand); // 在新增中間表 cids.forEach(cid -> { this.brandMapper.insertCategoryAndBrand(cid, brand.getId()); }); }
這裡呼叫了brandMapper中的一個自定義方法,來實現中間表的資料新增
1.2.3.Mapper
通用Mapper只能處理單表,也就是Brand的資料,因此我們手動編寫一個方法及sql,實現中間表的新增:
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})") int insertBrandAndCategory(@Param("cid") Long cid, @Param("bid") Long bid); }
1.2.4.測試
400:請求引數不合法
1.3.解決400
1.3.1.原因分析
我們填寫表單並提交,發現報錯了。檢視控制檯的請求詳情:
發現請求的資料格式是JSON格式。
原因分析:
axios處理請求體的原則會根據請求資料的格式來定:
-
如果請求體是物件:會轉為json傳送
-
如果請求體是String:會作為普通表單請求傳送,但需要我們自己保證String的格式是鍵值對。
如:name=jack&age=12
1.3.2.QS工具
QS是一個第三方庫,我們可以用npm install qs --save
來安裝。不過我們在專案中已經集成了,大家無需安裝:
這個工具的名字:QS,即Query String,請求引數字串。
什麼是請求引數字串?例如: name=jack&age=21
QS工具可以便捷的實現 JS的Object與QueryString的轉換。
在我們的專案中,將QS注入到了Vue的原型物件中,我們可以通過this.$qs
來獲取這個工具:
我們將this.$qs
物件列印到控制檯:
created(){ console.log(this.$qs); }
發現其中有3個方法:
這裡我們要使用的方法是stringify,它可以把Object轉為QueryString。
測試一下,使用瀏覽器工具,把qs物件儲存為一個臨時變數temp1,然後呼叫stringify方法:
成功將person物件變成了 name=zhangsan&age=30的字串了
1.3.3.解決問題
修改頁面,對引數處理後傳送:
然後再次發起請求,發現請求成功:
1.4.新增完成後關閉視窗
我們發現有一個問題:新增不管成功還是失敗,視窗都一致在這裡,不會關閉。
這樣很不友好,我們希望如果新增失敗,視窗保持;但是新增成功,視窗關閉才對。
因此,我們需要在新增的ajax請求完成以後,關閉視窗
但問題在於,控制視窗是否顯示的標記在父元件:MyBrand.vue中。子元件如何才能操作父元件的屬性?或者告訴父元件該關閉視窗了?
之前我們講過一個父子元件的通訊,有印象嗎?
-
第一步:在父元件中定義一個函式,用來關閉視窗,不過之前已經定義過了。父元件在使用子元件時,繫結事件,關聯到這個函式:Brand.vue
<!--對話方塊的內容,表單--> <v-card-text class="px-5" style="height:400px"> <brand-form @close="closeWindow" :oldBrand="oldBrand" :isEdit="isEdit"/> </v-card-text>
-
第二步,子元件通過
this.$emit
呼叫父元件的函式:BrandForm.vue
測試一下,儲存成功:
我們優化一下,關閉的同時重新載入資料:
closeWindow(){ // 關閉視窗 this.show = false; // 重新載入資料 this.getDataFromServer(); }
2.實現圖片上傳
剛才的新增實現中,我們並沒有上傳圖片,接下來我們一起完成圖片上傳邏輯。
檔案的上傳並不只是在品牌管理中有需求,以後的其它服務也可能需要,因此我們建立一個獨立的微服務,專門處理各種上傳。
2.1.搭建專案
2.1.1.建立module
2.1.2.依賴
我們需要EurekaClient和web依賴:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>leyou</artifactId> <groupId>com.leyou.parent</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.upload</groupId> <artifactId>leyou-upload</artifactId> <version>1.0.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> </project>
2.1.3.編寫配置
server:
port: 8082
spring:
application:
name: upload-service
servlet:
multipart:
max-file-size: 5MB # 限制檔案上傳的大小
# Eureka
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
lease-renewal-interval-in-seconds: 5 # 每隔5秒傳送一次心跳
lease-expiration-duration-in-seconds: 10 # 10秒不傳送就過期
需要注意的是,我們應該添加了限制檔案大小的配置
2.1.4.引導類
@SpringBootApplication @EnableDiscoveryClient public class LeyouUploadApplication { public static void main(String[] args) { SpringApplication.run(LeyouUploadApplication.class, args); } }
結構:
2.2.編寫上傳功能
檔案上傳功能,也是自定義元件完成的,參照自定義元件用法指南:
在頁面中的使用:
2.2.1.controller
編寫controller需要知道4個內容:結合用法指南
-
請求方式:上傳肯定是POST
-
請求路徑:/upload/image
-
請求引數:檔案,引數名是file,SpringMVC會封裝為一個介面:MultipartFile
-
返回結果:上傳成功後得到的檔案的url路徑,也就是返回String
程式碼如下:
@Controller @RequestMapping("upload") public class UploadController { @Autowired private UploadService uploadService; /** * 圖片上傳 * @param file * @return */ @PostMapping("image") public ResponseEntity<String> uploadImage(@RequestParam("file") MultipartFile file){ String url = this.uploadService.upload(file); if (StringUtils.isBlank(url)) { return ResponseEntity.badRequest().build(); } return ResponseEntity.status(HttpStatus.CREATED).body(url); } }
2.2.2.service
在上傳檔案過程中,我們需要對上傳的內容進行校驗:
-
校驗檔案大小
-
校驗檔案的媒體型別
-
校驗檔案的內容
檔案大小在Spring的配置檔案中設定,因此已經會被校驗,我們不用管。
具體程式碼:
@Service public class UploadService { private static final List<String> CONTENT_TYPES = Arrays.asList("image/jpeg", "image/gif"); private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class); public String upload(MultipartFile file) { String originalFilename = file.getOriginalFilename(); // 校驗檔案的型別 String contentType = file.getContentType(); if (!CONTENT_TYPES.contains(contentType)){ // 檔案型別不合法,直接返回null LOGGER.info("檔案型別不合法:{}", originalFilename); return null; } try { // 校驗檔案的內容 BufferedImage bufferedImage = ImageIO.read(file.getInputStream()); if (bufferedImage == null){ LOGGER.info("檔案內容不合法:{}", originalFilename); return null; } // 儲存到伺服器 file.transferTo(new File("C:\\leyou\\images\\" + originalFilename)); // 生成url地址,返回 return "http://image.leyou.com/" + originalFilename; } catch (IOException e) { LOGGER.info("伺服器內部錯誤:{}", originalFilename); e.printStackTrace(); } return null; } }
這裡有一個問題:為什麼圖片地址需要使用另外的url?
-
圖片不能儲存在伺服器內部,這樣會對伺服器產生額外的載入負擔
-
一般靜態資源都應該使用獨立域名,這樣訪問靜態資源時不會攜帶一些不必要的cookie,減小請求的資料量
2.2.3.測試上傳
我們通過RestClient工具來測試:
結果:
去目錄下檢視:
上傳成功!
2.3.繞過閘道器
圖片上傳是檔案的傳輸,如果也經過Zuul閘道器的代理,檔案就會經過多次網路傳輸,造成不必要的網路負擔。在高併發時,可能導致網路阻塞,Zuul閘道器不可用。這樣我們的整個系統就癱瘓了。
所以,我們上傳檔案的請求就不經過閘道器來處理了。
2.3.1.Zuul的路由過濾
Zuul中提供了一個ignored-patterns屬性,用來忽略不希望路由的URL路徑,示例:
zuul.ignored-patterns: /upload/**
路徑過濾會對一切微服務進行判定。
Zuul還提供了ignored-services
屬性,進行服務過濾:
zuul.ignored-services: upload-servie
我們這裡採用忽略服務:
zuul: ignored-services: - upload-service # 忽略upload-service服務
上面的配置採用了集合語法,代表可以配置多個。
2.3.2.Nginx的rewrite指令
現在,我們修改頁面的訪問路徑:
<v-upload v-model="brand.image" url="/upload/image" :multiple="false" :pic-width="250" :pic-height="90" />
檢視頁面的請求路徑:
可以看到這個地址不對,依然是去找Zuul閘道器,因為我們的系統全域性配置了URL地址。怎麼辦?
有同學會想:修改頁面請求地址不就好了。
注意:原則上,我們是不能把除了閘道器以外的服務對外暴露的,不安全。
既然不能修改頁面請求,那麼就只能在Nginx反向代理上做文章了。
我們修改nginx配置,將以/api/upload開頭的請求攔截下來,轉交到真實的服務地址:
location /api/upload {
proxy_pass http://127.0.0.1:8082;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
這樣寫大家覺得對不對呢?
顯然是不對的,因為ip和埠雖然對了,但是路徑沒變,依然是:http://127.0.0.1:8002/api/upload/image
前面多了一個/api
Nginx提供了rewrite指令,用於對地址進行重寫,語法規則:
rewrite "用來匹配路徑的正則" 重寫後的路徑 [指令];
我們的案例:
server {
listen 80;
server_name api.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 /api/upload {
proxy_pass http://127.0.0.1:8082;
proxy_connect_timeout 600;
proxy_read_timeout 600;
rewrite "^/api/(.*)$" /$1 break;
}
location / {
proxy_pass http://127.0.0.1:10010;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
-
首先,我們對映路徑是/api/upload,而下面一個對映路徑是 / ,根據最長路徑匹配原則,/api/upload優先順序更高。也就是說,凡是以/api/upload開頭的路徑,都會被第一個配置處理
-
proxy_pass
:反向代理,這次我們代理到8082埠,也就是upload-service服務 -
rewrite "^/api/(.*)$" /$1 break
,路徑重寫:-
"^/api/(.*)$"
:匹配路徑的正則表示式,用了分組語法,把/api/
以後的所有部分當做1組 -
/$1
:重寫的目標路徑,這裡用$1引用前面正則表示式匹配到的分組(組編號從1開始),即/api/
後面的所有。這樣新的路徑就是除去/api/
以外的所有,就達到了去除/api
字首的目的 -
break
:指令,常用的有2個,分別是:last、break-
last:重寫路徑結束後,將得到的路徑重新進行一次路徑匹配
-
break:重寫路徑結束後,不再重新匹配路徑。
我們這裡不能選擇last,否則以新的路徑/upload/image來匹配,就不會被正確的匹配到8082埠了
-
-
修改完成,輸入nginx -s reload
命令重新載入配置。然後再次上傳試試。
2.4.跨域問題
重啟nginx,再次上傳,發現跟上次的狀態碼已經不一樣了,但是依然報錯:
不過慶幸的是,這個錯誤已經不是第一次見了,跨域問題。
我們在upload-service中新增一個CorsFilter即可:
@Configuration public class LeyouCorsConfiguration { @Bean public CorsFilter corsFilter() { //1.新增CORS配置資訊 CorsConfiguration config = new CorsConfiguration(); //1) 允許的域,不要寫*,否則cookie就無法使用了 config.addAllowedOrigin("http://manage.leyou.com"); //3) 允許的請求方式 config.addAllowedMethod("OPTIONS"); config.addAllowedMethod("POST"); // 4)允許的頭資訊 config.addAllowedHeader("*"); //2.新增對映路徑,我們攔截一切請求 UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", config); //3.返回新的CorsFilter. return new CorsFilter(configSource); } }
再次測試:
不過,非常遺憾的是,訪問圖片地址,卻沒有響應。
這是因為我們並沒有任何伺服器對應image.leyou.com這個域名。。
這個問題,我們暫時放下,回頭再來解決。
2.5.檔案上傳的缺陷
先思考一下,現在上傳的功能,有沒有什麼問題?
上傳本身沒有任何問題,問題出在儲存檔案的方式,我們是儲存在伺服器機器,就會有下面的問題:
-
單機器儲存,儲存能力有限
-
無法進行水平擴充套件,因為多臺機器的檔案無法共享,會出現訪問不到的情況
-
資料沒有備份,有單點故障風險
-
併發能力差
這個時候,最好使用分散式檔案儲存來代替本地檔案儲存。
3.FastDFS
3.1.什麼是分散式檔案系統
分散式檔案系統(Distributed File System)是指檔案系統管理的物理儲存資源不一定直接連線在本地節點上,而是通過計算機網路與節點相連。
通俗來講:
-
傳統檔案系統管理的檔案就儲存在本機。
-
分散式檔案系統管理的檔案儲存在很多機器,這些機器通過網路連線,要被統一管理。無論是上傳或者訪問檔案,都需要通過管理中心來訪問
3.2.什麼是FastDFS
FastDFS是由淘寶的餘慶先生所開發的一個輕量級、高效能的開源分散式檔案系統。用純C語言開發,功能豐富:
-
檔案儲存
-
檔案同步
-
檔案訪問(上傳、下載)
-
存取負載均衡
-
線上擴容
適合有大容量儲存需求的應用或系統。同類的分散式檔案系統有谷歌的GFS、HDFS(Hadoop)、TFS(淘寶)等。
3.3.FastDFS的架構
3.3.1.架構圖
先上圖:
FastDFS兩個主要的角色:Tracker Server 和 Storage Server 。
-
Tracker Server:跟蹤伺服器,主要負責排程storage節點與client通訊,在訪問上起負載均衡的作用,和記錄storage節點的執行狀態,是連線client和storage節點的樞紐。
-
Storage Server:儲存伺服器,儲存檔案和檔案的meta data(元資料),每個storage server會啟動一個單獨的執行緒主動向Tracker cluster中每個tracker server報告其狀態資訊,包括磁碟使用情況,檔案同步情況及檔案上傳下載次數統計等資訊
-
Group:檔案組,多臺Storage Server的叢集。上傳一個檔案到同組內的一臺機器上後,FastDFS會將該檔案即時同步到同組內的其它所有機器上,起到備份的作用。不同組的伺服器,儲存的資料不同,而且相互獨立,不進行通訊。
-
Tracker Cluster:跟蹤伺服器的叢集,有一組Tracker Server(跟蹤伺服器)組成。
-
Storage Cluster :儲存叢集,有多個Group組成。
3.3.2.上傳和下載流程
上傳
-
Client通過Tracker server查詢可用的Storage server。
-
Tracker server向Client返回一臺可用的Storage server的IP地址和埠號。
-
Client直接通過Tracker server返回的IP地址和埠與其中一臺Storage server建立連線並進行檔案上傳。
-
上傳完成,Storage server返回Client一個檔案ID,檔案上傳結束。
下載
-
Client通過Tracker server查詢要下載檔案所在的的Storage server。
-
Tracker server向Client返回包含指定檔案的某個Storage server的IP地址和埠號。
-
Client直接通過Tracker server返回的IP地址和埠與其中一臺Storage server建立連線並指定要下載檔案。
-
下載檔案成功。
3.4.安裝和使用
參考課前資料的:
3.5.java客戶端
餘慶先生提供了一個Java客戶端,但是作為一個C程式設計師,寫的java程式碼可想而知。而且已經很久不維護了。
這裡推薦一個開源的FastDFS客戶端,支援最新的SpringBoot2.0。
配置使用極為簡單,支援連線池,支援自動生成縮圖,狂拽酷炫吊炸天啊,有木有。
地址:
接下來,我們就用FastDFS改造leyou-upload工程。
3.5.1.引入依賴
在父工程中,我們已經管理了依賴,版本為:
<fastDFS.client.version>1.26.2</fastDFS.client.version>
因此,這裡我們直接在taotao-upload工程的pom.xml中引入座標即可:
<dependency> <groupId>com.github.tobato</groupId> <artifactId>fastdfs-client</artifactId> </dependency>
3.5.2.引入配置類
純java配置:
@Configuration @Import(FdfsClientConfig.class) // 解決jmx重複註冊bean的問題 @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) public class FastClientImporter { }
3.5.3.編寫FastDFS屬性
在application.yml配置檔案中追加如下內容:
fdfs: so-timeout: 1501 # 超時時間 connect-timeout: 601 # 連線超時時間 thumb-image: # 縮圖 width: 60 height: 60 tracker-list: # tracker地址:你的虛擬機器伺服器地址+埠(預設是22122) - 192.168.56.101:22122
3.5.4.配置hosts
將來通過域名:image.leyou.com這個域名訪問fastDFS伺服器上的圖片資源。所以,需要代理到虛擬機器地址:
配置hosts檔案,使image.leyou.com可以訪問fastDFS伺服器
3.5.5.測試
建立測試類:
把以下內容copy進去:
@SpringBootTest @RunWith(SpringRunner.class) public class FastDFSTest { @Autowired private FastFileStorageClient storageClient; @Autowired private ThumbImageConfig thumbImageConfig; @Test public void testUpload() throws FileNotFoundException { // 要上傳的檔案 File file = new File("C:\\Users\\joedy\\Pictures\\xbx1.jpg"); // 上傳並儲存圖片,引數:1-上傳的檔案流 2-檔案的大小 3-檔案的字尾 4-可以不管他 StorePath storePath = this.storageClient.uploadFile( new FileInputStream(file), file.length(), "jpg", null); // 帶分組的路徑 System.out.println(storePath.getFullPath()); // 不帶分組的路徑 System.out.println(storePath.getPath()); } @Test public void testUploadAndCreateThumb() throws FileNotFoundException { File file = new File("C:\\Users\\joedy\\Pictures\\xbx1.jpg"); // 上傳並且生成縮圖 StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage( new FileInputStream(file), file.length(), "png", null); // 帶分組的路徑 System.out.println(storePath.getFullPath()); // 不帶分組的路徑 System.out.println(storePath.getPath()); // 獲取縮圖路徑 String path = thumbImageConfig.getThumbImagePath(storePath.getPath()); System.out.println(path); } }
結果:
group1/M00/00/00/wKg4ZVsWl5eAdLNZAABAhya2V0c424.jpg M00/00/00/wKg4ZVsWl5eAdLNZAABAhya2V0c424.jpg
group1/M00/00/00/wKg4ZVsWmD-ARnWiAABAhya2V0c772.png M00/00/00/wKg4ZVsWmD-ARnWiAABAhya2V0c772.png M00/00/00/wKg4ZVsWmD-ARnWiAABAhya2V0c772_60x60.png
訪問第二組第一個路徑:
訪問最後一個路徑(縮圖路徑),注意加組名(group1):
3.5.6.改造上傳邏輯
@Service public class UploadService { @Autowired private FastFileStorageClient storageClient; private static final List<String> CONTENT_TYPES = Arrays.asList("image/jpeg", "image/gif"); private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class); public String upload(MultipartFile file) { String originalFilename = file.getOriginalFilename(); // 校驗檔案的型別 String contentType = file.getContentType(); if (!CONTENT_TYPES.contains(contentType)){ // 檔案型別不合法,直接返回null LOGGER.info("檔案型別不合法:{}", originalFilename); return null; } try { // 校驗檔案的內容 BufferedImage bufferedImage = ImageIO.read(file.getInputStream()); if (bufferedImage == null){ LOGGER.info("檔案內容不合法:{}", originalFilename); return null; } // 儲存到伺服器 // file.transferTo(new File("C:\\leyou\\images\\" + originalFilename)); String ext = StringUtils.substringAfterLast(originalFilename, "."); StorePath storePath = this.storageClient.uploadFile(file.getInputStream(), file.getSize(), ext, null); // 生成url地址,返回 return "http://image.leyou.com/" + storePath.getFullPath(); } catch (IOException e) { LOGGER.info("伺服器內部錯誤:{}", originalFilename); e.printStackTrace(); } return null; } }
只需要把原來儲存檔案的邏輯去掉,然後上傳到FastDFS即可。
3.5.7.測試
通過RestClient測試:
3.6.頁面測試上傳
發現上傳成功:
4.修改品牌(作業)
修改的難點在於回顯。
當我們點選編輯按鈕,希望彈出視窗的同時,看到原來的資料:
4.1.點選編輯出現彈窗
這個比較簡單,修改show屬性為true即可實現,我們繫結一個點選事件:
<v-icon small class="mr-2" @click="editItem(props.item)"> edit </v-icon>
然後編寫事件,改變show 的狀態:
如果僅僅是這樣,編輯按鈕與新增按鈕將沒有任何區別,關鍵在於,如何回顯呢?
4.2.回顯資料
回顯資料,就是把當前點選的品牌資料傳遞到子元件(MyBrandForm)。而父元件給子元件傳遞資料,通過props屬性。
-
第一步:在編輯時獲取當前選中的品牌資訊,並且記錄到data中
先在data中定義屬性,用來接收用來編輯的brand資料:
我們在頁面觸發編輯事件時,把當前的brand傳遞給editBrand方法:
<v-btn color="info" @click="editBrand(props.item)">編輯</v-btn>
然後在editBrand中接收資料,賦值給oldBrand:
editItem(oldBrand){ // 使編輯視窗可見 this.dialog = true; // 初始化編輯的資料 this.oldBrand = oldBrand; }
-
第二步:把獲取的brand資料 傳遞給子元件
<!--對話方塊內容--> <v-card-text class="px-5"> <!--這是一個表單--> <my-brand-form @close="close" :oldBrand="oldBrand"></my-brand-form> </v-card-text>
-
第三步:在子元件(MyBrandForm.vue)中通過props接收要編輯的brand資料,Vue會自動完成回顯
接收資料:
通過watch函式監控oldBrand的變化,把值copy到本地的brand:
watch: { oldBrand: {// 監控oldBrand的變化 handler(val) { if(val){ // 注意不要直接賦值,否則這邊的修改會影響到父元件的資料,copy屬性即可 this.brand = Object.deepCopy(val) }else{ // 為空,初始化brand this.brand = { name: '', letter: '', image: '', categories: [] } } }, deep: true } }
-
Object.deepCopy 自定義的物件進行深度複製的方法。
-
需要判斷監聽到的是否為空,如果為空,應該進行初始化
-
測試:發現數據回顯了,除了商品分類以外:
4.3.商品分類回顯
為什麼商品分類沒有回顯?
因為品牌中並沒有商品分類資料。我們需要在進入編輯頁面之前,查詢商品分類資訊:
4.3.1.後臺提供介面
controller
/** * 通過品牌id查詢商品分類 * @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 new ResponseEntity<>(HttpStatus.NOT_FOUND); } return ResponseEntity.ok(list); }
Service
public List<Category> queryByBrandId(Long bid) { return this.categoryMapper.queryByBrandId(bid); }
mapper
因為需要通過中間表進行子查詢,所以這裡要手寫Sql:
/** * 根據品牌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(Long bid);
4.3.2.前臺查詢分類並渲染
我們在編輯頁面開啟之前,先把資料查詢完畢:
editBrand(oldBrand){ // 根據品牌資訊查詢商品分類 this.$http.get("/item/category/bid/" + oldBrand.id) .then(({data}) => { // 控制彈窗可見: this.dialog = true; // 獲取要編輯的brand this.oldBrand = oldBrand // 回顯商品分類 this.oldBrand.categories = data; }) }
再次測試:資料成功回顯了
4.3.3.新增視窗資料干擾
但是,此時卻產生了新問題:新增視窗竟然也有資料?
原因:
如果之前開啟過編輯,那麼在父元件中記錄的oldBrand會保留。下次再開啟視窗,如果是編輯視窗到沒問題,但是新增的話,就會再次顯示上次開啟的品牌資訊了。
解決:
新增視窗開啟前,把資料置空。
addBrand() { // 控制彈窗可見: this.dialog = true; // 把oldBrand變為null this.oldBrand = null; }
4.3.4.提交表單時判斷是新增還是修改
新增和修改是同一個頁面,我們該如何判斷?
父元件中點選按鈕彈出新增或修改的視窗,因此父元件非常清楚接下來是新增還是修改。
因此,最簡單的方案就是,在父元件中定義變數,記錄新增或修改狀態,當彈出頁面時,把這個狀態也傳遞給子元件。
第一步:在父元件中記錄狀態:
第二步:在新增和修改前,更改狀態:
第三步:傳遞給子元件
第四步,子元件接收標記:
標題的動態化:
表單提交動態:
axios除了除了get和post外,還有一個通用的請求方式:
// 將資料提交到後臺 // this.$http.post('/item/brand', this.$qs.stringify(params)) this.$http({ method: this.isEdit ? 'put' : 'post', // 動態判斷是POST還是PUT url: '/item/brand', data: this.$qs.stringify(this.brand) }).then(() => { // 關閉視窗 this.$emit("close"); this.$message.success("儲存成功!"); }) .catch(() => { this.$message.error("儲存失敗!"); });