feat: add reward code

This commit is contained in:
2025-12-22 17:37:41 +08:00
commit 1226bbe724
34 changed files with 8857 additions and 0 deletions

202
src/lib/uploadService.ts Normal file
View File

@@ -0,0 +1,202 @@
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<OssSignatureData> {
// ... (保留之前的 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<RecognitionTaskData> {
const data: RecognitionTaskRequest = {
file_url: path,
file_hash: fileHash,
file_name: fileName,
task_type: 'FORMULA'
};
return http.post<RecognitionTaskData>('/formula/recognition', data).then(res => {
if (!res.data) throw new Error('创建任务失败: 无返回数据');
return res.data;
});
},
/**
* 获取任务结果
*/
async getTaskResult(taskNo: string): Promise<RecognitionResultData> {
return http.get<RecognitionResultData>(`/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<TaskListData> {
return http.get<TaskListData>(`/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<File> {
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<string> {
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<OssSignatureData>('/oss/signature_url', data);
},
/**
* 上传文件到 OSS
*/
async uploadToOss(url: string, file: File): Promise<void> {
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('文件上传失败');
}
}
};