Ver código fonte

前端直传oss方案

liudongzhi 1 mês atrás
pai
commit
34ca1cd9d6

+ 300 - 0
alien-store/OSS直传使用说明.md

@@ -0,0 +1,300 @@
+# OSS直传优化方案使用说明
+
+## 概述
+
+本方案实现了阿里云OSS直传功能,支持:
+- ✅ 前端直接上传到OSS(不经过后端服务器)
+- ✅ 分片上传(支持大文件)
+- ✅ 断点续传
+- ✅ 上传进度查询
+- ✅ 上传回调验证
+
+## 核心优势
+
+1. **性能提升**:文件直接上传到OSS,不经过后端服务器,减少服务器带宽和IO压力
+2. **支持大文件**:通过分片上传,支持超大文件上传
+3. **断点续传**:上传中断后可以继续上传,无需重新开始
+4. **安全性**:使用签名验证,确保上传安全
+
+## API接口说明
+
+### 1. 生成OSS直传签名
+
+**接口地址**:`POST /oss/direct/signature`
+
+**请求参数**:
+- `dir`(可选):上传目录,默认 `video/`
+- `fileName`(可选):文件名,不传则自动生成UUID
+- `maxSize`(可选):最大文件大小(字节),默认100MB
+- `expireTime`(可选):过期时间(毫秒),默认1小时
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "data": {
+    "accessKeyId": "LTAI5t...",
+    "policy": "eyJleHBpcmF0aW9uIjoi...",
+    "signature": "abc123...",
+    "dir": "video/",
+    "host": "https://alien-volume.oss-cn-beijing.aliyuncs.com",
+    "expire": "1704067200",
+    "ossKey": "video/uuid-filename.mp4",
+    "callbackUrl": "",
+    "callbackBody": ""
+  }
+}
+```
+
+### 2. 初始化分片上传
+
+**接口地址**:`POST /oss/direct/multipart/init`
+
+**请求参数**:
+- `ossKey`(必需):OSS文件路径,如 `video/filename.mp4`
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "data": {
+    "uploadId": "abc123...",
+    "ossKey": "video/filename.mp4"
+  }
+}
+```
+
+### 3. 上传分片
+
+**接口地址**:`POST /oss/direct/multipart/upload`
+
+**请求参数**:
+- `ossKey`(必需):OSS文件路径
+- `uploadId`(必需):上传ID
+- `partNumber`(必需):分片序号(从1开始)
+- `partData`(必需):分片数据(二进制)
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "data": {
+    "partNumber": 1,
+    "eTag": "\"abc123...\""
+  }
+}
+```
+
+### 4. 完成分片上传(合并)
+
+**接口地址**:`POST /oss/direct/multipart/complete`
+
+**请求参数**:
+- `ossKey`(必需):OSS文件路径
+- `uploadId`(必需):上传ID
+- `partETags`(必需):所有分片的ETag列表(JSON数组)
+
+**请求Body示例**:
+```json
+[
+  {"partNumber": 1, "eTag": "\"abc123...\""},
+  {"partNumber": 2, "eTag": "\"def456...\""},
+  {"partNumber": 3, "eTag": "\"ghi789...\""}
+]
+```
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "data": {
+    "fileUrl": "https://alien-volume.oss-cn-beijing.aliyuncs.com/video/filename.mp4",
+    "ossKey": "video/filename.mp4"
+  }
+}
+```
+
+### 5. 取消分片上传
+
+**接口地址**:`POST /oss/direct/multipart/abort`
+
+**请求参数**:
+- `ossKey`(必需):OSS文件路径
+- `uploadId`(必需):上传ID
+
+### 6. 查询已上传的分片(断点续传)
+
+**接口地址**:`GET /oss/direct/multipart/list`
+
+**请求参数**:
+- `ossKey`(必需):OSS文件路径
+- `uploadId`(必需):上传ID
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "data": [
+    {
+      "partNumber": 1,
+      "eTag": "\"abc123...\"",
+      "size": 5242880
+    },
+    {
+      "partNumber": 2,
+      "eTag": "\"def456...\"",
+      "size": 5242880
+    }
+  ]
+}
+```
+
+### 7. OSS上传回调验证
+
+**接口地址**:`POST /oss/direct/callback`
+
+**请求头**:
+- `Authorization`:OSS回调的Authorization头
+
+**请求参数**:
+- `pubKeyUrl`:公钥URL
+- `callbackBody`:回调Body(JSON格式)
+
+## 使用流程
+
+### 方式一:简单直传(小文件,<100MB)
+
+1. 调用 `/oss/direct/signature` 获取签名
+2. 前端使用签名直接POST到OSS
+3. 上传成功后,OSS会返回文件URL
+
+### 方式二:分片上传(大文件,>100MB)
+
+1. 调用 `/oss/direct/multipart/init` 初始化分片上传,获取 `uploadId`
+2. 将文件分成多个分片(建议每个分片5-10MB)
+3. 对每个分片调用 `/oss/direct/multipart/upload` 上传
+4. 所有分片上传完成后,调用 `/oss/direct/multipart/complete` 合并分片
+5. 获取最终的文件URL
+
+### 方式三:断点续传
+
+1. 调用 `/oss/direct/multipart/list` 查询已上传的分片
+2. 只上传未完成的分片
+3. 所有分片完成后,调用 `/oss/direct/multipart/complete` 合并
+
+## 前端集成示例
+
+### JavaScript示例(简单直传)
+
+```javascript
+// 1. 获取签名
+const response = await fetch('/oss/direct/signature?dir=video/&fileName=test.mp4', {
+  method: 'POST'
+});
+const { data } = await response.json();
+
+// 2. 构建FormData
+const formData = new FormData();
+formData.append('key', data.ossKey);
+formData.append('policy', data.policy);
+formData.append('OSSAccessKeyId', data.accessKeyId);
+formData.append('signature', data.signature);
+formData.append('file', file); // 文件对象
+
+// 3. 直接上传到OSS
+const uploadResponse = await fetch(data.host, {
+  method: 'POST',
+  body: formData
+});
+
+// 4. 上传成功,获取文件URL
+const fileUrl = data.host + '/' + data.ossKey;
+```
+
+### JavaScript示例(分片上传)
+
+```javascript
+// 1. 初始化分片上传
+const initResponse = await fetch('/oss/direct/multipart/init', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+  body: `ossKey=video/test.mp4`
+});
+const { data: initData } = await initResponse.json();
+const { uploadId, ossKey } = initData;
+
+// 2. 分片上传
+const chunkSize = 5 * 1024 * 1024; // 5MB
+const chunks = Math.ceil(file.size / chunkSize);
+const partETags = [];
+
+for (let i = 0; i < chunks; i++) {
+  const start = i * chunkSize;
+  const end = Math.min(start + chunkSize, file.size);
+  const chunk = file.slice(start, end);
+  
+  const chunkResponse = await fetch('/oss/direct/multipart/upload', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+    body: new URLSearchParams({
+      ossKey,
+      uploadId,
+      partNumber: i + 1,
+      partData: await chunk.arrayBuffer()
+    })
+  });
+  
+  const { data: partData } = await chunkResponse.json();
+  partETags.push({
+    partNumber: partData.partNumber,
+    eTag: partData.eTag
+  });
+}
+
+// 3. 完成分片上传
+const completeResponse = await fetch('/oss/direct/multipart/complete', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    ossKey,
+    uploadId,
+    partETags
+  })
+});
+
+const { data: completeData } = await completeResponse.json();
+const fileUrl = completeData.fileUrl;
+```
+
+## 注意事项
+
+1. **文件大小限制**:建议单个分片5-10MB,最多支持10000个分片
+2. **签名过期**:签名默认1小时过期,过期后需要重新获取
+3. **错误处理**:上传失败时,可以调用 `/oss/direct/multipart/abort` 清理未完成的分片
+4. **安全性**:生产环境建议配置OSS回调,验证上传结果
+5. **性能优化**:大文件建议使用分片上传,可以并行上传多个分片提升速度
+
+## 配置说明
+
+确保在 `application.yml` 或 Nacos 中配置了以下OSS参数:
+
+```yaml
+ali:
+  oss:
+    accessKeyId: your-access-key-id
+    accessKeySecret: your-access-key-secret
+    endPoint: oss-cn-beijing.aliyuncs.com
+    bucketName: your-bucket-name
+```
+
+## 故障排查
+
+1. **签名验证失败**:检查AccessKey和Secret是否正确
+2. **分片上传失败**:检查分片大小是否合理(建议5-10MB)
+3. **合并失败**:确保所有分片都已成功上传,且ETag正确
+4. **回调验证失败**:检查公钥URL是否可访问

+ 227 - 0
alien-store/src/main/java/shop/alien/store/controller/OSSDirectUploadController.java

@@ -0,0 +1,227 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.store.util.oss.OSSDirectUploadUtil;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * OSS直传Controller
+ * 支持:签名生成、分片上传、断点续传、上传进度查询
+ *
+ * @author system
+ * @date 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"OSS直传接口"})
+@CrossOrigin
+@RestController
+@RequestMapping("/oss/direct")
+@RequiredArgsConstructor
+public class OSSDirectUploadController {
+
+    private final OSSDirectUploadUtil ossDirectUploadUtil;
+
+    @ApiOperation("生成OSS直传签名")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/signature")
+    public R<OSSDirectUploadUtil.OSSSignatureResult> generateSignature(
+            @RequestParam(required = false, defaultValue = "video/") String dir,
+            @RequestParam(required = false) String fileName,
+            @RequestParam(required = false) Long maxSize,
+            @RequestParam(required = false) Long expireTime) {
+        try {
+            log.info("生成OSS直传签名: dir={}, fileName={}, maxSize={}, expireTime={}", 
+                    dir, fileName, maxSize, expireTime);
+            OSSDirectUploadUtil.OSSSignatureResult result = 
+                    ossDirectUploadUtil.generatePostSignature(dir, fileName, maxSize, expireTime);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("生成OSS直传签名失败: {}", e.getMessage(), e);
+            return R.fail("生成签名失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("初始化分片上传")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/multipart/init")
+    public R<MultipartInitResult> initMultipartUpload(
+            @RequestParam String ossKey) {
+        try {
+            log.info("初始化分片上传: ossKey={}", ossKey);
+            String uploadId = ossDirectUploadUtil.initMultipartUpload(ossKey);
+            MultipartInitResult result = new MultipartInitResult();
+            result.setUploadId(uploadId);
+            result.setOssKey(ossKey);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("初始化分片上传失败: {}", e.getMessage(), e);
+            return R.fail("初始化分片上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("上传分片")
+    @ApiOperationSupport(order = 3)
+    @PostMapping("/multipart/upload")
+    public R<PartUploadResult> uploadPart(
+            @RequestParam String ossKey,
+            @RequestParam String uploadId,
+            @RequestParam Integer partNumber,
+            @RequestBody byte[] partData) {
+        try {
+            log.info("上传分片: ossKey={}, uploadId={}, partNumber={}, size={}", 
+                    ossKey, uploadId, partNumber, partData.length);
+            String eTag = ossDirectUploadUtil.uploadPart(ossKey, uploadId, partNumber, partData);
+            PartUploadResult result = new PartUploadResult();
+            result.setPartNumber(partNumber);
+            result.setETag(eTag);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("上传分片失败: {}", e.getMessage(), e);
+            return R.fail("上传分片失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("完成分片上传(合并)")
+    @ApiOperationSupport(order = 4)
+    @PostMapping("/multipart/complete")
+    public R<CompleteUploadResult> completeMultipartUpload(
+            @RequestParam String ossKey,
+            @RequestParam String uploadId,
+            @RequestBody List<PartETagRequest> partETags) {
+        try {
+            log.info("完成分片上传: ossKey={}, uploadId={}, partCount={}", 
+                    ossKey, uploadId, partETags.size());
+            
+            // 转换为OSS的PartETag列表
+            List<com.aliyun.oss.model.PartETag> ossPartETags = partETags.stream()
+                    .map(p -> new com.aliyun.oss.model.PartETag(p.getPartNumber(), p.getETag()))
+                    .collect(Collectors.toList());
+            
+            String fileUrl = ossDirectUploadUtil.completeMultipartUpload(ossKey, uploadId, ossPartETags);
+            
+            CompleteUploadResult result = new CompleteUploadResult();
+            result.setFileUrl(fileUrl);
+            result.setOssKey(ossKey);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("完成分片上传失败: {}", e.getMessage(), e);
+            return R.fail("完成分片上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("取消分片上传")
+    @ApiOperationSupport(order = 5)
+    @PostMapping("/multipart/abort")
+    public R<String> abortMultipartUpload(
+            @RequestParam String ossKey,
+            @RequestParam String uploadId) {
+        try {
+            log.info("取消分片上传: ossKey={}, uploadId={}", ossKey, uploadId);
+            ossDirectUploadUtil.abortMultipartUpload(ossKey, uploadId);
+            return R.data("取消成功");
+        } catch (Exception e) {
+            log.error("取消分片上传失败: {}", e.getMessage(), e);
+            return R.fail("取消失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("查询已上传的分片列表(用于断点续传)")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/multipart/list")
+    public R<List<PartInfo>> listParts(
+            @RequestParam String ossKey,
+            @RequestParam String uploadId) {
+        try {
+            log.info("查询已上传分片: ossKey={}, uploadId={}", ossKey, uploadId);
+            List<com.aliyun.oss.model.PartSummary> parts = 
+                    ossDirectUploadUtil.listParts(ossKey, uploadId);
+            
+            List<PartInfo> partInfos = parts.stream()
+                    .map(p -> {
+                        PartInfo info = new PartInfo();
+                        info.setPartNumber(p.getPartNumber());
+                        info.setETag(p.getETag());
+                        info.setSize(p.getSize());
+                        return info;
+                    })
+                    .collect(Collectors.toList());
+            
+            return R.data(partInfos);
+        } catch (Exception e) {
+            log.error("查询已上传分片失败: {}", e.getMessage(), e);
+            return R.fail("查询失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("OSS上传回调验证")
+    @ApiOperationSupport(order = 7)
+    @PostMapping("/callback")
+    public R<String> callback(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam(required = false) String pubKeyUrl,
+            @RequestBody(required = false) String callbackBody) {
+        try {
+            log.info("OSS回调验证: authorization={}", authorization);
+            
+            if (authorization == null || pubKeyUrl == null || callbackBody == null) {
+                return R.fail("回调参数不完整");
+            }
+            
+            boolean verified = ossDirectUploadUtil.verifyCallback(authorization, pubKeyUrl, callbackBody);
+            if (verified) {
+                // 解析回调Body,获取上传结果
+                // callbackBody格式通常是JSON,包含bucket、object、etag等信息
+                log.info("OSS回调验证通过: callbackBody={}", callbackBody);
+                return R.data("验证成功");
+            } else {
+                log.warn("OSS回调验证失败");
+                return R.fail("验证失败");
+            }
+        } catch (Exception e) {
+            log.error("OSS回调验证异常: {}", e.getMessage(), e);
+            return R.fail("验证异常: " + e.getMessage());
+        }
+    }
+
+    // ========== 内部类 ==========
+
+    @Data
+    public static class MultipartInitResult {
+        private String uploadId;
+        private String ossKey;
+    }
+
+    @Data
+    public static class PartUploadResult {
+        private Integer partNumber;
+        private String eTag;
+    }
+
+    @Data
+    public static class CompleteUploadResult {
+        private String fileUrl;
+        private String ossKey;
+    }
+
+    @Data
+    public static class PartETagRequest {
+        private Integer partNumber;
+        private String eTag;
+    }
+
+    @Data
+    public static class PartInfo {
+        private Integer partNumber;
+        private String eTag;
+        private Long size;
+    }
+}

+ 377 - 0
alien-store/src/main/java/shop/alien/store/util/oss/OSSDirectUploadUtil.java

@@ -0,0 +1,377 @@
+package shop.alien.store.util.oss;
+
+import com.alibaba.fastjson.JSON;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.common.comm.SignVersion;
+import com.aliyun.oss.common.utils.BinaryUtil;
+import com.aliyun.oss.model.*;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * OSS直传工具类
+ * 支持:签名生成、分片上传、断点续传、上传进度
+ *
+ * @author system
+ * @date 2025-01-XX
+ */
+@Slf4j
+@Component
+@RefreshScope
+public class OSSDirectUploadUtil {
+
+    @Value("${ali.oss.accessKeyId}")
+    private String accessKeyId;
+
+    @Value("${ali.oss.accessKeySecret}")
+    private String accessKeySecret;
+
+    @Value("${ali.oss.endPoint}")
+    private String endPoint;
+
+    @Value("${ali.oss.bucketName}")
+    private String bucketName;
+
+    // 默认过期时间:1小时
+    private static final long DEFAULT_EXPIRE_TIME = 3600 * 1000L;
+
+    /**
+     * 生成OSS直传签名(Post Policy方式)
+     * 
+     * @param dir 上传目录(如:video/、image/)
+     * @param fileName 文件名(可选,不传则自动生成)
+     * @param maxSize 最大文件大小(字节,默认100MB)
+     * @param expireTime 过期时间(毫秒,默认1小时)
+     * @return 签名信息
+     */
+    public OSSSignatureResult generatePostSignature(String dir, String fileName, Long maxSize, Long expireTime) {
+        try {
+            // 参数校验和默认值
+            if (StringUtils.isEmpty(dir)) {
+                dir = "upload/";
+            }
+            if (!dir.endsWith("/")) {
+                dir += "/";
+            }
+            if (StringUtils.isEmpty(fileName)) {
+                fileName = UUID.randomUUID().toString();
+            }
+            if (maxSize == null || maxSize <= 0) {
+                maxSize = 100 * 1024 * 1024L; // 默认100MB
+            }
+            if (expireTime == null || expireTime <= 0) {
+                expireTime = DEFAULT_EXPIRE_TIME;
+            }
+
+            // 生成OSS文件路径
+            String ossKey = dir + fileName;
+            
+            // 过期时间(Unix时间戳,秒)
+            long expire = (System.currentTimeMillis() + expireTime) / 1000;
+            Date expiration = new Date(expire * 1000);
+            
+            // 构建Post Policy
+            String policy = buildPostPolicy(bucketName, ossKey, maxSize, expiration);
+            
+            // 计算签名
+            String signature = calculateSignature(policy);
+            
+            // 构建返回结果
+            OSSSignatureResult result = new OSSSignatureResult();
+            result.setAccessKeyId(accessKeyId);
+            result.setPolicy(policy);
+            result.setSignature(signature);
+            result.setDir(dir);
+            result.setHost("https://" + bucketName + "." + endPoint);
+            result.setExpire(String.valueOf(expire));
+            result.setOssKey(ossKey);
+            result.setCallbackUrl(""); // 回调URL(可选)
+            result.setCallbackBody(""); // 回调Body(可选)
+            
+            log.info("生成OSS直传签名成功: dir={}, fileName={}, ossKey={}", dir, fileName, ossKey);
+            return result;
+            
+        } catch (Exception e) {
+            log.error("生成OSS直传签名失败: {}", e.getMessage(), e);
+            throw new RuntimeException("生成OSS签名失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 构建Post Policy JSON
+     */
+    private String buildPostPolicy(String bucket, String key, long maxSize, Date expiration) {
+        try {
+            Map<String, Object> policyMap = new LinkedHashMap<>();
+            policyMap.put("expiration", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(expiration));
+            
+            List<Object> conditions = new ArrayList<>();
+            conditions.add(new String[]{"eq", "$bucket", bucket});
+            conditions.add(new String[]{"eq", "$key", key});
+            conditions.add(new String[]{"content-length-range", String.valueOf(0), String.valueOf(maxSize)});
+            
+            policyMap.put("conditions", conditions);
+            
+            String policyJson = JSON.toJSONString(policyMap);
+            return BinaryUtil.toBase64String(policyJson.getBytes(StandardCharsets.UTF_8));
+        } catch (Exception e) {
+            log.error("构建Post Policy失败: {}", e.getMessage(), e);
+            throw new RuntimeException("构建Post Policy失败", e);
+        }
+    }
+
+    /**
+     * 计算签名
+     */
+    private String calculateSignature(String policy) {
+        try {
+            Mac hmac = Mac.getInstance("HmacSHA1");
+            hmac.init(new SecretKeySpec(accessKeySecret.getBytes(StandardCharsets.UTF_8), "HmacSHA1"));
+            byte[] signData = hmac.doFinal(policy.getBytes(StandardCharsets.UTF_8));
+            return BinaryUtil.toBase64String(signData);
+        } catch (Exception e) {
+            log.error("计算签名失败: {}", e.getMessage(), e);
+            throw new RuntimeException("计算签名失败", e);
+        }
+    }
+
+    /**
+     * 初始化分片上传
+     * 
+     * @param ossKey OSS文件路径
+     * @return 上传ID
+     */
+    public String initMultipartUpload(String ossKey) {
+        OSS ossClient = null;
+        try {
+            ossClient = createOSSClient();
+            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, ossKey);
+            InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
+            String uploadId = result.getUploadId();
+            log.info("初始化分片上传成功: ossKey={}, uploadId={}", ossKey, uploadId);
+            return uploadId;
+        } catch (Exception e) {
+            log.error("初始化分片上传失败: ossKey={}, error={}", ossKey, e.getMessage(), e);
+            throw new RuntimeException("初始化分片上传失败: " + e.getMessage(), e);
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 上传分片
+     * 
+     * @param ossKey OSS文件路径
+     * @param uploadId 上传ID
+     * @param partNumber 分片序号(从1开始)
+     * @param partData 分片数据
+     * @return 分片ETag
+     */
+    public String uploadPart(String ossKey, String uploadId, int partNumber, byte[] partData) {
+        OSS ossClient = null;
+        try {
+            ossClient = createOSSClient();
+            UploadPartRequest uploadPartRequest = new UploadPartRequest();
+            uploadPartRequest.setBucketName(bucketName);
+            uploadPartRequest.setKey(ossKey);
+            uploadPartRequest.setUploadId(uploadId);
+            uploadPartRequest.setPartNumber(partNumber);
+            uploadPartRequest.setInputStream(new java.io.ByteArrayInputStream(partData));
+            uploadPartRequest.setPartSize(partData.length);
+            
+            UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
+            String eTag = uploadPartResult.getETag();
+            log.debug("上传分片成功: ossKey={}, partNumber={}, eTag={}", ossKey, partNumber, eTag);
+            return eTag;
+        } catch (Exception e) {
+            log.error("上传分片失败: ossKey={}, partNumber={}, error={}", ossKey, partNumber, e.getMessage(), e);
+            throw new RuntimeException("上传分片失败: " + e.getMessage(), e);
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 完成分片上传(合并所有分片)
+     * 
+     * @param ossKey OSS文件路径
+     * @param uploadId 上传ID
+     * @param partETags 所有分片的ETag列表
+     * @return 文件URL
+     */
+    public String completeMultipartUpload(String ossKey, String uploadId, List<PartETag> partETags) {
+        OSS ossClient = null;
+        try {
+            ossClient = createOSSClient();
+            CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
+                    bucketName, ossKey, uploadId, partETags);
+            CompleteMultipartUploadResult completeResult = ossClient.completeMultipartUpload(completeRequest);
+            
+            String fileUrl = "https://" + bucketName + "." + endPoint + "/" + ossKey;
+            log.info("完成分片上传: ossKey={}, uploadId={}, fileUrl={}", ossKey, uploadId, fileUrl);
+            return fileUrl;
+        } catch (Exception e) {
+            log.error("完成分片上传失败: ossKey={}, uploadId={}, error={}", ossKey, uploadId, e.getMessage(), e);
+            throw new RuntimeException("完成分片上传失败: " + e.getMessage(), e);
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 取消分片上传(清理未完成的分片)
+     * 
+     * @param ossKey OSS文件路径
+     * @param uploadId 上传ID
+     */
+    public void abortMultipartUpload(String ossKey, String uploadId) {
+        OSS ossClient = null;
+        try {
+            ossClient = createOSSClient();
+            AbortMultipartUploadRequest abortRequest = new AbortMultipartUploadRequest(bucketName, ossKey, uploadId);
+            ossClient.abortMultipartUpload(abortRequest);
+            log.info("取消分片上传: ossKey={}, uploadId={}", ossKey, uploadId);
+        } catch (Exception e) {
+            log.error("取消分片上传失败: ossKey={}, uploadId={}, error={}", ossKey, uploadId, e.getMessage(), e);
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 列出已上传的分片
+     * 
+     * @param ossKey OSS文件路径
+     * @param uploadId 上传ID
+     * @return 已上传的分片列表
+     */
+    public List<PartSummary> listParts(String ossKey, String uploadId) {
+        OSS ossClient = null;
+        try {
+            ossClient = createOSSClient();
+            ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, ossKey, uploadId);
+            PartListing partListing = ossClient.listParts(listPartsRequest);
+            return partListing.getParts();
+        } catch (Exception e) {
+            log.error("列出分片失败: ossKey={}, uploadId={}, error={}", ossKey, uploadId, e.getMessage(), e);
+            throw new RuntimeException("列出分片失败: " + e.getMessage(), e);
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 验证OSS回调签名
+     * 
+     * @param authorizationHeader Authorization头
+     * @param pubKeyUrl 公钥URL
+     * @param callbackBody 回调Body
+     * @return 是否验证通过
+     */
+    public boolean verifyCallback(String authorizationHeader, String pubKeyUrl, String callbackBody) {
+        try {
+            // 从公钥URL获取公钥
+            String pubKey = getPublicKey(pubKeyUrl);
+            
+            // 验证签名
+            String[] authParts = authorizationHeader.split(":");
+            if (authParts.length != 2) {
+                return false;
+            }
+            
+            String signature = authParts[1];
+            String stringToSign = callbackBody;
+            
+            // 使用公钥验证签名
+            java.security.Signature signatureVerifier = java.security.Signature.getInstance("SHA1withRSA");
+            signatureVerifier.initVerify(java.security.KeyFactory.getInstance("RSA")
+                    .generatePublic(new java.security.spec.X509EncodedKeySpec(
+                            Base64.getDecoder().decode(pubKey))));
+            signatureVerifier.update(stringToSign.getBytes(StandardCharsets.UTF_8));
+            
+            return signatureVerifier.verify(Base64.getDecoder().decode(signature));
+        } catch (Exception e) {
+            log.error("验证OSS回调签名失败: {}", e.getMessage(), e);
+            return false;
+        }
+    }
+
+    /**
+     * 从URL获取公钥
+     */
+    private String getPublicKey(String pubKeyUrl) {
+        try {
+            java.net.URL url = new java.net.URL(pubKeyUrl);
+            java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("GET");
+            conn.connect();
+            
+            StringBuilder response = new StringBuilder();
+            try (java.io.BufferedReader reader = new java.io.BufferedReader(
+                    new java.io.InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    response.append(line);
+                }
+            }
+            
+            // 移除公钥的头部和尾部
+            String pubKey = response.toString();
+            pubKey = pubKey.replace("-----BEGIN PUBLIC KEY-----", "");
+            pubKey = pubKey.replace("-----END PUBLIC KEY-----", "");
+            pubKey = pubKey.replace("\n", "");
+            pubKey = pubKey.replace("\r", "");
+            
+            return pubKey;
+        } catch (Exception e) {
+            log.error("获取公钥失败: {}", e.getMessage(), e);
+            throw new RuntimeException("获取公钥失败", e);
+        }
+    }
+
+    /**
+     * 创建OSS客户端
+     */
+    private OSS createOSSClient() {
+        com.aliyun.oss.ClientBuilderConfiguration config = new com.aliyun.oss.ClientBuilderConfiguration();
+        config.setSignatureVersion(SignVersion.V4);
+        return new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
+    }
+
+    /**
+     * OSS签名结果
+     */
+    @Data
+    public static class OSSSignatureResult {
+        private String accessKeyId;
+        private String policy;
+        private String signature;
+        private String dir;
+        private String host;
+        private String expire;
+        private String ossKey;
+        private String callbackUrl;
+        private String callbackBody;
+    }
+}