樂優商城--品牌管理(新增、圖片上傳、FastDNS、修改)


0.學習目標

  • 獨立實現品牌新增

  • 實現圖片上傳

  • 了解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("保存失敗!");
        });
    }
}

 

 
  1. 通過this.$refs.myBrandForm選中表單,然后調用表單的validate方法,進行表單校驗。返回boolean值,true代表校驗通過

  2. 通過解構表達式來獲取brand中的值,categories需要處理,單獨獲取。其它的存入params對象中

  3. 品牌和商品分類的中間表只保存兩者的id,而brand.categories中保存的是對象數組,里面有id和name屬性,因此這里通過數組的map功能轉為id數組,然后通過join方法拼接為字符串

  4. 發起請求

  5. 彈窗提示成功還是失敗,這里用到的是我們的自定義組件功能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

在上傳文件過程中,我們需要對上傳的內容進行校驗:

  1. 校驗文件大小

  2. 校驗文件的媒體類型

  3. 校驗文件的內容

文件大小在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.上傳和下載流程

上傳


  1. Client通過Tracker server查找可用的Storage server。

  2. Tracker server向Client返回一台可用的Storage server的IP地址和端口號。

  3. Client直接通過Tracker server返回的IP地址和端口與其中一台Storage server建立連接並進行文件上傳。

  4. 上傳完成,Storage server返回Client一個文件ID,文件上傳結束。

下載


  1. Client通過Tracker server查找要下載文件所在的的Storage server。

  2. Tracker server向Client返回包含指定文件的某個Storage server的IP地址和端口號。

  3. Client直接通過Tracker server返回的IP地址和端口與其中一台Storage server建立連接並指定要下載文件。

  4. 下載文件成功。

3.4.安裝和使用

參考課前資料的:

3.5.java客戶端

余慶先生提供了一個Java客戶端,但是作為一個C程序員,寫的java代碼可想而知。而且已經很久不維護了。

這里推薦一個開源的FastDFS客戶端,支持最新的SpringBoot2.0。

配置使用極為簡單,支持連接池,支持自動生成縮略圖,狂拽酷炫吊炸天啊,有木有。

地址:tobato/FastDFS_client


接下來,我們就用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("保存失敗!");
});

 

 

5.刪除(作業)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM