result = new HashMap<>();
+ result.put("code", 200);
+ result.put("message", "success");
+ result.put("data", token);
+ return result;
+ }
+}
diff --git a/oss-direct-upload-demo/backend/OssConfig.java b/oss-direct-upload-demo/backend/OssConfig.java
new file mode 100644
index 0000000..ee3df3d
--- /dev/null
+++ b/oss-direct-upload-demo/backend/OssConfig.java
@@ -0,0 +1,79 @@
+package com.example.oss.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 阿里云 OSS 配置类
+ *
+ * 从 application.yml 中读取 aliyun.oss 前缀的配置项
+ *
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "aliyun.oss")
+public class OssConfig {
+
+ /**
+ * OSS Endpoint(如:oss-cn-hangzhou.aliyuncs.com)
+ */
+ private String endpoint;
+
+ /**
+ * 访问密钥 ID
+ */
+ private String accessKeyId;
+
+ /**
+ * 访问密钥秘密
+ */
+ private String accessKeySecret;
+
+ /**
+ * Bucket 名称
+ */
+ private String bucketName;
+
+ /**
+ * 文件最大大小(字节),默认 10MB
+ */
+ private Long maxFileSize = 10 * 1024 * 1024L;
+
+ /**
+ * 是否在启动时自动配置 OSS Bucket CORS(解决前端直传跨域)
+ */
+ private Boolean corsEnabled = false;
+
+ /**
+ * CORS 允许的来源,逗号分隔(如:http://localhost:5173,https://example.com)
+ * 使用 * 表示允许所有来源
+ */
+ private String corsAllowedOrigins = "http://localhost:5173,http://localhost:5174";
+
+ /**
+ * 允许的文件扩展名
+ */
+ private String[] allowedExtensions = new String[]{
+ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp",
+ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
+ ".mp4", ".avi", ".mov", ".wmv",
+ ".mp3", ".wav",
+ ".txt"
+ };
+
+ /**
+ * 获取完整访问路径(带 Bucket)
+ *
+ * @return 完整访问路径
+ */
+ public String getFullEndpoint() {
+ if (endpoint == null) {
+ return null;
+ }
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
+ return endpoint;
+ }
+ return "https://" + bucketName + "." + endpoint;
+ }
+}
diff --git a/oss-direct-upload-demo/backend/OssCorsInitRunner.java b/oss-direct-upload-demo/backend/OssCorsInitRunner.java
new file mode 100644
index 0000000..bd84142
--- /dev/null
+++ b/oss-direct-upload-demo/backend/OssCorsInitRunner.java
@@ -0,0 +1,40 @@
+package com.example.oss.config;
+
+import com.example.oss.util.OssUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * OSS Bucket CORS 初始化
+ *
+ * 应用启动时自动配置 OSS 跨域规则,解决前端直传跨域问题。
+ * 需在配置中开启:aliyun.oss.cors-enabled=true
+ *
+ *
+ * 工作原理:
+ *
+ * - Spring Boot 启动完成后自动执行
+ * - 读取 aliyun.oss.cors-enabled 配置
+ * - 如果开启,调用 OSS API 设置 Bucket 的 CORS 规则
+ * - 如果失败(如权限不足),仅打印警告,不影响应用启动
+ *
+ *
+ * 也可以不使用此自动配置,改为在阿里云控制台手动设置 CORS。
+ */
+@Slf4j
+@Component
+@Order(100) // 较晚执行,确保其他组件已就绪
+@RequiredArgsConstructor
+public class OssCorsInitRunner implements ApplicationRunner {
+
+ private final OssUtils ossUtils;
+
+ @Override
+ public void run(ApplicationArguments args) {
+ ossUtils.configureBucketCors();
+ }
+}
diff --git a/oss-direct-upload-demo/backend/OssTokenVo.java b/oss-direct-upload-demo/backend/OssTokenVo.java
new file mode 100644
index 0000000..e499f24
--- /dev/null
+++ b/oss-direct-upload-demo/backend/OssTokenVo.java
@@ -0,0 +1,54 @@
+package com.example.oss.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 阿里云 OSS 直传 Token 响应 VO
+ *
+ * 用于前端直传阿里云 OSS(PostObject 方式)
+ *
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OssTokenVo {
+
+ /**
+ * OSS 访问 ID(AccessKeyId)
+ */
+ private String accessid;
+
+ /**
+ * 合法性策略(Base64 编码的 Policy)
+ */
+ private String policy;
+
+ /**
+ * 签名信息
+ */
+ private String signature;
+
+ /**
+ * 上传目录前缀
+ */
+ private String dir;
+
+ /**
+ * OSS 上传地址(https://bucketname.endpoint)
+ */
+ private String host;
+
+ /**
+ * 完整文件路径(dir + fileName)
+ */
+ private String key;
+
+ /**
+ * 过期时间(秒)
+ */
+ private Integer expire;
+}
diff --git a/oss-direct-upload-demo/backend/OssUtils.java b/oss-direct-upload-demo/backend/OssUtils.java
new file mode 100644
index 0000000..e02576d
--- /dev/null
+++ b/oss-direct-upload-demo/backend/OssUtils.java
@@ -0,0 +1,257 @@
+package com.example.oss.util;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.model.SetBucketCORSRequest;
+import com.example.oss.config.OssConfig;
+import com.example.oss.vo.OssTokenVo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 阿里云 OSS 工具类(前端直传专用)
+ *
+ * 仅包含前端直传所需的核心方法:
+ * - generatePostObjectToken: 生成 PostObject 直传 Token
+ * - configureBucketCors: 配置 Bucket CORS 规则
+ *
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OssUtils {
+
+ private final OssConfig ossConfig;
+
+ /**
+ * 获取 OSS 客户端
+ *
+ * @return OSS 客户端
+ */
+ private OSS getOssClient() {
+ return new OSSClientBuilder().build(
+ ossConfig.getEndpoint(),
+ ossConfig.getAccessKeyId(),
+ ossConfig.getAccessKeySecret()
+ );
+ }
+
+ // ==================== 前端直传核心方法 ====================
+
+ /**
+ * 生成阿里云 OSS PostObject 直传 Token
+ *
+ * 用于前端直传文件到 OSS,无需经过后端中转。
+ * 前端拿到此 Token 后,通过 FormData + POST 方式直接上传到 OSS。
+ *
+ *
+ * 签名流程:
+ *
+ * - 构建 Policy JSON(包含过期时间和 key 约束)
+ * - Base64 编码 Policy
+ * - 使用 AccessKeySecret + HMAC-SHA1 对 Base64(Policy) 计算签名
+ *
+ *
+ * @param fileName 原始文件名(如:图片.jpg)
+ * @param dir 目录前缀(可选,如:avatar, course/cover)
+ * @return OSS 直传 Token VO
+ */
+ public OssTokenVo generatePostObjectToken(String fileName, String dir) {
+ // 校验文件名
+ if (fileName == null || fileName.isEmpty()) {
+ throw new IllegalArgumentException("文件名不能为空");
+ }
+
+ // 校验文件扩展名
+ if (!isAllowedExtension(fileName)) {
+ throw new IllegalArgumentException("不支持的文件类型:" + fileName);
+ }
+
+ // 获取文件扩展名
+ String extension = getFileExtension(fileName);
+
+ // 生成唯一文件名(UUID + 原扩展名)
+ String uniqueFilename = UUID.randomUUID().toString().replace("-", "") + extension;
+
+ // 生成日期路径(如:2026-03-16/)
+ String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + "/";
+
+ // 组合存储路径
+ String objectKey;
+ if (dir != null && !dir.isEmpty()) {
+ // 确保 dir 以/结尾
+ if (!dir.endsWith("/")) {
+ dir = dir + "/";
+ }
+ objectKey = dir + datePath + uniqueFilename;
+ } else {
+ objectKey = datePath + uniqueFilename;
+ }
+
+ // 设置过期时间(30 秒)
+ int expire = 30;
+ long expiration = System.currentTimeMillis() + expire * 1000L;
+
+ // 构建 PostPolicy
+ String policy = buildPostPolicy(objectKey, expiration);
+
+ // 计算签名
+ String signature = computeSignature(policy, ossConfig.getAccessKeySecret());
+
+ // 构建上传地址
+ String host = "https://" + ossConfig.getBucketName() + "." + ossConfig.getEndpoint();
+
+ // 返回 Token VO
+ return OssTokenVo.builder()
+ .accessid(ossConfig.getAccessKeyId())
+ .policy(policy)
+ .signature(signature)
+ .dir(dir != null ? dir : "")
+ .host(host)
+ .key(objectKey)
+ .expire(expire)
+ .build();
+ }
+
+ // ==================== CORS 配置 ====================
+
+ /**
+ * 配置 OSS Bucket CORS 规则,解决前端直传跨域问题
+ *
+ * 需确保 OSS 账号有 oss:PutBucketCors 权限。
+ * 也可在阿里云控制台手动配置。
+ *
+ */
+ public void configureBucketCors() {
+ if (!Boolean.TRUE.equals(ossConfig.getCorsEnabled())) {
+ return;
+ }
+ OSS ossClient = getOssClient();
+ try {
+ SetBucketCORSRequest request = new SetBucketCORSRequest(ossConfig.getBucketName());
+ SetBucketCORSRequest.CORSRule rule = new SetBucketCORSRequest.CORSRule();
+
+ // 允许的来源(开发:localhost,生产:需配置实际域名)
+ String origins = ossConfig.getCorsAllowedOrigins();
+ if (origins == null || origins.isBlank()) {
+ origins = "http://localhost:5173,http://localhost:5174";
+ }
+ rule.setAllowedOrigins(Arrays.asList(origins.trim().split("\s*,\s*")));
+
+ // 允许的方法:POST(直传)、GET、PUT、DELETE、HEAD
+ rule.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "HEAD"));
+
+ // 允许的请求头(* 表示全部,PostObject 需要)
+ rule.setAllowedHeaders(Arrays.asList("*"));
+
+ // 暴露给前端的响应头
+ rule.setExposeHeaders(Arrays.asList("ETag", "x-oss-request-id"));
+
+ // 预检请求缓存时间(秒)
+ rule.setMaxAgeSeconds(600);
+
+ request.setCorsRules(List.of(rule));
+ ossClient.setBucketCORS(request);
+ log.info("OSS Bucket CORS 配置成功,允许来源: {}", origins);
+ } catch (Exception e) {
+ log.warn("OSS Bucket CORS 配置失败(可在阿里云控制台手动配置): {}", e.getMessage());
+ } finally {
+ ossClient.shutdown();
+ }
+ }
+
+ // ==================== 私有辅助方法 ====================
+
+ /**
+ * 构建 PostPolicy
+ *
+ * @param objectKey 对象键
+ * @param expiration 过期时间戳
+ * @return Base64 编码的 Policy
+ */
+ private String buildPostPolicy(String objectKey, long expiration) {
+ // ISO 8601 GMT 格式(阿里云要求 YYYY-MM-DDTHH:mm:ss.sssZ,必须以 Z 结尾)
+ String expireTime = java.time.Instant.ofEpochMilli(expiration).toString();
+
+ // 构建 Policy JSON
+ String policyJson = String.format(
+ "{\"expiration\":\"%s\",\"conditions\":[[\"eq\",\"$key\",\"%s\"]]}",
+ expireTime,
+ objectKey
+ );
+
+ // Base64 编码
+ return Base64.getEncoder().encodeToString(policyJson.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * 计算签名
+ *
+ * @param policy Base64 编码的 Policy
+ * @param accessKeySecret OSS 访问密钥
+ * @return 签名(Base64 编码)
+ */
+ private String computeSignature(String policy, String accessKeySecret) {
+ try {
+ // 使用 HMAC-SHA1 算法
+ Mac mac = Mac.getInstance("HmacSHA1");
+ SecretKeySpec keySpec = new SecretKeySpec(
+ accessKeySecret.getBytes(StandardCharsets.UTF_8),
+ "HmacSHA1"
+ );
+ mac.init(keySpec);
+
+ // 计算签名
+ byte[] signData = mac.doFinal(policy.getBytes(StandardCharsets.UTF_8));
+
+ // Base64 编码
+ return Base64.getEncoder().encodeToString(signData);
+ } catch (Exception e) {
+ log.error("计算 OSS 签名失败:{}", e.getMessage(), e);
+ throw new RuntimeException("计算 OSS 签名失败:" + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 检查文件扩展名是否允许
+ *
+ * @param filename 文件名
+ * @return 是否允许
+ */
+ private boolean isAllowedExtension(String filename) {
+ if (filename == null || filename.isEmpty()) {
+ return false;
+ }
+ String extension = getFileExtension(filename).toLowerCase();
+ return Arrays.stream(ossConfig.getAllowedExtensions())
+ .anyMatch(allowed -> allowed.toLowerCase().equals(extension));
+ }
+
+ /**
+ * 获取文件扩展名
+ *
+ * @param filename 文件名
+ * @return 扩展名(包含点)
+ */
+ private String getFileExtension(String filename) {
+ if (filename == null || filename.isEmpty()) {
+ return "";
+ }
+ int lastDot = filename.lastIndexOf(".");
+ if (lastDot == -1) {
+ return "";
+ }
+ return filename.substring(lastDot);
+ }
+}
diff --git a/oss-direct-upload-demo/backend/application-oss.yml b/oss-direct-upload-demo/backend/application-oss.yml
new file mode 100644
index 0000000..570a3fe
--- /dev/null
+++ b/oss-direct-upload-demo/backend/application-oss.yml
@@ -0,0 +1,32 @@
+# ============================================================
+# 阿里云 OSS 配置片段
+# ============================================================
+# 将以下内容复制到你的 application.yml(或 application-dev.yml)中
+# 所有敏感配置建议使用环境变量,不要硬编码
+# ============================================================
+
+aliyun:
+ oss:
+ # OSS Endpoint(不带 http:// 前缀)
+ endpoint: ${OSS_ENDPOINT:oss-cn-hangzhou.aliyuncs.com}
+
+ # 阿里云 AccessKey(建议使用环境变量)
+ access-key-id: ${OSS_ACCESS_KEY_ID:your-access-key-id}
+ access-key-secret: ${OSS_ACCESS_KEY_SECRET:your-access-key-secret}
+
+ # Bucket 名称
+ bucket-name: ${OSS_BUCKET_NAME:your-bucket-name}
+
+ # 文件最大大小(字节),默认 10MB
+ max-file-size: ${OSS_MAX_FILE_SIZE:10485760}
+
+ # 前端直传跨域:启动时自动配置 OSS CORS
+ # 设为 true 时,应用启动会自动调用 OSS API 设置 CORS 规则
+ # 设为 false 时,需要在阿里云控制台手动配置
+ cors-enabled: ${OSS_CORS_ENABLED:true}
+
+ # CORS 允许的来源,逗号分隔
+ # 开发环境:http://localhost:5173,http://localhost:5174
+ # 生产环境:https://your-domain.com
+ # 使用 * 表示允许所有来源(仅建议开发环境)
+ cors-allowed-origins: ${OSS_CORS_ORIGINS:http://localhost:5173,http://localhost:5174}
diff --git a/oss-direct-upload-demo/backend/pom-oss.xml b/oss-direct-upload-demo/backend/pom-oss.xml
new file mode 100644
index 0000000..8fd1715
--- /dev/null
+++ b/oss-direct-upload-demo/backend/pom-oss.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+ com.aliyun.oss
+ aliyun-sdk-oss
+ 3.17.1
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.3.0
+
diff --git a/oss-direct-upload-demo/frontend/UploadDemo.vue b/oss-direct-upload-demo/frontend/UploadDemo.vue
new file mode 100644
index 0000000..959364b
--- /dev/null
+++ b/oss-direct-upload-demo/frontend/UploadDemo.vue
@@ -0,0 +1,210 @@
+
+
+
+
阿里云 OSS 直传上传 Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
上传成功!
+
文件路径:{{ result.filePath }}
+
文件大小:{{ (result.fileSize / 1024).toFixed(1) }} KB
+
![预览]()
+
+
+
+
+
+
+
+
+
+
diff --git a/oss-direct-upload-demo/frontend/env.ts b/oss-direct-upload-demo/frontend/env.ts
new file mode 100644
index 0000000..dd14b32
--- /dev/null
+++ b/oss-direct-upload-demo/frontend/env.ts
@@ -0,0 +1,59 @@
+/**
+ * 环境工具函数
+ *
+ * 提供环境相关的配置,用于自动为 OSS 路径添加环境前缀(dev/test/prod)。
+ * 这样不同环境的文件会存储在不同的目录下,互不干扰。
+ */
+
+/**
+ * OSS 环境前缀映射
+ *
+ * 根据你的项目环境变量名修改此映射。
+ * Vite 默认使用 import.meta.env.MODE,值通常为 development / test / production。
+ */
+const OSS_ENV_PREFIX_MAP: Record = {
+ development: "dev",
+ test: "test",
+ production: "prod",
+};
+
+/**
+ * 获取当前 Vite 环境
+ */
+export function getViteEnv(): string {
+ return import.meta.env.MODE || "development";
+}
+
+/**
+ * 获取 OSS 环境前缀
+ *
+ * @returns OSS 环境前缀(dev/test/prod)
+ */
+export function getOssEnvPrefix(): string {
+ const env = getViteEnv();
+ return OSS_ENV_PREFIX_MAP[env] || "dev";
+}
+
+/**
+ * 构建完整的 OSS 目录路径
+ *
+ * @param bizDir 业务目录(如:avatar, course/cover)
+ * @returns 完整目录路径(如:dev/avatar, test/course/cover)
+ *
+ * @example
+ * buildOssDirPath("avatar") // 开发环境 → "dev/avatar"
+ * buildOssDirPath("course/cover") // 测试环境 → "test/course/cover"
+ * buildOssDirPath() // 生产环境 → "prod"
+ */
+export function buildOssDirPath(bizDir?: string): string {
+ const envPrefix = getOssEnvPrefix();
+
+ if (!bizDir) {
+ return envPrefix;
+ }
+
+ // 移除 bizDir 开头可能存在的环境前缀,避免重复
+ const cleanBizDir = bizDir.replace(/^(dev|test|prod)\//, "");
+
+ return `${envPrefix}/${cleanBizDir}`;
+}
diff --git a/oss-direct-upload-demo/frontend/file.ts b/oss-direct-upload-demo/frontend/file.ts
new file mode 100644
index 0000000..5f55728
--- /dev/null
+++ b/oss-direct-upload-demo/frontend/file.ts
@@ -0,0 +1,162 @@
+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 {
+ // 自动添加环境前缀(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 {
+ // 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,
+ };
+}