fix: OSS客户端直传

This commit is contained in:
2025-12-23 14:34:22 +08:00
parent e9b79b2963
commit 84cafd66ae
7 changed files with 686 additions and 38 deletions

View File

@@ -2,22 +2,67 @@ import { requestClient } from '#/api/request';
/**
* 文件上传
* @param data
* 通过后端服务器上传文件到OSS
*
* @param data 上传数据包含file字段文件对象
* @returns Promise 返回上传结果包含url字段文件URL
*/
export async function uploadFile(data: any) {
return requestClient.upload('upload/image', data);
}
/**
* 文件上传 - 聊天信息
* @param data
* 用于聊天场景的文件上传
*
* @param data 上传数据包含file字段文件对象
* @returns Promise 返回上传结果包含url字段文件URL
*/
export async function uploadChatFile(data: any) {
return requestClient.upload('upload/chat-file', data);
}
/**
* 获取OSS上传签名信息
*
* 该接口用于获取OSS直传所需的签名信息包括
* - accessKeyId: 阿里云访问密钥ID
* - policy: Base64编码的上传策略
* - signature: 签名值
* - host: OSS服务端点URL
* - bucket: 存储桶名称(从后端环境变量获取,返回给前端)
* - key: 文件路径前缀
* - expire: 签名过期时间戳
*
* 使用场景:
* - 前端需要直接上传文件到OSS时先调用此接口获取签名
* - 签名具有时效性,需要在有效期内使用
* - 每次上传前建议重新获取签名,确保签名未过期
*
* 错误处理:
* - 如果后端返回错误会通过requestClient的统一错误处理机制处理
* - 调用方需要捕获异常并进行相应处理
*
* @returns Promise<OssSignature> 返回OSS签名信息对象
* @throws 如果API调用失败会抛出错误
*
* @example
* try {
* const signature = await getOssSignature();
* console.log('获取签名成功', signature);
* } catch (error) {
* console.error('获取签名失败', error);
* }
*/
export async function getOssSignature() {
return requestClient.get('/upload/oss-signature');
}
/**
* 通过文件id集合获取文件信息
* @param data
*
* @param data 请求数据包含fileInfoIds字段文件ID数组或逗号分隔的字符串
* @returns Promise 返回文件信息数组
*/
export async function getFileInfoByIds(data: any) {
return requestClient.post('/sys/fileInfo/getFileInfoByIds', data);

View File

@@ -2,7 +2,10 @@
import { useVModel } from '@vueuse/core';
import { Upload } from 'ant-design-vue';
// 导入上传相关API和工具函数
import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload';
import { preferences } from '@vben/preferences';
import { Icon } from '#/components/icon';
defineOptions({
@@ -19,12 +22,59 @@ const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const customRequest = (e: any) => {
uploadFile({
file: e.file,
}).then((data: any) => {
/**
* 自定义上传请求处理函数
*
* 该函数根据preferences配置的上传方式选择使用OSS直传或后端上传
* - 'direct': OSS直传模式文件直接从浏览器上传到阿里云OSS
* - 'backend': 后端上传模式文件先上传到后端服务器再由后端上传到OSS
*
* 上传方式说明:
* 1. OSS直传direct
* - 调用uploadToOss函数直接上传到OSS
* - 减少服务器负载,上传速度更快
*
* 2. 后端上传backend
* - 调用uploadFile API通过后端服务器上传
* - 兼容现有功能,所有上传逻辑由后端统一处理
*
* 兼容性处理:
* - 两种上传方式返回的数据结构保持一致:{ url: string }
* - 上传成功后将URL赋值给mValue触发组件更新
*
* @param e 上传事件对象包含file字段文件对象
*/
const customRequest = async (e: any) => {
try {
// 从preferences中读取上传方式配置
// uploadMethod可能的值'direct'OSS直传或 'backend'(后端上传)
const uploadMethod = preferences.app.uploadMethod || 'direct';
let data: { url: string };
// 根据配置选择上传方式
if (uploadMethod === 'direct') {
// OSS直传模式文件直接从浏览器上传到OSS
// uploadToOss函数会处理签名获取、文件上传等所有逻辑
data = await uploadToOss({
file: e.file as File,
});
} else {
// 后端上传模式文件先上传到后端服务器再由后端上传到OSS
// uploadFile函数会将文件发送到后端API/upload/image
data = await uploadFile({
file: e.file,
});
}
// 上传成功将URL赋值给mValue
// mValue是双向绑定的值更新后会触发父组件的更新
mValue.value = data.url;
});
} catch (error) {
// 上传失败,记录错误信息
// 注意这里没有显示错误提示如果需要可以添加message.error
console.error('头像上传失败', error);
}
};
const handleRemove = (e: Event) => {
e.stopPropagation();

View File

@@ -7,7 +7,10 @@ import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { useVModel } from '@vueuse/core';
import { message } from 'ant-design-vue';
// 导入上传相关API和工具函数
import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload';
import { preferences } from '@vben/preferences';
const props = defineProps({
value: {
@@ -136,33 +139,82 @@ const insertImageToEditor = (quill: any, imageUrl: string): void => {
};
/**
* 图片上传函数
*
* 该函数根据preferences配置的上传方式选择使用OSS直传或后端上传
* - 'direct': OSS直传模式文件直接从浏览器上传到阿里云OSS
* - 'backend': 后端上传模式文件先上传到后端服务器再由后端上传到OSS
*
* 上传方式说明:
* 1. OSS直传direct
* - 调用uploadToOss函数直接上传到OSS
* - 减少服务器负载,上传速度更快
* - 支持上传进度回调(如果需要可以添加)
*
* 2. 后端上传backend
* - 调用uploadFile API通过后端服务器上传
* - 兼容现有功能,所有上传逻辑由后端统一处理
*
* 兼容性处理:
* - 两种上传方式返回的数据结构保持一致:{ url: string }
* - 上传成功或失败都会显示相应的提示信息
* - 上传完成后将isUpload标记设置为false允许下次上传
*
* @param {File} file - 要上传的图片文件
* @returns {Promise<string>} - 返回上传后的图片URL
* @returns {Promise<string>} - 返回上传后的图片URL,如果上传失败返回空字符串
*/
const uploadImage = async (file: File): Promise<string> => {
try {
// 创建FormData对象用于发送文件数据
const formData = new FormData();
formData.append('file', file);
// 显示上传中的提示
message.loading({ content: '图片上传中...', key: 'imageUpload' });
return uploadFile({
file,
}).then((data: any) => {
message.success({
content: '图片上传成功',
key: 'imageUpload',
duration: 2,
// 从preferences中读取上传方式配置
// uploadMethod可能的值'direct'OSS直传或 'backend'(后端上传)
const uploadMethod = preferences.app.uploadMethod || 'direct';
let data: { url: string };
// 根据配置选择上传方式
if (uploadMethod === 'direct') {
// OSS直传模式文件直接从浏览器上传到OSS
// uploadToOss函数会处理签名获取、文件上传等所有逻辑
data = await uploadToOss({
file: file,
// 如果需要显示上传进度可以添加onProgress回调
// onProgress: (percent) => {
// message.loading({ content: `图片上传中... ${percent}%`, key: 'imageUpload' });
// },
});
isUpload.value = false;
return data.url;
} else {
// 后端上传模式文件先上传到后端服务器再由后端上传到OSS
// uploadFile函数会将文件发送到后端API/upload/image
data = await uploadFile({
file,
});
}
// 上传成功,显示成功提示
message.success({
content: '图片上传成功',
key: 'imageUpload',
duration: 2,
});
// 重置上传标记,允许下次上传
isUpload.value = false;
// 返回上传后的图片URL
return data.url;
} catch (error) {
// 处理上传错误
console.error('图片上传错误:', error);
// 显示错误提示
message.error({ content: '图片上传失败', key: 'imageUpload', duration: 2 });
// 重置上传标记,允许重试
isUpload.value = false;
// 返回空字符串,表示上传失败
return '';
}
};

View File

@@ -4,7 +4,10 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { Modal, Upload } from 'ant-design-vue';
import type { UploadFile } from 'ant-design-vue';
// 导入上传相关API和工具函数
import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload';
import { preferences } from '@vben/preferences';
import { Icon } from '#/components/icon';
defineOptions({
@@ -132,11 +135,36 @@ const initSortable = () => {
});
};
// 使用原有的uploadFile接口与upload-image.vue保持一致
/**
* 自定义上传请求处理函数
*
* 该函数根据preferences配置的上传方式选择使用OSS直传或后端上传
* - 'direct': OSS直传模式文件直接从浏览器上传到阿里云OSS
* - 'backend': 后端上传模式文件先上传到后端服务器再由后端上传到OSS
*
* 两种上传方式的区别:
* 1. OSS直传direct
* - 优点:减少服务器负载,上传速度更快,用户体验更好
* - 缺点:需要后端提供签名接口,配置相对复杂
* - 实现调用uploadToOss函数直接上传到OSS
*
* 2. 后端上传backend
* - 优点:兼容现有功能,所有上传逻辑由后端统一处理
* - 缺点:增加服务器负载,上传速度相对较慢
* - 实现调用uploadFile API通过后端服务器上传
*
* 兼容性处理:
* - 两种上传方式返回的数据结构保持一致:{ url: string }
* - 确保无论使用哪种方式,组件的行为都是一致的
* - 上传成功后,会重新初始化拖拽排序功能
* - 如果上传失败会更新文件状态为error并调用onError回调
*
* @param options 上传选项包含file、onProgress、onSuccess、onError等
*/
const customRequest = async (options: any) => {
const { file, onProgress, onSuccess, onError } = options;
// 创建上传中的文件项
// 创建上传中的文件项用于在UI中显示上传状态
const uploadingFile: FileItem = {
uid: file.uid,
name: file.name,
@@ -144,15 +172,48 @@ const customRequest = async (options: any) => {
url: '',
};
// 将上传中的文件添加到文件列表
fileList.value = [...fileList.value, uploadingFile];
try {
// 使用原有的uploadFile接口
const res = await uploadFile({
file: file,
});
// 从preferences中读取上传方式配置
// uploadMethod可能的值'direct'OSS直传或 'backend'(后端上传)
const uploadMethod = preferences.app.uploadMethod || 'direct';
// 更新文件状态为完成
let res: { url: string };
// 根据配置选择上传方式
if (uploadMethod === 'direct') {
// OSS直传模式文件直接从浏览器上传到OSS
// uploadToOss函数会
// 1. 从后端获取OSS签名信息
// 2. 生成全局唯一的文件名UUID + 时间戳)
// 3. 使用FormData构造POST请求直接上传到OSS
// 4. 处理上传进度和错误
// 5. 返回上传后的文件URL
res = await uploadToOss({
file: file as File,
onProgress: (percent) => {
// 将OSS上传进度传递给组件
// percent范围0-100表示上传百分比
if (onProgress) {
onProgress(percent);
}
},
});
} else {
// 后端上传模式文件先上传到后端服务器再由后端上传到OSS
// uploadFile函数会
// 1. 将文件发送到后端API/upload/image
// 2. 后端接收文件后上传到OSS
// 3. 返回上传后的文件URL
res = await uploadFile({
file: file,
});
}
// 上传成功,更新文件状态为完成
// 将上传结果中的URL赋值给文件项
const updatedFileList = fileList.value.map(item =>
item.uid === file.uid
? { ...item, status: 'done' as const, url: res.url }
@@ -160,17 +221,24 @@ const customRequest = async (options: any) => {
);
fileList.value = updatedFileList;
// 更新组件的modelValue触发父组件的更新
updateModelValue();
// 重新初始化排序
// 重新初始化拖拽排序功能
// 因为文件列表发生了变化,需要重新绑定拖拽事件
nextTick(() => {
initSortable();
});
// 调用成功回调,通知上传组件上传已完成
onSuccess(res);
} catch (error) {
// 上传失败,记录错误信息
console.error('上传失败', error);
// 更新文件状态为错误
// 错误状态的文件会在UI中显示错误图标
const updatedFileList = fileList.value.map(item =>
item.uid === file.uid
? { ...item, status: 'error' as const }
@@ -178,6 +246,8 @@ const customRequest = async (options: any) => {
);
fileList.value = updatedFileList;
// 调用错误回调,通知上传组件上传失败
onError(error);
}
};

View File

@@ -3,8 +3,10 @@ import { computed, ref, watch } from 'vue';
import { Modal, Upload } from 'ant-design-vue';
import type { UploadFile } from 'ant-design-vue';
// 恢复使用原有的上传接口
// 导入上传相关API和工具函数
import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload';
import { preferences } from '@vben/preferences';
import { Icon } from '#/components/icon';
// 定义接口
@@ -68,11 +70,35 @@ const updateModelValue = () => {
emit('update:modelValue', urls);
};
// 使用原有的uploadFile接口
/**
* 自定义上传请求处理函数
*
* 该函数根据preferences配置的上传方式选择使用OSS直传或后端上传
* - 'direct': OSS直传模式文件直接从浏览器上传到阿里云OSS
* - 'backend': 后端上传模式文件先上传到后端服务器再由后端上传到OSS
*
* 两种上传方式的区别:
* 1. OSS直传direct
* - 优点:减少服务器负载,上传速度更快,用户体验更好
* - 缺点:需要后端提供签名接口,配置相对复杂
* - 实现调用uploadToOss函数直接上传到OSS
*
* 2. 后端上传backend
* - 优点:兼容现有功能,所有上传逻辑由后端统一处理
* - 缺点:增加服务器负载,上传速度相对较慢
* - 实现调用uploadFile API通过后端服务器上传
*
* 兼容性处理:
* - 两种上传方式返回的数据结构保持一致:{ url: string }
* - 确保无论使用哪种方式,组件的行为都是一致的
* - 如果上传失败会更新文件状态为error并调用onError回调
*
* @param options 上传选项包含file、onProgress、onSuccess、onError等
*/
const customRequest = async (options: any) => {
const { file, onProgress, onSuccess, onError } = options;
// 创建上传中的文件项
// 创建上传中的文件项用于在UI中显示上传状态
const uploadingFile: FileItem = {
uid: file.uid,
name: file.name,
@@ -80,15 +106,48 @@ const customRequest = async (options: any) => {
url: '',
};
// 将上传中的文件添加到文件列表
fileList.value = [...fileList.value, uploadingFile];
try {
// 使用原有的uploadFile接口
const res = await uploadFile({
file: file,
});
// 从preferences中读取上传方式配置
// uploadMethod可能的值'direct'OSS直传或 'backend'(后端上传)
const uploadMethod = preferences.app.uploadMethod || 'direct';
// 更新文件状态为完成
let res: { url: string };
// 根据配置选择上传方式
if (uploadMethod === 'direct') {
// OSS直传模式文件直接从浏览器上传到OSS
// uploadToOss函数会
// 1. 从后端获取OSS签名信息
// 2. 生成全局唯一的文件名UUID + 时间戳)
// 3. 使用FormData构造POST请求直接上传到OSS
// 4. 处理上传进度和错误
// 5. 返回上传后的文件URL
res = await uploadToOss({
file: file as File,
onProgress: (percent) => {
// 将OSS上传进度传递给组件
// percent范围0-100表示上传百分比
if (onProgress) {
onProgress(percent);
}
},
});
} else {
// 后端上传模式文件先上传到后端服务器再由后端上传到OSS
// uploadFile函数会
// 1. 将文件发送到后端API/upload/image
// 2. 后端接收文件后上传到OSS
// 3. 返回上传后的文件URL
res = await uploadFile({
file: file,
});
}
// 上传成功,更新文件状态为完成
// 将上传结果中的URL赋值给文件项
const updatedFileList = fileList.value.map(item =>
item.uid === file.uid
? { ...item, status: 'done' as const, url: res.url }
@@ -96,11 +155,18 @@ const customRequest = async (options: any) => {
);
fileList.value = updatedFileList;
// 更新组件的modelValue触发父组件的更新
updateModelValue();
// 调用成功回调,通知上传组件上传已完成
onSuccess(res);
} catch (error) {
// 上传失败,记录错误信息
console.error('上传失败', error);
// 更新文件状态为错误
// 错误状态的文件会在UI中显示错误图标
const updatedFileList = fileList.value.map(item =>
item.uid === file.uid
? { ...item, status: 'error' as const }
@@ -108,6 +174,8 @@ const customRequest = async (options: any) => {
);
fileList.value = updatedFileList;
// 调用错误回调,通知上传组件上传失败
onError(error);
}
};

View File

@@ -34,6 +34,24 @@ export const overridesPreferences = defineOverridesPreferences({
// 检查更新的时间间隔,单位为分钟
checkUpdatesInterval: 1,
version: '1.0.0',
/**
* 文件上传方式配置
*
* 可选值:
* - 'direct': OSS直传模式文件直接从浏览器上传到阿里云OSS推荐性能更好
* - 'backend': 后端上传模式文件先上传到后端服务器再由后端上传到OSS兼容模式
*
* 默认值:'direct'OSS直传
*
* 说明:
* - OSS直传模式减少服务器负载上传速度更快但需要后端提供签名接口
* - 后端上传模式:兼容现有功能,所有上传逻辑由后端处理
*
* 切换方式:
* - 可以通过修改此配置值来切换上传方式
* - 配置会持久化到本地存储,刷新页面后仍然有效
*/
uploadMethod: 'direct' as 'direct' | 'backend',
},
sidebar: {
extraCollapse: false,

View File

@@ -0,0 +1,345 @@
/**
* OSS直传工具函数
*
* 该模块实现了阿里云OSS的Web直传功能支持客户端直接上传文件到OSS存储桶
* 参考文档https://help.aliyun.com/zh/oss/user-guide/obtain-signature-information-from-the-server-and-upload-data-to-oss
*
* 主要功能:
* 1. 从后端获取OSS上传签名信息
* 2. 生成全局唯一的文件名UUID + 时间戳)
* 3. 使用FormData构造POST请求直接上传到OSS
* 4. 处理上传进度和错误
* 5. 返回上传后的文件URL
*/
import { getOssSignature } from '#/api/core/upload';
/**
* OSS签名信息接口
* 后端返回的签名数据结构
*/
export interface OssSignature {
/** 阿里云AccessKeyId */
accessKeyId: string;
/** Base64编码的PostPolicy策略 */
policy: string;
/** 签名值 */
signature: string;
/** OSS服务端点URL */
host: string;
/** 存储桶名称(从后端返回,不能硬编码) */
bucket: string;
/** 文件路径前缀(仅作为路径前缀,完整文件名由前端生成) */
key: string;
/** 签名过期时间戳Unix时间戳秒级 */
expire: number;
}
/**
* 上传选项接口
*/
export interface UploadOptions {
/** 要上传的文件对象 */
file: File;
/** 上传进度回调函数参数为0-100的进度百分比 */
onProgress?: (percent: number) => void;
}
/**
* 上传结果接口
*/
export interface UploadResult {
/** 上传后的文件完整URL */
url: string;
/** 文件在OSS中的objectName相对路径 */
objectName: string;
}
/**
* 生成全局唯一的文件名
*
* 文件名格式xk_upload_日期/UUID_时间戳.扩展名
* 例如xk_upload_20250108/550e8400-e29b-41d4-a716-446655440000_1736323200.jpg
*
* 规则说明:
* - 基础路径xk_upload_日期/ 日期格式YYYYMMDD
* - 文件名UUID_时间戳.扩展名
* - UUID使用浏览器原生crypto.randomUUID()生成,确保全局唯一
* - 时间戳使用Unix时间戳秒级通过Math.floor(Date.now() / 1000)获取
* - 扩展名:从原始文件名提取
*
* @param file 文件对象,用于获取文件名和扩展名
* @returns 返回完整的objectName路径
*
* @example
* const objectName = generateObjectName(file);
* // 返回: "xk_upload_20250108/550e8400-e29b-41d4-a716-446655440000_1736323200.jpg"
*/
export function generateObjectName(file: File): string {
// 获取当前日期格式YYYYMMDD
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
const dateStr = `${year}${month}${day}`;
// 生成UUID使用浏览器原生API确保全局唯一
// crypto.randomUUID() 是Web Crypto API的一部分支持现代浏览器
let uuid: string;
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
uuid = crypto.randomUUID();
} else {
// 降级方案如果浏览器不支持randomUUID使用替代方案
// 生成类似UUID格式的随机字符串
uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// 获取Unix时间戳秒级
// Date.now()返回毫秒级时间戳除以1000并向下取整得到秒级时间戳
const timestamp = Math.floor(Date.now() / 1000);
// 获取文件扩展名
// 从文件名中提取扩展名,如果没有扩展名则使用空字符串
const fileName = file.name;
const lastDotIndex = fileName.lastIndexOf('.');
const extension = lastDotIndex > 0 ? fileName.substring(lastDotIndex + 1).toLowerCase() : '';
// 组合成完整的objectName路径
// 格式xk_upload_日期/UUID_时间戳.扩展名
const objectName = `xk_upload_${dateStr}/${uuid}_${timestamp}${extension ? '.' + extension : ''}`;
return objectName;
}
/**
* 从后端获取OSS上传签名信息
*
* 该函数调用后端API获取OSS上传所需的签名信息包括
* - accessKeyId: 阿里云访问密钥ID
* - policy: Base64编码的上传策略
* - signature: 签名值
* - host: OSS服务端点
* - bucket: 存储桶名称(从后端返回,确保配置灵活性)
* - key: 文件路径前缀
* - expire: 签名过期时间
*
* 错误处理:
* - 如果API调用失败会抛出错误
* - 调用方需要捕获错误并进行相应处理
*
* @returns Promise<OssSignature> 返回签名信息对象
* @throws 如果API调用失败或返回数据格式不正确会抛出错误
*
* @example
* try {
* const signature = await getOssSignature();
* console.log('获取签名成功', signature);
* } catch (error) {
* console.error('获取签名失败', error);
* }
*/
export async function getOssSignatureFromBackend(): Promise<OssSignature> {
try {
// 调用后端API获取签名信息
const response = await getOssSignature();
// 验证返回数据的完整性
if (!response || !response.accessKeyId || !response.policy || !response.signature) {
throw new Error('后端返回的签名信息不完整');
}
// 确保bucket字段存在重要bucket必须从后端返回
if (!response.bucket) {
throw new Error('后端未返回bucket信息无法进行上传');
}
return response as OssSignature;
} catch (error) {
// 记录错误信息,便于调试
console.error('获取OSS签名失败:', error);
throw error;
}
}
/**
* 上传文件到OSS
*
* 该函数实现了完整的OSS直传流程
* 1. 从后端获取签名信息
* 2. 生成全局唯一的文件名
* 3. 构造FormData并设置必要的字段
* 4. 使用XMLHttpRequest发送POST请求到OSS
* 5. 处理上传进度和错误
* 6. 返回上传后的文件URL
*
* 上传流程说明:
* - 使用FormData构造multipart/form-data格式的请求
* - 必须包含的字段key, policy, OSSAccessKeyId, signature, file
* - key字段使用生成的objectName
* - 上传到OSS的host端点
* - 上传成功后文件URL格式host/objectName
*
* 进度处理:
* - 使用XMLHttpRequest的upload.onprogress事件监听上传进度
* - 进度值范围0-100表示上传百分比
* - 通过onProgress回调函数通知调用方
*
* 错误处理:
* - 网络错误捕获XMLHttpRequest的错误事件
* - OSS错误解析OSS返回的XML错误响应
* - 签名过期:检查签名是否在有效期内
*
* @param options 上传选项,包含文件和进度回调
* @returns Promise<UploadResult> 返回上传结果包含文件URL和objectName
* @throws 如果上传失败,会抛出包含错误信息的异常
*
* @example
* try {
* const result = await uploadToOss({
* file: fileObject,
* onProgress: (percent) => {
* console.log(`上传进度: ${percent}%`);
* }
* });
* console.log('上传成功文件URL:', result.url);
* } catch (error) {
* console.error('上传失败', error);
* }
*/
export async function uploadToOss(options: UploadOptions): Promise<UploadResult> {
const { file, onProgress } = options;
// 步骤1从后端获取OSS签名信息
// 签名信息包含上传所需的所有认证参数
const signature = await getOssSignatureFromBackend();
// 步骤2生成全局唯一的文件名
// 使用UUID + 时间戳确保文件名全局唯一,避免冲突
const objectName = generateObjectName(file);
// 步骤3构造FormData设置OSS上传所需的字段
// FormData用于构造multipart/form-data格式的请求体
const formData = new FormData();
// key: 文件在OSS中的完整路径objectName
// 这是OSS中文件的唯一标识
formData.append('key', objectName);
// policy: Base64编码的上传策略
// 策略定义了上传的条件,如文件大小限制、允许的文件类型等
formData.append('policy', signature.policy);
// OSSAccessKeyId: 阿里云访问密钥ID
// 用于标识上传请求的发起者
formData.append('OSSAccessKeyId', signature.accessKeyId);
// signature: 签名值
// 使用AccessKeySecret对policy进行签名确保策略未被篡改
formData.append('signature', signature.signature);
// success_action_status: 指定上传成功后OSS返回的HTTP状态码
// 默认是204无内容设置为200后OSS返回200状态码
// 这样前端可以通过200状态码明确判断上传成功
// 注意此字段必须与后端Policy中的条件一致否则会上传失败
formData.append('success_action_status', '200');
// x-oss-object-acl: 指定文件的访问权限ACL
// 设置为 public-read 表示文件可以被公开访问无需签名URL
// 这与后端上传保持一致后端上传后会调用putObjectAcl设置公共读
// 注意此字段必须与后端Policy中的条件一致否则会上传失败
formData.append('x-oss-object-acl', 'public-read');
// file: 要上传的文件
// 必须是最后一个字段OSS要求file字段在FormData的最后
formData.append('file', file);
// 步骤4使用XMLHttpRequest发送POST请求到OSS
// 使用XMLHttpRequest而不是fetch因为需要监听上传进度
return new Promise<UploadResult>((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 监听上传进度
// upload.onprogress事件在上传过程中会多次触发
if (onProgress && xhr.upload) {
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
// 计算上传进度百分比
// loaded: 已上传的字节数
// total: 文件总字节数
const percent = Math.round((event.loaded / event.total) * 100);
onProgress(percent);
}
});
}
// 监听请求完成事件
xhr.addEventListener('load', () => {
// 检查HTTP状态码
// 200-299表示成功204表示成功但无内容
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 204) {
// 上传成功构造文件URL
// URL格式host/objectName
// 注意使用后端返回的host确保配置的灵活性
const fileUrl = `${signature.host}/${objectName}`;
resolve({
url: fileUrl,
objectName: objectName,
});
} else {
// HTTP状态码表示错误尝试解析OSS返回的错误信息
let errorMessage = `上传失败HTTP状态码: ${xhr.status}`;
try {
// OSS错误响应通常是XML格式
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xhr.responseText, 'text/xml');
const errorNode = xmlDoc.querySelector('Error');
if (errorNode) {
const codeNode = errorNode.querySelector('Code');
const messageNode = errorNode.querySelector('Message');
if (codeNode && messageNode) {
errorMessage = `OSS错误 [${codeNode.textContent}]: ${messageNode.textContent}`;
}
}
} catch (parseError) {
// 如果解析失败,使用默认错误信息
console.warn('解析OSS错误响应失败:', parseError);
}
reject(new Error(errorMessage));
}
});
// 监听网络错误事件
// 当网络请求失败时触发(如网络断开、超时等)
xhr.addEventListener('error', () => {
reject(new Error('网络错误无法连接到OSS服务器'));
});
// 监听请求超时事件
// 如果请求时间超过timeout设置的时间会触发此事件
xhr.addEventListener('timeout', () => {
reject(new Error('上传超时:请求时间过长'));
});
// 设置请求超时时间30秒
// 对于大文件,可能需要增加超时时间
xhr.timeout = 30000;
// 打开POST请求
// 使用后端返回的host作为请求URL
xhr.open('POST', signature.host, true);
// 发送请求
// 将FormData作为请求体发送
xhr.send(formData);
});
}