前后端分離——基於Vue+Axios+SpringBoot的文件上傳與下載功能的核心實現


1、文件上傳

1.1 功能描述

在頁面選擇一個文件,后端處理:

​ 1、上傳到阿里雲 OSS

​ 2、將文件的 URL、ContentType 等信息保存到數據庫

1.2 頁面搭建

前端使用的框架為 Vue + ElementUI + Axios

template 代碼如下 (略去了template以及唯一的根標簽):

<el-upload
  ref="fileUploadForm"
  action=""
  :multiple="false"
  :auto-upload="false"
  :show-file-list="false"
  :file-list="fileList"
  :http-request="handleFileUploadSubmit"
  :on-change="handleFileListChanged"
>
  <el-row>
    <el-col style="display: flex; text-align: center; width: 400px">
      <el-input :value="currentFileName" placeholder="請選擇文件" readonly/>
      <el-button type="info" round style="margin-left: 20px" size="middle">選擇文件</el-button>
    </el-col>
  </el-row>
</el-upload>

<el-row style="margin-top: 30px; margin-bottom: 30px">
  <el-col style="text-align: center">
    <el-button round @click="clearFileList">清空</el-button>
    <el-button type="success" round style="margin-left: 20px" @click="submitFile">上傳</el-button>
  </el-col>
</el-row>

<el-progress
 type="circle"
 :color="progressColor"
 :width="150"
 :stroke-width="15"
 :percentage="uploadPercentage"
 :format="formatUploadPercentage"
/>

script 代碼如下:

