Przeglądaj źródła

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

Xiao_123 9 miesięcy temu
rodzic
commit
85c1877d6d

+ 3 - 0
.env

@@ -11,6 +11,9 @@ SHOPRO_BASE_URL = https://menduner.citupro.com:2443
 SHOPRO_DEV_BASE_URL = https://menduner.citupro.com:2443
 ### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
 
+# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
+SHOPRO_UPLOAD_TYPE = client
+
 # 后端接口前缀(一般不建议调整)
 SHOPRO_API_PATH = /app-api
 

+ 1 - 1
manifest.json

@@ -184,7 +184,7 @@
     "versionCode": 100
   },
   "mp-weixin": {
-    "appid": "wx6decdf12f9e7a061",
+    "appid": "wx5dd538ccc752b03a",
     "setting": {
       "urlCheck": false,
       "minified": true,

+ 3 - 2
package.json

@@ -88,9 +88,10 @@
     }
   },
   "dependencies": {
+    "crypto-js": "^4.2.0",
     "dayjs": "^1.11.7",
-    "lodash": "^4.18.17",
-    "lodash-es": "^4.18.17",
+    "lodash": "^4.17.21",
+    "lodash-es": "^4.17.21",
     "luch-request": "^3.0.8",
     "pinia": "^2.0.33",
     "pinia-plugin-persist-uni": "^1.2.0",

+ 113 - 27
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,72 @@ 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,
-          );
-        }
+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 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;
+    }
+  }
+
+  // 如果是前端直连上传
+  if (UPLOAD_TYPE.CLIENT === import.meta.env.SHOPRO_UPLOAD_TYPE) {
+    // 为上传创建一组 Promise
+    const uploadPromises = files.map(async (file) => {
+      try {
+        // 1.1 获取文件预签名地址
+        const { data: presignedInfo } = await FileApi.getFilePresignedUrl(file.name);
+        // 1.2 获取二进制文件对象
+        const fileBuffer = await convertToArrayBuffer(file);
+
+        // 返回上传的 Promise
+        return new Promise((resolve, reject) => {
+          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);
+              resolve(file);
+            },
+            fail: (err) => {
+              console.error('上传失败:', err);
+              reject(err);
+            },
+          });
+        });
+      } catch (error) {
+        console.error('上传失败:', error);
+        throw error;
       }
-      return files;
     });
+
+    // 等待所有上传完成
+    return await Promise.all(uploadPromises); // 返回已上传的文件列表
+  } else {
+    // 后端上传
+    for (let file of files) {
+      const { data } = await FileApi.uploadFile(file.path);
+      file.url = data;
+    }
+
+    return files;
+  }
 }
 
 function chooseAndUploadFile(
@@ -210,4 +268,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 };

+ 17 - 24
sheep/components/s-uploader/s-uploader.vue

@@ -44,13 +44,7 @@
 
 <script>
   import { chooseAndUploadFile, uploadCloudFiles } from './choose-and-upload-file.js';
-  import {
-    get_file_ext,
-    get_extname,
-    get_files_and_is_max,
-    get_file_info,
-    get_file_data,
-  } from './utils.js';
+  import { get_extname, get_files_and_is_max, get_file_data } from './utils.js';
   import uploadImage from './upload-image.vue';
   import uploadFile from './upload-file.vue';
   import sheep from '@/sheep';
@@ -352,24 +346,23 @@
       /**
        * 选择文件并上传
        */
-      chooseFiles() {
+      async chooseFiles() {
         const _extname = get_extname(this.fileExtname);
         // 获取后缀
-        uniCloud
-          .chooseAndUploadFile({
-            type: this.fileMediatype,
-            compressed: false,
-            sizeType: this.sizeType,
-            // TODO 如果为空,video 有问题
-            extension: _extname.length > 0 ? _extname : undefined,
-            count: this.limitLength - this.files.length, //默认9
-            onChooseFile: this.chooseFileCallback,
-            onUploadProgress: (progressEvent) => {
-              this.setProgress(progressEvent, progressEvent.index);
-            },
-          })
+        await chooseAndUploadFile({
+          type: this.fileMediatype,
+          compressed: false,
+          sizeType: this.sizeType,
+          // TODO 如果为空,video 有问题
+          extension: _extname.length > 0 ? _extname : undefined,
+          count: this.limitLength - this.files.length, //默认9
+          onChooseFile: this.chooseFileCallback,
+          onUploadProgress: (progressEvent) => {
+            this.setProgress(progressEvent, progressEvent.index);
+          },
+        })
           .then((result) => {
-            this.setSuccessAndError(result.tempFiles);
+            this.setSuccessAndError(result);
           })
           .catch((err) => {
             console.log('选择失败', err);
@@ -453,7 +446,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 +580,7 @@
             path: v.path,
             size: v.size,
             fileID: v.fileID,
-            url: v.url,
+            url: v.path,
           });
         });
         return newFilesData;

+ 108 - 85
sheep/util/const.js

