/** * COS 上传服务 * 实现腾讯云 COS 直传功能 */ import { get, post } from "./request"; import { getBaseUrl } from "./server"; /** * 获取预签名 URL * @param {Object} params - 请求参数 * @param {Date} params.recordTime - 记录时间 * @param {string} params.deptName - 部门名称 * @param {string} params.content - 工作内容 * @param {Array} params.workers - 工作人员列表 * @param {string} params.fileExt - 文件扩展名,默认 .jpg * @param {number} params.imageCount - 图片数量 * @returns {Promise} - 返回预签名 URL 信息 */ export const getUploadUrls = async (params) => { const baseUrl = getBaseUrl(); const url = baseUrl + "api/cos/getUploadUrls"; const requestData = { RecordTime: params.recordTime instanceof Date ? params.recordTime.toISOString() : params.recordTime, DeptName: params.deptName || "", Content: params.content || "", Workers: params.workers || [], FileExt: params.fileExt || ".jpg", ImageCount: params.imageCount || 1 }; console.log("获取预签名 URL 请求:", url, requestData); const res = await post(url, requestData); console.log("获取预签名 URL 响应:", res); if (res.code !== 200) { throw new Error(res.msg || "获取预签名 URL 失败"); } return res.data; }; /** * 直传图片到 COS(使用 PUT 方法) * @param {string} uploadUrl - 预签名上传 URL * @param {string} filePath - 本地文件路径 * @param {number} retryCount - 重试次数,默认 3 * @returns {Promise} - 上传是否成功 */ export const uploadToCos = async (uploadUrl, filePath, retryCount = 3) => { let lastError = null; for (let i = 0; i < retryCount; i++) { try { console.log(`COS 上传尝试 ${i + 1}/${retryCount}:`, uploadUrl); let result = false; // #ifdef H5 // H5 环境使用 fetch API result = await uploadToCosH5(uploadUrl, filePath); // #endif // #ifndef H5 // App/小程序环境使用 uni.uploadFile result = await uploadToCosApp(uploadUrl, filePath); // #endif if (result) { console.log("COS 上传成功"); return true; } } catch (error) { console.error(`COS 上传失败 (尝试 ${i + 1}):`, error); lastError = error; } } throw lastError || new Error("COS 上传失败,已重试 " + retryCount + " 次"); }; /** * H5 环境上传到 COS * @param {string} uploadUrl - 预签名上传 URL * @param {string} filePath - 本地文件路径(blob URL 或 base64) * @returns {Promise} */ const uploadToCosH5 = async (uploadUrl, filePath) => { // 获取文件 Blob let blob; if (filePath.startsWith("blob:")) { // blob URL const response = await fetch(filePath); blob = await response.blob(); } else if (filePath.startsWith("data:")) { // base64 数据 const response = await fetch(filePath); blob = await response.blob(); } else { // 普通 URL const response = await fetch(filePath); blob = await response.blob(); } // 使用 PUT 方法上传 const response = await fetch(uploadUrl, { method: "PUT", body: blob, headers: { "Content-Type": blob.type || "image/jpeg" } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return true; }; /** * App/小程序环境上传到 COS * @param {string} uploadUrl - 预签名上传 URL * @param {string} filePath - 本地文件路径 * @returns {Promise} */ const uploadToCosApp = (uploadUrl, filePath) => { return new Promise((resolve, reject) => { // 先读取文件内容 uni.getFileSystemManager().readFile({ filePath: filePath, success: (fileRes) => { // 使用 uni.request 发送 PUT 请求 uni.request({ url: uploadUrl, method: "PUT", data: fileRes.data, header: { "Content-Type": "image/jpeg" }, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(true); } else { reject(new Error(`HTTP ${res.statusCode}`)); } }, fail: (err) => { reject(err); } }); }, fail: (err) => { // 如果 getFileSystemManager 不可用,尝试使用 uploadFile uni.uploadFile({ url: uploadUrl, filePath: filePath, name: "file", header: { "Content-Type": "image/jpeg" }, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(true); } else { reject(new Error(`HTTP ${res.statusCode}`)); } }, fail: (uploadErr) => { reject(uploadErr); } }); } }); }); }; /** * 并发上传多张图片到 COS * @param {Array} uploadTasks - 上传任务列表 * @param {string} uploadTasks[].filePath - 本地文件路径 * @param {Object} uploadTasks[].uploadUrls - 预签名 URL 对象 * @param {number} concurrency - 最大并发数,默认 3 * @param {Function} onProgress - 进度回调 (completed, total) * @returns {Promise>} - 返回上传结果列表 */ export const uploadImagesToCos = async (uploadTasks, concurrency = 3, onProgress = null) => { const results = []; let completed = 0; const total = uploadTasks.length; // 创建任务队列 const queue = [...uploadTasks]; const executing = []; const executeTask = async (task, index) => { try { // 上传到 Daily 目录 if (task.uploadUrls.daily) { await uploadToCos(task.uploadUrls.daily, task.filePath); } // 上传到 Workers 目录(每个工人一个目录) if (task.uploadUrls.workers) { for (const workerName in task.uploadUrls.workers) { const workerUrl = task.uploadUrls.workers[workerName]; await uploadToCos(workerUrl, task.filePath); } } // 上传到 Content 目录 if (task.uploadUrls.content) { await uploadToCos(task.uploadUrls.content, task.filePath); } // 上传到 Dept 目录 if (task.uploadUrls.dept) { await uploadToCos(task.uploadUrls.dept, task.filePath); } results[index] = { success: true, accessUrl: task.accessUrl }; } catch (error) { console.error(`图片 ${index + 1} 上传失败:`, error); results[index] = { success: false, error: error.message }; } finally { completed++; if (onProgress) { onProgress(completed, total); } } }; // 并发控制 for (let i = 0; i < queue.length; i++) { const task = queue[i]; const promise = executeTask(task, i); executing.push(promise); if (executing.length >= concurrency) { await Promise.race(executing); // 移除已完成的 Promise for (let j = executing.length - 1; j >= 0; j--) { // Promise.race 不会告诉我们哪个完成了,所以我们需要检查 // 这里简化处理,等待所有当前执行的任务完成 } } } // 等待所有任务完成 await Promise.all(executing); return results; }; /** * 批量上传图片到 COS(完整流程) * @param {Array} filePaths - 本地文件路径列表 * @param {Object} recordInfo - 记录信息 * @param {Date} recordInfo.recordTime - 记录时间 * @param {string} recordInfo.deptName - 部门名称 * @param {string} recordInfo.content - 工作内容 * @param {Array} recordInfo.workers - 工作人员列表 * @param {Function} onProgress - 进度回调 (stage, completed, total) * @returns {Promise>} - 返回上传成功的 COS URL 列表 */ export const batchUploadToCos = async (filePaths, recordInfo, onProgress = null) => { if (!filePaths || filePaths.length === 0) { throw new Error("没有要上传的图片"); } // 1. 获取预签名 URL if (onProgress) { onProgress("getting_urls", 0, filePaths.length); } const urlsResponse = await getUploadUrls({ recordTime: recordInfo.recordTime, deptName: recordInfo.deptName, content: recordInfo.content, workers: recordInfo.workers, fileExt: ".jpg", imageCount: filePaths.length }); if (!urlsResponse || !urlsResponse.images || urlsResponse.images.length === 0) { throw new Error("获取预签名 URL 失败"); } // 2. 构建上传任务 const uploadTasks = filePaths.map((filePath, index) => { const imageInfo = urlsResponse.images[index]; return { filePath: filePath, uploadUrls: imageInfo.uploadUrls, accessUrl: imageInfo.accessUrl }; }); // 3. 并发上传 const uploadResults = await uploadImagesToCos( uploadTasks, 3, // 最大并发数 (completed, total) => { if (onProgress) { onProgress("uploading", completed, total); } } ); // 4. 收集成功上传的 URL const successUrls = []; const failedIndexes = []; uploadResults.forEach((result, index) => { if (result.success) { successUrls.push(result.accessUrl); } else { failedIndexes.push(index); } }); if (failedIndexes.length > 0) { console.warn(`${failedIndexes.length} 张图片上传失败:`, failedIndexes); } if (successUrls.length === 0) { throw new Error("所有图片上传失败"); } return successUrls; };