import imageCompression from 'browser-image-compression'; import SparkMD5 from 'spark-md5'; import http from './api'; import { OssSignatureData, OssSignatureRequest, RecognitionTaskData, RecognitionTaskRequest, RecognitionResultData, TaskListData } from '../types/api'; /** * 文件上传服务 */ export const uploadService = { /** * 处理并上传文件 */ async uploadFile(file: File): Promise { // ... (保留之前的 uploadFile 逻辑) // 1. 验证文件 if (!this.validateFile(file)) { throw new Error('不支持的文件类型或文件大小超过限制'); } // 2. 计算文件 Hash (使用原始文件,确保去重逻辑基于用户源文件) // 压缩后的文件 Hash 可能会因为压缩过程的微小差异(如时间戳元数据)而不同 const fileHash = await this.calculateMD5(file); // 3. 压缩图片 (如果是图片且超过一定大小) let processedFile = file; if (file.type.startsWith('image/')) { try { processedFile = await this.compressImage(file); } catch (error) { console.warn('图片压缩失败,尝试使用原文件上传', error); } } // 4. 获取上传签名 const signatureData = await this.getOssSignature({ file_hash: fileHash, file_name: processedFile.name, // 使用处理后的文件名(通常相同) }); // 5. 如果需要上传 (repeat = false),则上传到 OSS (上传处理后的文件) if (!signatureData.data?.repeat && signatureData.data?.sign_url) { await this.uploadToOss(signatureData.data.sign_url, processedFile); } if (!signatureData.data) { throw new Error('获取上传签名失败'); } return signatureData.data; }, /** * 创建公式识别任务 */ async createRecognitionTask(path: string, fileHash: string, fileName: string): Promise { const data: RecognitionTaskRequest = { file_url: path, file_hash: fileHash, file_name: fileName, task_type: 'FORMULA' }; return http.post('/formula/recognition', data).then(res => { if (!res.data) throw new Error('创建任务失败: 无返回数据'); return res.data; }); }, /** * 获取任务结果 */ async getTaskResult(taskNo: string): Promise { return http.get(`/formula/recognition/${taskNo}`).then(res => { if (!res.data) throw new Error('获取结果失败: 无返回数据'); return res.data; }); }, /** * 获取任务历史记录列表 */ async getTaskList(taskType: 'FORMULA' = 'FORMULA', page: number = 1, pageSize: number = 5): Promise { return http.get(`/task/list?task_type=${taskType}&page=${page}&page_size=${pageSize}`).then(res => { if (!res.data) throw new Error('获取历史记录失败: 无返回数据'); return res.data; }); }, // ... (保留其他 helper 方法: validateFile, compressImage, calculateMD5, getOssSignature, uploadToOss) /** * 验证文件 */ validateFile(file: File): boolean { const validTypes = ['image/jpeg', 'image/png', 'image/jpg']; // 检查类型 (虽然 input accept 做了限制,但这里再检查一次) // 注意:有些 jpg 文件的 type 可能是 image/jpeg if (!file.type.startsWith('image/')) { // 用户提到支持 jpg, png。 // 暂时只允许图片,虽然 UI 代码里有 application/pdf,但用户需求只提到了图片。 // 根据用户需求:支持 jpg, png 图片上传 if (validTypes.indexOf(file.type) === -1 && !file.name.match(/\.(jpg|jpeg|png)$/i)) { return false; } } // 5MB 限制 (前端压缩前的检查,压缩后应该更小) // 但如果用户传了个很大的文件,压缩可能也花很久。 // 用户说 "文件大小显示5M, 前端进行压缩处理",可能指压缩目标是5M,或者限制原文件5M? // 通常是限制上传大小。如果原文件很大,压缩后小于5M也可以。 // 这里暂时不做严格的源文件大小限制,交给压缩处理,或者设置一个合理的上限比如 20MB。 return true; }, /** * 压缩图片 */ async compressImage(file: File): Promise { const options = { maxSizeMB: 5, // 限制最大 5MB maxWidthOrHeight: 1920, // 限制最大宽/高,防止过大图片 useWebWorker: true, initialQuality: 0.8, }; try { return await imageCompression(file, options); } catch (error) { console.error('Image compression error:', error); throw error; } }, /** * 计算文件 MD5 */ calculateMD5(file: File): Promise { return new Promise((resolve, reject) => { const blobSlice = File.prototype.slice || (File.prototype as any).mozSlice || (File.prototype as any).webkitSlice; const chunkSize = 2097152; // 2MB const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); fileReader.onload = function (e) { if (e.target?.result) { spark.append(e.target.result as ArrayBuffer); } currentChunk++; if (currentChunk < chunks) { loadNext(); } else { resolve(spark.end()); } }; fileReader.onerror = function () { reject('File read failed'); }; function loadNext() { const start = currentChunk * chunkSize; const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } loadNext(); }); }, /** * 获取 OSS 签名 */ async getOssSignature(data: OssSignatureRequest) { return http.post('/oss/signature_url', data); }, /** * 上传文件到 OSS */ async uploadToOss(url: string, file: File): Promise { try { // OSS 直接 PUT 上传,不需要额外的 headers (除非签名里有要求,通常 Content-Type 需要匹配) // 注意:Content-Type 需要根据文件实际类型设置,或者 application/octet-stream // 这里使用文件类型 await fetch(url, { method: 'PUT', body: file, headers: { // 有些 OSS 签名会绑定 Content-Type,如果不一致会报错。 // 这里尽量使用 file.type,如果为空则设为 application/octet-stream // 如果签名不限制 Content-Type,则无所谓。 // 按照常规 OSS PutObject,建议带上 type 'Content-Type': file.type || 'application/octet-stream', } }); } catch (error) { console.error('Upload to OSS failed:', error); throw new Error('文件上传失败'); } } };