<script lang="js">
  import apiService from 'axios'

  export default {
    name: 'UploadDemo',
    data() {
      return {
        // 當前已選擇的文件的文件名
        currentFileName: '',
        // 待上傳的文件列表
        fileList: [],
        // 上傳進度(百分比,0-100)
        uploadPercentage: 0,
        // 進度條的顏色
        progressColor: 'rgb(0, 0, 255)'
      };
    },
    methods: {

      /**
       * 文件狀態改變時的操作
       * @param {Object} selectedFile 選擇的文件對象
       * */
      handleFileListChanged(selectedFile) {
        // TODO 選擇了文件時可以做一些文件類型的校驗之類的操作
        // 文件列表修改為選擇的文件
        this.fileList = [selectedFile];
        // 提示框展示為選擇的文件的文件名
        this.currentFileName = selectedFile.name;
      },

      /**
       * 清空待上傳的文件列表
       * */
      clearFileList() {
        // 待上傳的文件列表清空
        this.fileList = [];
        // 提示框展示的文件名清空
        this.currentFileName = '';
      },

      /**
       * 點擊【上傳】按鈕時要進行的操作
       * */
      submitFile() {
        // TODO 還可以進行文件校驗等操作
        if (this.fileList.length < 1) {
          this.$alert('請選擇文件后再上傳!', '提示', {
            type: 'warning'
          });
          return;
        }
        // 校驗無誤將要上傳,這里會調用 http-request 所綁定的方法
        this.$refs.fileUploadForm.submit();
      },

      /**
       * 發送文件上傳的網絡請求
       * */
      handleFileUploadSubmit({ file }) {
        // 將進度條的顏色重置為藍色
        this.progressColor = 'rgb(0, 0, 255)';
        // 創建一個表單,用於存放請求數據(主要是文件)
        const fileFormData = new FormData();
        // 在表單中添加要發送給接口的數據
        fileFormData.append('username', 'Alice');
        fileFormData.append('excelFile', file);
        // 使用 axios 發送文件上傳請求,並監聽文件上傳進度
        apiService.post('http://192.168.199.203:2021/business/file/upload', fileFormData, {
          onUploadProgress: progressEvent => {
            if (progressEvent.lengthComputable) {
              const percentage = progressEvent.loaded / progressEvent.total;
              // 此處的"percentage"達到1時,並不意味着已經得到了接口的響應結果,因此只有當得到響應時才將進度置百
              if (percentage < 1) {
                this.uploadPercentage = percentage * 100;
              }
            }
          }
        }).then(({data: respObj}) => {
          if (respObj.result !== 0) {
            // 文件上傳失敗,將進度條置為紅色
            this.progressColor = 'rgb(255, 0, 0)';
            this.$alert('上傳失敗!', '提示', {type: 'error'});
            console.error(respObj.message);
            return;
          }
          // 已成功得到接口的響應結果,將進度條的顏色置為綠色,並將進度置為100%
          this.progressColor = 'rgb(0, 255, 0)';
          this.uploadPercentage = 100;
          this.$alert('上傳成功!', '提示', {type: 'success'});
          console.log('文件信息: ', respObj.file);
        }).catch(error => {
          // 請求失敗,將進度條置為紅色
          this.progressColor = 'rgb(255, 0, 0)';
          this.$alert('上傳失敗!', '提示', {type: 'error'});
          console.error(error);
        });
      },

      /**
       * 進度條內容格式化
       * @param {number} percentage 進度
       * */
      formatUploadPercentage(percentage) {
        // 進度百分比保留兩位小數展示,達到100%時展示"上傳成功"
        return percentage === 100 ? '上傳完成' : `${percentage.toFixed(2)}%`;
      }

</script>

頁面效果如下:

image

1.3 后端接收

/file/upload 接口的實現如下:

/**
 * 文件上傳接口
 * 注:當前服務並不直接進行文件的存儲操作,而是交給另一個專門用來做這個工作的 oss 服務
 * 使用 RestTemplate 調用該服務時:
 *    1、需要使用 MultiValueMap 封裝數據(是 java.util.Map 的一個子接口)
 *    2、文件數據需要封裝為 ByteArrayResource 或 FileSystemResource (均為 AbstractResource 的子類)
 * @param username 操作文件的用戶名
 * @param multipartFile 由 MultipartFile 封裝的上傳的文件
 * @return 包含了操作結果信息的 Map
 */
@PostMapping(value = {"/upload"})
public Map<String, Object> fileUpload(@RequestPart(value = "username") String username,
				      @RequestPart(value = "excelFile") MultipartFile multipartFile) {
  log.info("username: " + username);
  log.info("fileName: " + multipartFile.getOriginalFilename());
  Map<String, Object> respMap = new HashMap<>(2);
  try {
    /*
    * 將文件轉化為輸入流資源
    * 為了避免生成臨時文件,這里使用了 ByteArrayResource,且這里需要重寫其中的 getFilename() 方法
    * */
    ByteArrayResource byteArrayResourceOfFile = new ByteArrayResource(multipartFile.getBytes()) {
      @Override
      public String getFilename() {
        return multipartFile.getOriginalFilename();
      }
    };
    // 創建 MultiValueMap 實例,這里使用 LinkedMultiValueMap 這一實現類來進行實例化
    MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>(2);
    multiValueMap.add("username", username);
    multiValueMap.add("file", byteArrayResourceOfFile);
    // 使用 RestTemplate 調用目標服務,獲得上傳成功后文件的 URL
    String ossProjectServer = "http://192.168.199.203:3021/oss/file/upload";
    FileEntity fileEntity = restTemplate.postForObject(ossProjectServer, multiValueMap, FileEntity.class);
    if (fileEntity == null) {
      log.error("上傳失敗!");
      respMap.put("result", -1);
      respMap.put("message", "上傳失敗!");
      return respMap;
    }
    // 上傳成功,將文件信息存入數據庫
    String fileId = fileDao.saveFile(fileEntity);
    log.info("fileId: " + fileId);
    // 將結果返回
    respMap.put("result", 0);
    respMap.put("file", fileEntity);
    return respMap;
  } catch (Exception e) {
    e.printStackTrace();
    respMap.put("result", -1);
    respMap.put("message", e.getMessage());
    return respMap;
  }

}

用於將文件上傳到 OSS 的服務接口如下:

@PostMapping(value = {"/upload"})
public FileEntity fileHandler(@RequestPart(value = "username") String username,
                              @RequestPart(value = "file") MultipartFile multipartFile) {
  OSS ossClient = null;
  try (
    // 獲取要上傳的文件的輸入流
      InputStream fileInputStream = multipartFile.getInputStream()
  ) {

      // 創建 OSSClient 實例
      ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, secretAccessKey);

      // 獲取當前日期信息並准備 UUID 和時間戳
      LocalDate now = LocalDate.now();
      String datePrefix = now.getYear() + "/" + now.getMonthValue() + "/" + now.getDayOfMonth();
      String uuIdAndTimestamp = UUID.randomUUID().toString() + "-" + System.currentTimeMillis();

      /*
      * 對象名稱的規則如下:
      *     用戶名/年份/月份/日期/uuid-時間戳-文件名.擴展名
      * */
      String objectName
	  	= username + "/" + datePrefix + "/" + uuIdAndTimestamp + "-" + multipartFile.getOriginalFilename();

      log.info("objectName: " + objectName);

      // 將文件上傳到 OSS
      PutObjectResult putObjectResult = ossClient.putObject(bucketName, objectName, fileInputStream);

      if (putObjectResult != null) {
        log.info("putObjectResult.getETag(): " + putObjectResult.getETag());
      }

      // 獲取由 OSS 識別后的文件類型
      OSSObject ossObject = ossClient.getObject(bucketName, objectName);
      ObjectMetadata objectMetadata = ossObject.getObjectMetadata();
      String contentType = objectMetadata.getContentType();

      // 獲取文件路徑
      String createdUrl = "http://" + bucketName + "." + endpoint.substring(endpoint.indexOf("oss")) + "/" + objectName;
      // 將自定義的 FileEntity 實體類返回
      return new FileEntity()
	  .setFileName(multipartFile.getOriginalFilename())
	  .setFileUrl(createdUrl)
	  .setContentType(contentType)
	  .setUsername(username);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    } finally {
      if (ossClient != null) {
        ossClient.shutdown();
      }
    }
  }

注:如果在 ossClient.putObject(bucketName, objectName, fileInputStream) 時,出現了
java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
的錯誤,最簡單直接的方式是將 OSS 的 endpoint 中的 "https" 改為 "http"。

2、文件下載

2.1 功能描述

文件服務器中存有如下的 Excel 文件:

image

數據庫中有tb_student表:

image

在頁面點擊"下載成績單"按鈕后,后端將從數據庫中查到的學生信息寫入到 Excel 文件中,並返回文件的字節數組。

2.2 后端搭建

/file/download 接口的實現如下:

@PostMapping(value = {"/download"})
public Map<String, Object> fileDownload(@RequestBody Map<String, String> dataMap) {
  String fileId = dataMap.get("fileId");
  log.info("fileId: " + fileId);
  Map<String, Object> respMap = new HashMap<>(2);
  if (fileId == null || "".equals(fileId)) {
    respMap.put("result", -1);
    respMap.put("message", "文件Id不能為空");
    return respMap;
  }

  InputStream urlResourceInputStream = null;
  Workbook workbook = null;
  ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  try {
    // 獲取文件的輸入流
    FileEntity fileEntity = fileDao.getFileUrl(fileId);
    UrlResource urlResource = new UrlResource(fileEntity.getFileUrl());
    urlResourceInputStream = urlResource.getInputStream();

    // 從數據庫中查詢出所有的學生成績信息
    List<StudentEntity> studentEntities = studentDao.listAll();
    // 根據文件輸入流創建 Workbook 實例
    workbook = WorkbookFactory.create(urlResourceInputStream);
    // 獲取 Excel 文件中的第一個 Sheet 頁
    Sheet firstSheet = workbook.getSheetAt(0);

    // 創建"宋體"字體
    Font font1 = workbook.createFont();
    font1.setFontName("宋體");
    // 創建"Times New Roman"字體
    Font font2 = workbook.createFont();
    font2.setFontName("Times New Roman");

    // 創建單元格樣式1: "宋體" + 居中
    CellStyle cellStyle1 = workbook.createCellStyle();
    cellStyle1.setFont(font1);
    cellStyle1.setAlignment(HorizontalAlignment.CENTER);
    cellStyle1.setAlignment(HorizontalAlignment.CENTER_SELECTION);

    // 創建單元格樣式2: "Times New Roman" + 居中
    CellStyle cellStyle2 = workbook.createCellStyle();
    cellStyle2.setFont(font2);
    cellStyle2.setAlignment(HorizontalAlignment.CENTER);
    cellStyle2.setAlignment(HorizontalAlignment.CENTER_SELECTION);

    // 創建單元格樣式3: "Times New Roman" + 居中 + 保留兩位小數
    CellStyle cellStyle3 = workbook.createCellStyle();
    cellStyle3.setFont(font2);
    cellStyle3.setAlignment(HorizontalAlignment.CENTER);
    cellStyle3.setAlignment(HorizontalAlignment.CENTER_SELECTION);
    cellStyle3.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00"));

    // 數據長度
    int studentsCount = studentEntities.size();
    // 遍歷學生成績信息,將數據寫入單元格
    for (int i = 0; i < studentsCount; i++) {
      // 獲取當前索引的學生成績信息
      StudentEntity studentEntity = studentEntities.get(i);
      // 創建Excel"行",索引"+1"是因為要跳過表頭行
      Row currentRow = firstSheet.createRow(i + 1);

      // 創建存儲"學號"數據的單元格並賦值
      Cell studentIdCell = currentRow.createCell(0, CellType.STRING);
      studentIdCell.setCellValue(String.valueOf(studentEntity.getStudentId()));
      studentIdCell.setCellStyle(cellStyle2);

      // 創建存儲"姓名"數據的單元格並賦值
      Cell studentNameCell = currentRow.createCell(1, CellType.STRING);
      studentNameCell.setCellValue(studentEntity.getStudentName());
      studentNameCell.setCellStyle(cellStyle1);

      // 創建存儲"性別"數據的單元格並賦值
      Cell studentGenderCell = currentRow.createCell(2, CellType.STRING);
      studentGenderCell.setCellValue(studentEntity.getStudentGender() == 1 ? "男" : "女");
      studentGenderCell.setCellStyle(cellStyle1);

      // 創建存儲"班級"數據的單元格並賦值
      Cell classNameCell = currentRow.createCell(3, CellType.STRING);
      classNameCell.setCellValue(studentEntity.getClassName());
      classNameCell.setCellStyle(cellStyle1);

      // 創建存儲"語文成績"數據的單元格並賦值
      Cell chineseScoreCell = currentRow.createCell(4, CellType.NUMERIC);
      chineseScoreCell.setCellValue(studentEntity.getChineseScore());
      chineseScoreCell.setCellStyle(cellStyle2);

      // 創建存儲"數學成績"數據的單元格並賦值
      Cell mathScoreCell = currentRow.createCell(5, CellType.NUMERIC);
      mathScoreCell.setCellValue(studentEntity.getMathScore());
      mathScoreCell.setCellStyle(cellStyle2);

      // 創建存儲"英語成績"數據的單元格並賦值
      Cell englishScoreCell = currentRow.createCell(6, CellType.NUMERIC);
      englishScoreCell.setCellValue(studentEntity.getEnglishScore());
      englishScoreCell.setCellStyle(cellStyle2);

      // 創建存儲"總分"數據的單元格並賦值
      Cell totalScoreCell = currentRow.createCell(7, CellType.FORMULA);
      // 總分的數據使用 Excel 中的公式自動計算出
      totalScoreCell.setCellFormula("SUM(E" + (i + 2) + ":G" + (i + 2) + ")");
      totalScoreCell.setCellStyle(cellStyle2);

      // 創建存儲"平均分"數據的單元格並賦值
      Cell averageScoreCell = currentRow.createCell(8, CellType.FORMULA);
      // 平均分的數據也使用 Excel 中的公式自動計算出
      averageScoreCell.setCellFormula("AVERAGE(E" + (i + 2) + ":G" + (i + 2) + ")");
      // 設置數據格式: 保留兩位小數
      averageScoreCell.setCellStyle(cellStyle3);
    }
    // 寫入到字節數組輸出流
    workbook.write(byteArrayOutputStream);

    respMap.put("result", 0);
    // 默認的文件名
    respMap.put("name", "成績單.xls");
    // 文件類型
    respMap.put("type", fileEntity.getContentType());
    // 輸出流轉為字節數組返回
    respMap.put("data", byteArrayOutputStream.toByteArray());
    return respMap;
  } catch (Exception e) {
    e.printStackTrace();
    respMap.put("result", -1);
    respMap.put("message", e.getMessage());
    return respMap;
  } finally {
    try {
      if (workbook != null) {
        workbook.close();
      }
      if (urlResourceInputStream != null) {
        urlResourceInputStream.close();
      }
      byteArrayOutputStream.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

2.3 頁面接收

template 代碼如下 (簡化為一個下載按鈕,參數略):

<template>
  <div>
    <hr style="margin-top: 50px"/>
    <el-button type="primary" @click="handleFileDownload">下載成績單</el-button>
  </div>
</template>

methods 代碼如下:

/**
 * 處理文件下載操作
 * */
handleFileDownload() {
  apiService.post('http://192.168.199.203:2021/business/file/download', {
          fileId: '123456789'
  }).then(({data: respObj}) => {
    if (respObj.result !== 0) {
      this.$alert('下載失敗!', { type: 'error' });
      console.error(respObj.message);
      return;
    }
    this.downloadByteArrayFile(respObj.data, '成績單.xls', respObj.type);
  }).catch(error => {
    this.$alert('下載失敗!', { type: 'error' });
    console.error(error);
  });
},

/**
 * 將 Base64 編碼后的數據解碼后由瀏覽器下載
 * @param encodedData Base64編碼后的數據
 * @param fileName 文件名
 * @param contentType MIME
 */
downloadByteArrayFile(encodedData, fileName, contentType) {
  const rawData = atob(encodedData);
  const blobPartsArray = [];
  const sliceLength = 128;
  for (let i = 0; i < rawData.length; i += sliceLength) {
    const sliceString = rawData.substr(i, i + sliceLength);
    const unicodeByteArray = new Array(sliceLength);
    for (let j = 0; j < sliceLength; j++) {
      unicodeByteArray[j] = sliceString.charCodeAt(j);
    }
    blobPartsArray.push(new Uint8Array(unicodeByteArray));
  }
  const blob = new Blob(blobPartsArray, {
    type: contentType
  });
  const objectURL = URL.createObjectURL(blob);
  const hyperLinkElement = document.createElement('a');
  hyperLinkElement.setAttribute('href', objectURL);
  hyperLinkElement.setAttribute('download', fileName);
  hyperLinkElement.click();
}

3、遺留問題

  • https 資源的獲取
  • xlsx 格式的 Excel 文件的寫操作


免責聲明!

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



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