坑点:1、跨域问题:去 OSS 控制台 → 你的 bucket → 权限管理 → 跨域设置
2、权限问题403:后端凭证要给对
/**
* 处理文件上传
*/
async handleUploadForm(param) {
this.multiUploadLoading = true;
const file = param.file;
const data = {
model: this.modelName ? this.modelName : 'product',
pid: this.tableData.pid ? this.tableData.pid : 0,
};
try {
// 定义需要分片上传的文件类型
const needsMultipartUpload = (file) => {
// 视频文件
if (file.type.startsWith('video/')) {
return true;
}
// 文档文件 (doc, docx)
const fileName = file.name.toLowerCase();
return fileName.endsWith('.doc') || fileName.endsWith('.docx');
};
// 对于视频文件,使用阿里云OSS直传
if (needsMultipartUpload(file)) {
let bucketType = 0;
if (file.type.startsWith('video/')) {
bucketType = 1;
}
// 显示上传进度条
this.progressText = '准备上传...';
// 不要直接设置为0,保持之前的状态
// this.multipartUploadProgress = 0;
this.showProgressModal = true;
// 获取STS凭证
const creds = await credentials();
// 创建上传器实例
const uploader = new AliyunOSSUploader();
// 直接从本地存储获取检查点,不依赖上传器实例
const checkpointKey = `aliyun_oss_upload_checkpoint_${file.name}_${file.size}`;
let checkpoint = null;
try {
const checkpointStr = localStorage.getItem(checkpointKey);
if (checkpointStr) {
checkpoint = JSON.parse(checkpointStr);
// 检查是否过期(24小时)
if (Date.now() - checkpoint.timestamp > 24 * 60 * 60 * 1000) {
localStorage.removeItem(checkpointKey);
checkpoint = null;
} else {
console.log('找到检查点:', checkpoint);
}
}
} catch (error) {
console.error('解析检查点失败:', error);
checkpoint = null;
}
if (checkpoint) {
// 计算初始进度
const totalSize = file.size;
const doneParts = checkpoint.doneParts || [];
let uploadedSize = 0;
if (doneParts.length > 0) {
doneParts.forEach((part) => {
if (part.size) {
uploadedSize += part.size;
} else {
uploadedSize += checkpoint.partSize || 5 * 1024 * 1024;
}
});
}
const initialProgress = totalSize > 0 ? Math.round((uploadedSize / totalSize) * 100) : 0;
this.multipartUploadProgress = initialProgress;
this.progressText = `准备续传... ${initialProgress}%`;
} else {
// 新上传,设置为0
this.multipartUploadProgress = 0;
}
// 使用阿里云OSS直传(直接调用阿里云OSS API,不经过后端)
const result = await uploader.multipartUpload(file, creds, bucketType, (progress, key) => {
this.multipartUploadProgress = progress;
this.progressText = `上传中... ${progress}%`;
});
// 上传完成
this.progressText = '上传成功';
this.multipartUploadProgress = 100;
this.showProgressModal = false;
// 存储到接口
try {
await this.saveFileUrl(result, file, bucketType);
this.$message.success('上传成功');
this.multipartUploadProgress = 0;
} catch (saveError) {
console.error('存储文件链接失败:', saveError);
this.$message.error('上传成功,但存储链接失败');
}
this.multiUploadLoading = false;
// 刷新文件列表
this.getFileList();
} else {
// 对于其他文件类型,使用原有上传方式
const formData = new FormData();
formData.append('multipart', file);
this.uploadPic(formData, data);
}
} catch (error) {
console.error('上传失败:', error);
this.$message.error(error.message || '上传失败');
this.showProgressModal = false;
this.multipartUploadProgress = 0;
this.multiUploadLoading = false;
}
},
aliyunOSS.js
import OSS from 'ali-oss';
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB per chunk
const MAX_PARALLEL_UPLOADS = 3; // 并发上传数
const CHECKPOINT_KEY = 'aliyun_oss_upload_checkpoint';
const CHECKPOINT_EXPIRE_TIME = 24 * 60 * 60 * 1000; // 24小时过期
class AliyunOSSUploader {
constructor() {
this.credentials = null;
this.file = null;
this.fileName = null;
this.bucketType = null;
this.bucket = null;
this.endpoint = null;
this.key = null;
this.client = null;
}
async init(credentials) {
this.credentials = credentials;
const { accessKeyId, accessKeySecret, securityToken, endpoint, bucketName, bucketNamePublic } = credentials;
// 清理endpoint,提取region
// endpoint格式: https://oss-cn-hangzhou.aliyuncs.com
// 需要提取为: oss-cn-hangzhou
const cleanEndpoint = endpoint.replace(/^https?:\/\//, '');
this.endpoint = cleanEndpoint;
const region = cleanEndpoint.split('.')[0]; // oss-cn-hangzhou
this.bucket = this.bucketType === 0 ? bucketNamePublic : bucketName;
// 使用STS凭证创建OSS客户端
this.client = new OSS({
region: region,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
stsToken: securityToken,
bucket: this.bucket,
endpoint: `https://${cleanEndpoint}`,
});
return true;
}
getCheckpointKey() {
// 使用文件指纹作为检查点 key,确保同一个文件的检查点可以被复用
if (!this.fileName) {
throw new Error('文件名未设置,无法生成检查点键');
}
if (!this.file || !this.file.size) {
throw new Error('文件大小未设置,无法生成检查点键');
}
const key = `${CHECKPOINT_KEY}_${this.fileName}_${this.file.size}`;
console.debug('检查点键:', key);
return key;
}
saveCheckpoint(key, uploadId, parts) {
const checkpoint = {
uploadId: uploadId,
key: key,
partSize: CHUNK_SIZE,
doneParts: parts || [],
fileName: this.fileName,
fileSize: this.file.size,
timestamp: Date.now(),
};
try {
localStorage.setItem(this.getCheckpointKey(), JSON.stringify(checkpoint));
console.debug('检查点保存:', checkpoint);
} catch (error) {
console.error('保存检查点失败:', error);
}
}
getCheckpoint() {
try {
const checkpointStr = localStorage.getItem(this.getCheckpointKey());
if (!checkpointStr) return null;
const checkpoint = JSON.parse(checkpointStr);
// 检查是否过期(24小时)
if (Date.now() - checkpoint.timestamp > CHECKPOINT_EXPIRE_TIME) {
localStorage.removeItem(this.getCheckpointKey());
return null;
}
console.debug('获取到检查点:', checkpoint);
return checkpoint;
} catch (error) {
console.error('获取检查点失败:', error);
localStorage.removeItem(this.getCheckpointKey());
return null;
}
}
clearCheckpoint() {
try {
console.debug('清除检查点:', this.getCheckpointKey());
localStorage.removeItem(this.getCheckpointKey());
} catch (error) {
console.error('清除检查点失败:', error);
}
}
async multipartUpload(file, credentials, bucketType = 0, onProgress = () => { }) {
this.file = file;
this.fileName = file.name;
this.bucketType = bucketType;
// 初始化
await this.init(credentials);
// 生成唯一文件名
let userInfo;
try {
const userInfoStr = localStorage.getItem('userInfo');
if (!userInfoStr) {
throw new Error('用户信息不存在');
}
userInfo = JSON.parse(userInfoStr);
if (!userInfo || typeof userInfo !== 'object') {
throw new Error('用户信息格式错误');
}
} catch (error) {
console.error('解析用户信息失败:', error);
throw new Error('无法获取用户信息,请重新登录');
}
const key = `${userInfo.userType || 2}-${userInfo.userId || 2}/${file.name}`;
this.key = key;
// 检查是否有断点记录
const checkpoint = this.getCheckpoint();
try {
let result;
if (checkpoint && checkpoint.key === key) {
console.debug('检测到有效断点,准备续传:', {
checkpoint: checkpoint,
file: this.file.name,
size: this.file.size,
totalChunks: Math.ceil(this.file.size / CHUNK_SIZE)
});
// 计算初始进度(基于已完成的部分)并传递给回调
const totalSize = this.file.size;
const doneParts = checkpoint.doneParts || [];
let uploadedSize = 0;
if (doneParts.length > 0) {
// 根据已完成的分片计算已上传大小
doneParts.forEach(part => {
// 如果part对象包含size信息则使用,否则按partSize计算
if (part.size) {
uploadedSize += part.size;
} else {
uploadedSize += checkpoint.partSize || CHUNK_SIZE;
}
});
}
const initialProgress = totalSize > 0 ? Math.round((uploadedSize / totalSize) * 100) : 0;
console.debug('断点续传初始进度计算:', {
uploadedSize: uploadedSize,
totalSize: totalSize,
initialProgress: initialProgress,
doneParts: doneParts,
partSize: checkpoint.partSize || CHUNK_SIZE
});
// 触发初始进度回调
onProgress(initialProgress, key);
// 断点续传
result = await this.resumeUpload(key, onProgress);
} else {
console.debug('无有效断点,开始新上传:', {
file: this.file.name,
size: this.file.size,
totalChunks: Math.ceil(this.file.size / CHUNK_SIZE)
});
// 新上传
result = await this.newUpload(key, onProgress);
}
// 清除断点记录
this.clearCheckpoint();
return {
key: result.name,
url: `https://${this.bucket}.${this.endpoint}/${result.name}`,
etag: result.etag,
};
} catch (error) {
throw error;
}
}
async newUpload(key, onProgress) {
const checkpoint = this.getCheckpoint();
const options = {
partSize: CHUNK_SIZE,
parallel: MAX_PARALLEL_UPLOADS,
progress: (p, cpt) => {
if (cpt) {
this.saveCheckpoint(key, cpt.uploadId, cpt.doneParts || []);
}
const progress = Math.round(p * 100);
onProgress(progress, key);
},
};
if (checkpoint && checkpoint.key === key) {
options.checkpoint = checkpoint;
}
const result = await this.client.multipartUpload(key, this.file, options);
return result;
}
async resumeUpload(key, onProgress) {
console.debug('开始断点续传:', {
key: key,
fileName: this.fileName,
fileSize: this.file.size
});
// 参数校验
if (!key) {
throw new Error('Key参数不能为空');
}
if (typeof onProgress !== 'function') {
throw new Error('onProgress必须是一个函数');
}
// 确保客户端已初始化
if (!this.client) {
throw new Error('OSS客户端未初始化');
}
// 确保文件存在
if (!this.file) {
throw new Error('文件未设置');
}
const checkpoint = this.getCheckpoint();
// 检查断点是否存在
if (!checkpoint) {
throw new Error('没有找到有效的上传断点');
}
// 确保checkpoint中的必要字段存在
if (!checkpoint.uploadId) {
throw new Error('断点数据不完整:缺少uploadId');
}
// 计算初始进度(基于已完成的部分)
const totalSize = this.file.size;
const doneParts = checkpoint.doneParts || [];
let uploadedSize = 0;
if (doneParts.length > 0) {
// 根据已完成的分片计算已上传大小
doneParts.forEach(part => {
// 如果part对象包含size信息则使用,否则按partSize计算
if (part.size) {
uploadedSize += part.size;
} else {
uploadedSize += checkpoint.partSize || CHUNK_SIZE;
}
});
}
const initialProgress = totalSize > 0 ? (uploadedSize / totalSize) : 0;
console.debug('断点续传初始进度详细信息:', {
totalSize: totalSize,
uploadedSize: uploadedSize,
donePartsCount: doneParts.length,
doneParts: doneParts,
initialProgress: initialProgress,
percentage: Math.round(initialProgress * 100)
});
// 构造options对象
const options = {
partSize: CHUNK_SIZE,
parallel: MAX_PARALLEL_UPLOADS,
progress: (p, cpt) => {
if (cpt) {
// 统一安全地访问checkpoint对象的属性
const updatedDoneParts = cpt.doneParts || cpt.uploadedParts || cpt.parts || [];
this.saveCheckpoint(key, cpt.uploadId, updatedDoneParts);
console.debug('上传进度更新:', {
rawProgress: p,
donePartsCount: updatedDoneParts.length,
totalChunks: Math.ceil(totalSize / CHUNK_SIZE),
uploadedChunks: updatedDoneParts.map(part => part.number || part.partNumber)
});
}
// 计算实际进度:初始进度 + 当前新增进度的比例
const actualProgress = Math.min(100, Math.round((initialProgress + p * (1 - initialProgress)) * 100));
onProgress(actualProgress, key);
},
};
// 添加checkpoint配置
options.checkpoint = {
file: this.file, // 确保文件引用存在
name: key, // 确保文件名存在
uploadId: checkpoint.uploadId,
doneParts: checkpoint.doneParts || [],
partSize: checkpoint.partSize || CHUNK_SIZE
};
console.debug('断点续传参数配置:', {
checkpointUploadId: checkpoint.uploadId,
checkpointDoneParts: checkpoint.doneParts,
checkpointPartSize: checkpoint.partSize || CHUNK_SIZE,
options: options
});
let result;
try {
result = await this.client.multipartUpload(key, this.file, options);
console.debug('断点续传成功:', result);
} catch (error) {
console.error('断点续传失败:', error);
// 如果是checkpoint相关错误,清除无效的checkpoint
if (error.message && (error.message.includes('checkpoint') || error.message.includes('replace'))) {
this.clearCheckpoint();
}
throw error;
}
return result;
}
async cancelUpload() {
if (this.client && this.key) {
try {
await this.client.abortMultipartUpload(this.key);
this.clearCheckpoint();
} catch (error) {
console.error('取消上传失败:', error);
}
}
}
}
export default AliyunOSSUploader;