Prechádzať zdrojové kódy

!103 【功能新增】现已支持前端文件直传到OSS服务
Merge pull request !103 from 卢越/master

芋道源码 10 mesiacov pred
rodič
commit
2ca7f48131

+ 12 - 9
.env

@@ -1,28 +1,31 @@
 # 版本号
-SHOPRO_VERSION = v1.8.3
+SHOPRO_VERSION=v1.8.3
 
 # 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
-SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
+SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
 
 # 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
-SHOPRO_DEV_BASE_URL = http://127.0.0.1:48080
+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
+SHOPRO_API_PATH=/app-api
 
 # 后端 websocket 接口前缀
-SHOPRO_WEBSOCKET_PATH = /infra/ws
+SHOPRO_WEBSOCKET_PATH=/infra/ws
 
 # 开发环境运行端口
-SHOPRO_DEV_PORT = 3000
+SHOPRO_DEV_PORT=3000
 
 # 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地  |  http(s)://xxx.xxx=自定义静态资源地址前缀
-SHOPRO_STATIC_URL = http://test.yudao.iocoder.cn
+SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn
 ### SHOPRO_STATIC_URL = https://file.sheepjs.com
 
 # 是否开启直播  1 开启直播 | 0 关闭直播 (小程序官方后台未审核开通直播权限时请勿开启)
-SHOPRO_MPLIVE_ON = 0
+SHOPRO_MPLIVE_ON=0
 
 # 租户ID 默认 1
-SHOPRO_TENANT_ID = 1
+SHOPRO_TENANT_ID=1

+ 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;

+ 101 - 29
sheep/components/s-uploader/choose-and-upload-file.js

@@ -116,6 +116,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 +187,58 @@ 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) {
+      // 1.1 获取文件预签名地址
+      const { data: presignedInfo } = await FileApi.getFilePresignedUrl(file.name);
+      // 1.2 获取二进制文件对象
+      const fileBuffer = await convertToArrayBuffer(file);
+      // 1.3 上传文件
+      await uni.request({
+        url: presignedInfo.uploadUrl, // 预签名的上传 URL
+        method: 'PUT', // 使用 PUT 方法
+        header: {
+          'Content-Type': file.fileType + '/' + file.name.substring(file.name.lastIndexOf('.') + 1), // 设置内容类型
+        },
+        data: fileBuffer, // 文件的路径,适用于小程序
+        success: (res) => {
+          // 1.4. 记录文件信息到后端(异步)
+          createFile(presignedInfo, file);
+          // 1.5. 重新赋值
+          file.url = presignedInfo.url;
+          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 +254,32 @@ function chooseAndUploadFile(
   return uploadFiles(chooseAll(opts), opts);
 }
 
+/**
+ * 创建文件信息
+ * @param vo 文件预签名信息
+ * @param file 文件
+ */
+function createFile(vo, file) {
+  const fileVo = {
+    configId: vo.configId,
+    url: vo.url,
+    path: file.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;