From c23e7ac3d018a7e4425a7e7d9f4b970cf2759300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A2=E8=B6=8A?= <552369664@qq.com> Date: Tue, 24 Sep 2024 17:16:38 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E3=80=91=E7=8E=B0=E5=B7=B2=E6=94=AF=E6=8C=81=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=9B=B4=E4=BC=A0=E5=88=B0OSS=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 5 +- package.json | 3 +- sheep/api/infra/file.js | 21 +++ .../s-uploader/choose-and-upload-file.js | 158 ++++++++++++++---- sheep/components/s-uploader/s-uploader.vue | 6 +- 5 files changed, 158 insertions(+), 35 deletions(-) diff --git a/.env b/.env index 6172fd55..a78c4d81 100644 --- a/.env +++ b/.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 diff --git a/package.json b/package.json index d2e3bb5f..cf0e81dd 100644 --- a/package.json +++ b/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", diff --git a/sheep/api/infra/file.js b/sheep/api/infra/file.js index f7f2a964..fdf59ac5 100644 --- a/sheep/api/infra/file.js +++ b/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; diff --git a/sheep/components/s-uploader/choose-and-upload-file.js b/sheep/components/s-uploader/choose-and-upload-file.js index 604ff9d8..64175e4c 100644 --- a/sheep/components/s-uploader/choose-and-upload-file.js +++ b/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, - ); - } +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; - } - 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 }; diff --git a/sheep/components/s-uploader/s-uploader.vue b/sheep/components/s-uploader/s-uploader.vue index 95cfb057..a459d84f 100644 --- a/sheep/components/s-uploader/s-uploader.vue +++ b/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;