133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
import { ApiClientBound } from '../decorators';
|
||
import { API_ENDPOINTS } from './common';
|
||
import type {
|
||
UploadChunkRequest,
|
||
UploadChunkResponse,
|
||
MergeChunksRequest,
|
||
MergeChunksResponse,
|
||
CheckUploadStatusRequest,
|
||
CheckUploadStatusResponse
|
||
} from '../types/upload/api';
|
||
import type { AxiosHttpClient } from '../client';
|
||
|
||
/**
|
||
* 上传API服务类
|
||
* 使用自定义实现而非@Request装饰器,以支持分片上传的复杂headers
|
||
*/
|
||
@ApiClientBound
|
||
export class UploadApiService {
|
||
/**
|
||
* 客户端实例,由ApiClientBound装饰器注入
|
||
*/
|
||
declare client: AxiosHttpClient;
|
||
|
||
/**
|
||
* 上传文件分片
|
||
* @param chunkRequest 分片上传请求参数
|
||
* @param chunkBlob 文件分片数据
|
||
* @returns 分片上传响应
|
||
*/
|
||
async uploadChunk(
|
||
chunkRequest: UploadChunkRequest,
|
||
chunkBlob: Blob
|
||
): Promise<UploadChunkResponse> {
|
||
// 创建FormData以支持文件上传
|
||
const formData = new FormData();
|
||
formData.append('chunk', chunkBlob);
|
||
formData.append('chunkIndex', chunkRequest.chunkIndex.toString());
|
||
formData.append('totalChunks', chunkRequest.totalChunks.toString());
|
||
formData.append('fileName', chunkRequest.fileName);
|
||
formData.append('fileType', chunkRequest.fileType);
|
||
formData.append('fileId', chunkRequest.fileId);
|
||
|
||
// 自定义headers,不设置Content-Type,让浏览器自动设置multipart/form-data
|
||
const headers = {
|
||
'X-Chunk-Index': chunkRequest.chunkIndex.toString(),
|
||
'X-Total-Chunks': chunkRequest.totalChunks.toString(),
|
||
'X-File-Id': chunkRequest.fileId,
|
||
'X-Chunk-Size': chunkRequest.chunkSize.toString(),
|
||
'X-Total-Size': chunkRequest.totalSize.toString(),
|
||
// 不设置Content-Type,让浏览器自动处理
|
||
};
|
||
|
||
// 直接使用client实例发送请求,而不通过@Request装饰器
|
||
return await this.client.request({
|
||
method: 'POST',
|
||
url: API_ENDPOINTS.UPLOAD_CHUNK,
|
||
body: formData,
|
||
headers,
|
||
// 允许跨域携带凭证
|
||
withCredentials: true,
|
||
// 禁用默认的Content-Type设置(禁止axios的jsonify处理)
|
||
transformRequest: [(data: any) => data]
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 合并文件分片
|
||
* @param mergeRequest 合并分片请求参数
|
||
* @returns 合并分片响应
|
||
*/
|
||
async mergeChunks(mergeRequest: MergeChunksRequest): Promise<MergeChunksResponse> {
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
'X-File-Id': mergeRequest.fileId,
|
||
'X-Total-Chunks': mergeRequest.totalChunks.toString()
|
||
};
|
||
|
||
return await this.client.request({
|
||
method: 'POST',
|
||
url: API_ENDPOINTS.MERGE_CHUNKS,
|
||
body: mergeRequest,
|
||
headers,
|
||
withCredentials: true
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 检查文件上传状态
|
||
* @param checkRequest 检查上传状态请求参数
|
||
* @returns 上传状态响应
|
||
*/
|
||
async checkUploadStatus(
|
||
checkRequest: CheckUploadStatusRequest
|
||
): Promise<CheckUploadStatusResponse> {
|
||
const headers = {
|
||
'X-File-Id': checkRequest.fileId
|
||
};
|
||
|
||
return await this.client.request({
|
||
method: 'POST',
|
||
url: API_ENDPOINTS.CHECK_UPLOAD_STATUS,
|
||
body: checkRequest,
|
||
headers,
|
||
withCredentials: true
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 生成文件唯一ID
|
||
* @param _fileName 文件名(未使用,保留参数位置)
|
||
* @param fileSize 文件大小
|
||
* @returns 文件唯一ID
|
||
*/
|
||
generateFileId(_fileName: string, fileSize: number): string {
|
||
const timestamp = Date.now().toString(36);
|
||
const random = Math.random().toString(36).substring(2, 9);
|
||
return `${timestamp}_${random}_${fileSize}`;
|
||
}
|
||
|
||
/**
|
||
* 计算分片数量
|
||
* @param fileSize 文件大小
|
||
* @param chunkSize 分片大小
|
||
* @returns 分片数量
|
||
*/
|
||
calculateChunks(fileSize: number, chunkSize: number = 5 * 1024 * 1024): number {
|
||
return Math.ceil(fileSize / chunkSize);
|
||
}
|
||
}
|
||
|
||
// 导出单例实例
|
||
export const uploadApi = new UploadApiService();
|