@@ -4,107 +4,130 @@
  * 与后端Terminal枚举一一对应
  */
 export const TerminalEnum = {
-  UNKNOWN: 0, // 未知, 目的:在无法解析到 terminal 时,使用它
-  WECHAT_MINI_PROGRAM: 10, //微信小程序
-  WECHAT_WAP: 11, // 微信公众号
-  H5: 20, // H5 网页
-  APP: 31, // 手机 App
-};
-
-/**
- * 将 uni-app 提供的平台转换为后端所需的 terminal值
- *
- * @return 终端
- */
-export const getTerminal = () => {
-  const platformType = uni.getSystemInfoSync().uniPlatform;
-  // 与后端terminal枚举一一对应
-  switch (platformType) {
-    case 'app':
-      return TerminalEnum.APP;
-    case 'web':
-      return TerminalEnum.H5;
-    case 'mp-weixin':
-      return TerminalEnum.WECHAT_MINI_PROGRAM;
-    default:
-      return TerminalEnum.UNKNOWN;
-  }
-};
-
-// ========== MALL - 营销模块 ==========
-
-import dayjs from "dayjs";
-
-/**
- * 优惠类型枚举
- */
-export const PromotionDiscountTypeEnum = {
+    UNKNOWN: 0, // 未知, 目的:在无法解析到 terminal 时,使用它
+    WECHAT_MINI_PROGRAM: 10, //微信小程序
+    WECHAT_WAP: 11, // 微信公众号
+    H5: 20, // H5 网页
+    APP: 31, // 手机 App
+  };
+  
+  /**
+   * 将 uni-app 提供的平台转换为后端所需的 terminal值
+   *
+   * @return 终端
+   */
+  export const getTerminal = () => {
+    const platformType = uni.getSystemInfoSync().uniPlatform;
+    // 与后端terminal枚举一一对应
+    switch (platformType) {
+      case 'app':
+        return TerminalEnum.APP;
+      case 'web':
+        return TerminalEnum.H5;
+      case 'mp-weixin':
+        return TerminalEnum.WECHAT_MINI_PROGRAM;
+      default:
+        return TerminalEnum.UNKNOWN;
+    }
+  };
+  
+  // ========== MALL - 营销模块 ==========
+  
+  import dayjs from 'dayjs';
+  
+  /**
+   * 优惠类型枚举
+   */
+  export const PromotionDiscountTypeEnum = {
     PRICE: {
-        type: 1,
-        name: '满减'
+      type: 1,
+      name: '满减',
     },
     PERCENT: {
-        type: 2,
-        name: '折扣'
-    }
-}
-
-/**
- * 优惠劵模板的有限期类型的枚举
- */
-export const CouponTemplateValidityTypeEnum = {
+      type: 2,
+      name: '折扣',
+    },
+  };
+  
+  /**
+   * 优惠劵模板的有限期类型的枚举
+   */
+  export const CouponTemplateValidityTypeEnum = {
     DATE: {
-        type: 1,
-        name: '固定日期可用'
+      type: 1,
+      name: '固定日期可用',
     },
     TERM: {
-        type: 2,
-        name: '领取之后可用'
-    }
-}
-
-/**
- * 营销的商品范围枚举
- */
-export const PromotionProductScopeEnum = {
+      type: 2,
+      name: '领取之后可用',
+    },
+  };
+  
+  /**
+   * 营销的商品范围枚举
+   */
+  export const PromotionProductScopeEnum = {
     ALL: {
-        scope: 1,
-        name: '通用劵'
+      scope: 1,
+      name: '通用劵',
     },
     SPU: {
-        scope: 2,
-        name: '商品劵'
+      scope: 2,
+      name: '商品劵',
     },
     CATEGORY: {
-        scope: 3,
-        name: '品类劵'
-    }
-}
-
-
-// 时间段的状态枚举
-export const TimeStatusEnum = {
+      scope: 3,
+      name: '品类劵',
+    },
+  };
+  
+  
+  // 时间段的状态枚举
+  export const TimeStatusEnum = {
     WAIT_START: '即将开始',
     STARTED: '进行中',
     END: '已结束',
-}
-
-/**
- * 微信小程序的订阅模版
- */
-export const WxaSubscribeTemplate = {
-  TRADE_ORDER_DELIVERY: "订单发货通知",
-  PROMOTION_COMBINATION_SUCCESS: "拼团结果通知",
-  PAY_WALLET_RECHARGER_SUCCESS: "充值成功通知",
-}
-
-export const getTimeStatusEnum = (startTime, endTime) => {
+  };
+  
+  /**
+   * 微信小程序的订阅模版
+   */
+  export const WxaSubscribeTemplate = {
+    TRADE_ORDER_DELIVERY: '订单发货通知',
+    PROMOTION_COMBINATION_SUCCESS: '拼团结果通知',
+    PAY_WALLET_RECHARGER_SUCCESS: '充值成功通知',
+  };
+  export const PromotionActivityTypeEnum = {
+    NORMAL: {
+      type: 0,
+      name: '普通',
+    },
+    SECKILL: {
+      type: 1,
+      name: '秒杀',
+    },
+    BARGAIN: {
+      type: 2,
+      name: '砍价',
+    },
+    COMBINATION: {
+      type: 3,
+      name: '拼团',
+    },
+    POINT: {
+      type: 4,
+      name: '积分商城',
+    },
+  };
+  
+  export const getTimeStatusEnum = (startTime, endTime) => {
     const now = dayjs();
     if (now.isBefore(startTime)) {
-        return TimeStatusEnum.WAIT_START;
+      return TimeStatusEnum.WAIT_START;
     } else if (now.isAfter(endTime)) {
-        return TimeStatusEnum.END;
+      return TimeStatusEnum.END;
     } else {
-        return TimeStatusEnum.STARTED;
+      return TimeStatusEnum.STARTED;
     }
-}
+  };
+