Переглянути джерело

【功能新增】现已支持前端文件直传到OSS服务器

卢越 10 місяців тому
батько
коміт
c23e7ac3d0

+ 4 - 1
.env

@@ -2,12 +2,15 @@
 SHOPRO_VERSION = v1.8.3
 
 # 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
-SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
+SHOPRO_BASE_URL = http://123.183.21.179:48080
 
 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
 SHOPRO_DEV_BASE_URL = http://127.0.0.1:48080
 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
 
+# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
+SHOPRO_UPLOAD_TYPE = client
+
 # 后端接口前缀(一般不建议调整)
 SHOPRO_API_PATH = /app-api
 

+ 2 - 1
package.json

@@ -94,7 +94,8 @@
     "luch-request": "^3.0.8",
     "pinia": "^2.0.33",
     "pinia-plugin-persist-uni": "^1.2.0",
-    "weixin-js-sdk": "^1.6.0"
+    "weixin-js-sdk": "^1.6.0",
+    "crypto-js": "^4.2.0"
   },
   "devDependencies": {
     "prettier": "^2.8.7",

+ 21 - 0
sheep/api/infra/file.js

@@ -1,4 +1,5 @@
 import { baseUrl, apiPath, tenantId } from '@/sheep/config';
+import request from '@/sheep/request';
 
 const FileApi = {
   // 上传文件
@@ -40,6 +41,26 @@ const FileApi = {
       });
     });
   },
+
+  // 获取文件预签名地址
+  getFilePresignedUrl: (path) => {
+    return request({
+      url: '/infra/file/presigned-url',
+      method: 'GET',
+      params: {
+        path,
+      },
+    });
+  },
+
+  // 创建文件
+  createFile: (data) => {
+    return request({
+      url: '/infra/file/create', // 请求的 URL
+      method: 'POST', // 请求方法
+      data: data, // 要发送的数据
+    });
+  },
 };
 
 export default FileApi;

+ 128 - 30
sheep/components/s-uploader/choose-and-upload-file.js

@@ -1,5 +1,6 @@
 'use strict';
 import FileApi from '@/sheep/api/infra/file';
+import CryptoJS from 'crypto-js';
 
 const ERR_MSG_OK = 'chooseAndUploadFile:ok';
 const ERR_MSG_FAIL = 'chooseAndUploadFile:fail';
@@ -106,7 +107,7 @@ function normalizeChooseAndUploadFileRes(res, fileType) {
       item.name = item.path.substring(item.path.lastIndexOf('/') + 1);
     }
     if (fileType) {
-      item.fileType = fileType;
+      item.type = fileType;
     }
     item.cloudPath = Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.'));
   });
@@ -116,6 +117,28 @@ function normalizeChooseAndUploadFileRes(res, fileType) {
   return res;
 }
 
+function convertToArrayBuffer(uniFile) {
+  return new Promise((resolve, reject) => {
+    const fs = uni.getFileSystemManager();
+
+    fs.readFile({
+      filePath: uniFile.path, // 确保路径正确
+      success: (fileRes) => {
+        try {
+          // 将读取的内容转换为 ArrayBuffer
+          const arrayBuffer = new Uint8Array(fileRes.data).buffer;
+          resolve(arrayBuffer);
+        } catch (error) {
+          reject(new Error(`转换为 ArrayBuffer 失败: ${error.message}`));
+        }
+      },
+      fail: (error) => {
+        reject(new Error(`读取文件失败: ${error.errMsg}`));
+      },
+    });
+  });
+}
+
 function uploadCloudFiles(files, max = 5, onUploadProgress) {
   files = JSON.parse(JSON.stringify(files));
   const len = files.length;
@@ -165,36 +188,61 @@ function uploadCloudFiles(files, max = 5, onUploadProgress) {
   });
 }
 
