-
獨立實現品牌新增
-
實現圖片上傳
-
了解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("保存失敗!"); });