From f1bb1447bbce73b52d7a0e2bc3ec62e13e87278b Mon Sep 17 00:00:00 2001 From: En Date: Mon, 16 Mar 2026 18:11:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=20uploadFile=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=94=B9=E4=B8=BA=20OSS=20=E7=9B=B4=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 uploadFile 方法使用 OSS 直传方式上传 - 新增 getOssToken 和 uploadToOss 方法 - 返回格式保持兼容,filePath 返回 OSS 完整 URL - 自动添加环境前缀 (dev/test/prod) Co-Authored-By: Claude Opus 4.6 --- reading-platform-frontend/src/api/file.ts | 119 +++++++++++++++++----- 1 file changed, 93 insertions(+), 26 deletions(-) diff --git a/reading-platform-frontend/src/api/file.ts b/reading-platform-frontend/src/api/file.ts index 4cada95..74f5147 100644 --- a/reading-platform-frontend/src/api/file.ts +++ b/reading-platform-frontend/src/api/file.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { buildOssDirPath } from '@/utils/env'; const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'; @@ -16,50 +17,114 @@ export interface DeleteResult { message: string; } +/** + * OSS 直传 Token 响应 + */ +export interface OssToken { + accessid: string; + policy: string; + signature: string; + dir: string; + host: string; + key: string; + expire: number; +} + /** * 文件上传 API */ export const fileApi = { /** - * 上传文件 + * 获取阿里云 OSS 直传 Token + * 自动根据当前环境添加前缀(dev/test/prod) + * + * @param fileName 文件名 + * @param dir 业务目录(如:avatar, course/cover),会自动添加环境前缀 + * @returns OSS 直传 Token + */ + getOssToken: async ( + fileName: string, + dir?: string, + ): Promise => { + // 自动添加环境前缀 + const fullDir = buildOssDirPath(dir); + + const response = await axios.get<{ data: OssToken }>(`${API_BASE}/api/v1/files/oss/token`, { + params: { fileName, dir: fullDir }, + }); + return response.data.data; + }, + + /** + * 直接上传文件到阿里云 OSS + */ + uploadToOss: async ( + file: File, + token: OssToken, + onProgress?: (percent: number) => void, + ): Promise<{ url: string }> => { + const formData = new FormData(); + + // 按照阿里云 OSS PostObject 要求构造表单 + formData.append('OSSAccessKeyId', token.accessid); + formData.append('policy', token.policy); + formData.append('signature', token.signature); + formData.append('key', token.key); + formData.append('x-oss-credential', token.accessid); + formData.append('file', file); + + await axios.post(token.host, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (progressEvent.total && onProgress) { + const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); + onProgress(percentCompleted); + } + }, + }); + + // 上传成功后返回文件访问 URL + return { + url: `${token.host}/${token.key}`, + }; + }, + /** + * 上传文件(使用 OSS 直传方式) + * + * @param file 要上传的文件 + * @param type 文件类型(用于指定 OSS 目录前缀) + * @param _courseId 课程 ID(可选,用于关联业务 - 预留参数) + * @returns 上传结果,包含 OSS 文件 URL */ uploadFile: async ( file: File, type: 'cover' | 'ebook' | 'audio' | 'video' | 'ppt' | 'poster' | 'document' | 'other', - courseId?: number, + _courseId?: number, ): Promise => { - const formData = new FormData(); - formData.append('file', file); - formData.append('type', type); - if (courseId) { - formData.append('courseId', courseId.toString()); - } + // 1. 获取 OSS 直传 Token(自动添加环境前缀) + const token = await getOssToken(file.name, type); - const response = await axios.post( - `${API_BASE}/files/upload`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, - // 添加上传进度回调 - onUploadProgress: (progressEvent) => { - if (progressEvent.total) { - const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); - console.log(`Upload progress: ${percentCompleted}%`); - } - }, - }, - ); + // 2. 上传到 OSS + await uploadToOss(file, token); - return response.data; + // 3. 返回兼容格式的结果 + return { + success: true, + filePath: `${token.host}/${token.key}`, + fileName: file.name, + originalName: file.name, + fileSize: file.size, + mimeType: file.type, + }; }, /** * 删除文件 */ deleteFile: async (filePath: string): Promise => { - const response = await axios.delete(`${API_BASE}/files/delete`, { + const response = await axios.delete(`${API_BASE}/api/v1/files/delete`, { data: { filePath }, }); return response.data; @@ -132,3 +197,5 @@ export const validateFileType = ( export const uploadFile = fileApi.uploadFile; export const deleteFile = fileApi.deleteFile; export const getFileUrl = fileApi.getFileUrl; +export const getOssToken = fileApi.getOssToken; +export const uploadToOss = fileApi.uploadToOss;