-function uploadFiles(choosePromise, { onChooseFile, onUploadProgress }) {
-  return choosePromise
-    .then((res) => {
-      if (onChooseFile) {
-        const customChooseRes = onChooseFile(res);
-        if (typeof customChooseRes !== 'undefined') {
-          return Promise.resolve(customChooseRes).then((chooseRes) =>
-            typeof chooseRes === 'undefined' ? res : chooseRes,
-          );
-        }
-      }
-      return res;
-    })
-    .then((res) => {
-      if (res === false) {
-        return {
-          errMsg: ERR_MSG_OK,
-          tempFilePaths: [],
-          tempFiles: [],
-        };
-      }
-      return res;
-    })
-    .then(async (files) => {
-      for (let file of files.tempFiles) {
-        const { data } = await FileApi.uploadFile(file.path);
-        file.url = data;
+async function uploadFiles(choosePromise, { onChooseFile, onUploadProgress }) {
+  // 获取选择的文件
+  const res = await choosePromise;
+  // 处理文件选择回调
+  let files = res.tempFiles || [];
+  if (onChooseFile) {
+    const customChooseRes = onChooseFile(res);
+    if (typeof customChooseRes !== 'undefined') {
+      files = await Promise.resolve(customChooseRes);
+      if (typeof files === 'undefined') {
+        files = res.tempFiles || []; // Fallback
       }
-      return files;
-    });
+    }
+  }
+
+  // 如果是前端直连上传
+  if (UPLOAD_TYPE.CLIENT === import.meta.env.SHOPRO_UPLOAD_TYPE) {
+    for (const file of files) {
+      // 获取二进制文件对象
+      const fileBuffer = await convertToArrayBuffer(file);
+      // 1.1 生成文件名称
+      const fileName = await generateFileName(fileBuffer, file.name);
+      // 1.2 获取文件预签名地址
+      const { data: presignedInfo } = await FileApi.getFilePresignedUrl(file.name);
+      // 1.3 上传文件
+      await uni.request({
+        url: presignedInfo.uploadUrl, // 预签名的上传 URL
+        method: 'PUT', // 使用 PUT 方法
+        header: {
+          'Content-Type': file.type + '/' + file.name.substring(file.name.lastIndexOf('.') + 1), // 设置内容类型
+        },
+        data: fileBuffer, // 文件的路径,适用于小程序
+        success: (res) => {
+          // 1.4. 记录文件信息到后端(异步)
+          createFile(presignedInfo, fileName, file);
+          // 1.5. 重新赋值
+          file.url = presignedInfo.url;
+          file.name = fileName;
+          console.log('上传成功:', res);
+        },
+        fail: (err) => {
+          console.error('上传失败:', err);
+        },
+      });
+    }
+    return files;
+  } else {
+    // 后端上传
+    for (let file of files) {
+      const { data } = await FileApi.uploadFile(file.path);
+      file.url = data;
+    }
+
+    return files;
+  }
 }
 
 function chooseAndUploadFile(
@@ -210,4 +258,54 @@ function chooseAndUploadFile(
   return uploadFiles(chooseAll(opts), opts);
 }
 
+/**
+ * 生成文件名称(使用算法SHA256)
+ * @param arrayBuffer 二进制文件对象
+ * @param fileName  文件名称
+ */
+async function generateFileName(arrayBuffer, fileName) {
+  return new Promise((resolve, reject) => {
+    try {
+      // 创建 WordArray
+      const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(arrayBuffer));
+      // 计算SHA256
+      const sha256 = CryptoJS.SHA256(wordArray).toString();
+      // 拼接后缀
+      const ext = fileName.substring(fileName.lastIndexOf('.'));
+      resolve(`${sha256}${ext}`);
+    } catch (error) {
+      reject(new Error('计算SHA256失败: ' + error.message));
+    }
+  });
+}
+
+/**
+ * 创建文件信息
+ * @param vo 文件预签名信息
+ * @param name 文件名称
+ * @param file 文件
+ */
+function createFile(vo, name, file) {
+  const fileVo = {
+    configId: vo.configId,
+    url: vo.url,
+    path: name,
+    name: file.name,
+    type: file.fileType,
+    size: file.size,
+  };
+  FileApi.createFile(fileVo);
+  return fileVo;
+}
+
+/**
+ * 上传类型
+ */
+const UPLOAD_TYPE = {
+  // 客户端直接上传(只支持S3服务)
+  CLIENT: 'client',
+  // 客户端发送到后端上传
+  SERVER: 'server',
+};
+
 export { chooseAndUploadFile, uploadCloudFiles };

+ 3 - 3
sheep/components/s-uploader/s-uploader.vue

@@ -369,7 +369,7 @@
             },
           })
           .then((result) => {
-            this.setSuccessAndError(result.tempFiles);
+            this.setSuccessAndError(result);
           })
           .catch((err) => {
             console.log('选择失败', err);
@@ -453,7 +453,7 @@
 
           if (index === -1 || !this.files) break;
           if (item.errMsg === 'request:fail') {
-            this.files[index].url = item.path;
+            this.files[index].url = item.url;
             this.files[index].status = 'error';
             this.files[index].errMsg = item.errMsg;
             // this.files[index].progress = -1
@@ -587,7 +587,7 @@
             path: v.path,
             size: v.size,
             fileID: v.fileID,
-            url: v.url,
+            url: v.path,
           });
         });
         return newFilesData;