library-picturebook-activity/oss-direct-upload-demo/frontend/file.ts
En b9ed5e17c6 feat: OSS 客户端直传改造(STS Token 签发 + 前端直传 + CORS 自动配置)
后端新增 OssUtils/OssTokenVo/OssCorsInitRunner,通过 STS 临时凭证实现客户端直传 OSS;
前端 upload API 适配直传流程,赛事创建/作品提交/作业/富文本编辑器均已切换;
多环境(dev/test/prod) OSS 配置补全;新增 oss-direct-upload-demo 示例项目及 E2E 测试。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:19:43 +08:00

163 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from "axios";
import { buildOssDirPath } from "./env";
// ==================== 类型定义 ====================
/**
* OSS 直传 Token 响应
*/
export interface OssToken {
/** OSS 访问 ID */
accessid: string;
/** Base64 编码的 Policy */
policy: string;
/** 签名 */
signature: string;
/** 上传目录前缀 */
dir: string;
/** OSS 上传地址https://bucketname.endpoint */
host: string;
/** 完整文件路径dir + 日期路径 + UUID文件名 */
key: string;
/** 过期时间(秒) */
expire: number;
}
/**
* 上传结果
*/
export interface UploadResult {
success: boolean;
filePath: string;
fileName: string;
fileSize: number;
mimeType: string;
}
// ==================== 配置 ====================
/**
* 后端 API 基础地址
* 根据你的项目修改此值
*/
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api";
// ==================== 核心方法 ====================
/**
* 获取阿里云 OSS 直传 Token
*
* @param fileName 文件名(如:图片.jpg
* @param dir 业务目录avatar, course/cover会自动添加环境前缀
* @returns OSS 直传 Token
*/
export async function getOssToken(
fileName: string,
dir?: string,
): Promise<OssToken> {
// 自动添加环境前缀dev/test/prod
const fullDir = buildOssDirPath(dir);
const response = await axios.get<{ code: number; data: OssToken }>(
`${API_BASE_URL}/v1/files/oss/token`,
{
params: { fileName, dir: fullDir },
},
);
return response.data.data;
}
/**
* 直接上传文件到阿里云 OSS
*
* 使用 FormData + POST 方式直传,无需经过后端中转。
* 支持:进度回调、取消上传、超时设置。
*
* @param file 要上传的文件
* @param token OSS Token通过 getOssToken 获取)
* @param options 可选配置
* @returns 上传后的文件 URL
*/
export async function uploadToOss(
file: File,
token: OssToken,
options?: {
/** 上传进度回调0-100 */
onProgress?: (percent: number) => void;
/** 取消信号 */
signal?: AbortSignal;
/** 超时时间(毫秒),默认 5 分钟 */
timeout?: number;
},
): Promise<{ url: string }> {
const opts = options ?? {};
const formData = new FormData();
// 按照阿里云 OSS PostObject 要求构造表单
// 注意file 必须为最后一个表单域
formData.append("success_action_status", "200"); // 成功时返回 200
formData.append("OSSAccessKeyId", token.accessid);
formData.append("policy", token.policy);
formData.append("signature", token.signature);
formData.append("key", token.key);
formData.append("file", file); // file 必须为最后一个表单域
await axios.post(token.host, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
timeout: opts.timeout ?? 1000 * 60 * 5, // 默认 5 分钟
signal: opts.signal,
onUploadProgress: (progressEvent) => {
if (opts.onProgress) {
const percent =
progressEvent.progress != null
? progressEvent.progress * 100
: progressEvent.total
? (progressEvent.loaded * 100) / progressEvent.total
: 0;
opts.onProgress(Math.round(percent));
}
},
});
return {
url: `${token.host}/${token.key}`,
};
}
/**
* 一站式上传文件(获取 Token + 直传 OSS
*
* @param file 要上传的文件
* @param dir 业务目录avatar, course/cover
* @param options 可选配置
* @returns 上传结果
*/
export async function uploadFile(
file: File,
dir?: string,
options?: {
onProgress?: (percent: number) => void;
signal?: AbortSignal;
},
): Promise<UploadResult> {
// 1. 获取 OSS 直传 Token
const token = await getOssToken(file.name, dir);
// 2. 直传到 OSS
await uploadToOss(file, token, {
onProgress: options?.onProgress,
signal: options?.signal,
});
// 3. 返回结果
return {
success: true,
filePath: `${token.host}/${token.key}`,
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
};
}