# COS 预签名 URL 直传指南（推荐方式）

## 🎯 为什么使用预签名 URL？

相比 POST 表单上传，预签名 URL 直传更简单：
- ✅ **无需复杂的表单字段** - 只需要一个 URL
- ✅ **前端代码更简洁** - 直接 PUT 文件即可
- ✅ **自动处理 CORS** - 预签名 URL 包含签名，不受 CORS 限制
- ✅ **更安全** - URL 有过期时间，临时授权

---

## 📋 完整的前端上传代码

### Vue 3 + Element Plus 示例

```vue
<template>
  <el-upload
    :action="uploadUrl"
    :before-upload="beforeUpload"
    :http-request="customUpload"
    :show-file-list="false"
    accept="image/*"
  >
    <el-button type="primary">选择图片</el-button>
  </el-upload>
</template>

<script setup>
import { ref } from 'vue';
import { ElMessage } from 'element-plus';

const uploadUrl = ref('');
const fileUrl = ref('');

// 上传前获取预签名 URL
const beforeUpload = async (file) => {
  try {
    // 1. 获取预签名 URL
    const response = await fetch('/user/oss/presigned-url', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + localStorage.getItem('token')
      },
      body: JSON.stringify({
        fileName: file.name,
        userId: '123'  // 从登录状态获取
      })
    });

    const result = await response.json();
    if (result.code === 200) {
      uploadUrl.value = result.data.uploadUrl;
      fileUrl.value = result.data.fileUrl;
      return true;
    } else {
      ElMessage.error(result.message);
      return false;
    }
  } catch (error) {
    console.error('获取预签名URL失败:', error);
    ElMessage.error('获取上传地址失败');
    return false;
  }
};

// 自定义上传方法
const customUpload = async ({ file }) => {
  try {
    // 2. 直接 PUT 文件到预签名 URL
    const response = await fetch(uploadUrl.value, {
      method: 'PUT',
      body: file,
      headers: {
        'Content-Type': file.type
      }
    });

    if (response.ok) {
      ElMessage.success('上传成功');
      console.log('文件访问地址:', fileUrl.value);
      // 3. 使用 fileUrl.value 更新头像等
      return { url: fileUrl.value };
    } else {
      throw new Error('上传失败');
    }
  } catch (error) {
    console.error('上传失败:', error);
    ElMessage.error('上传失败');
    throw error;
  }
};
</script>
```

---

### 原生 JavaScript 示例

```javascript
async function uploadFile(file, userId) {
  try {
    // 1. 获取预签名 URL
    const signResponse = await fetch('/user/oss/presigned-url', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + localStorage.getItem('token')
      },
      body: JSON.stringify({
        fileName: file.name,
        userId: userId
      })
    });

    const signResult = await signResponse.json();
    if (signResult.code !== 200) {
      throw new Error(signResult.message);
    }

    const { uploadUrl, fileUrl } = signResult.data;

    // 2. 直接 PUT 文件到预签名 URL
    const uploadResponse = await fetch(uploadUrl, {
      method: 'PUT',
      body: file,
      headers: {
        'Content-Type': file.type
      }
    });

    if (!uploadResponse.ok) {
      throw new Error('上传失败');
    }

    // 3. 上传成功，返回文件访问地址
    console.log('上传成功，文件地址:', fileUrl);
    return fileUrl;

  } catch (error) {
    console.error('上传失败:', error);
    throw error;
  }
}

// 使用示例
document.getElementById('fileInput').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (file) {
    try {
      const fileUrl = await uploadFile(file, '123');
      alert('上传成功: ' + fileUrl);
    } catch (error) {
      alert('上传失败: ' + error.message);
    }
  }
});
```

---

### React 示例

```jsx
import { useState } from 'react';
import { message } from 'antd';

function FileUpload() {
  const [uploading, setUploading] = useState(false);

  const handleUpload = async (file) => {
    setUploading(true);
    try {
      // 1. 获取预签名 URL
      const signResponse = await fetch('/user/oss/presigned-url', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        },
        body: JSON.stringify({
          fileName: file.name,
          userId: '123'
        })
      });

      const signResult = await signResponse.json();
      if (signResult.code !== 200) {
        throw new Error(signResult.message);
      }

      const { uploadUrl, fileUrl } = signResult.data;

      // 2. PUT 文件到预签名 URL
      const uploadResponse = await fetch(uploadUrl, {
        method: 'PUT',
        body: file,
        headers: {
          'Content-Type': file.type
        }
      });

      if (!uploadResponse.ok) {
        throw new Error('上传失败');
      }

      message.success('上传成功');
      console.log('文件地址:', fileUrl);
      return fileUrl;

    } catch (error) {
      console.error('上传失败:', error);
      message.error('上传失败: ' + error.message);
    } finally {
      setUploading(false);
    }
  };

  return (
    <input
      type="file"
      accept="image/*"
      onChange={(e) => handleUpload(e.target.files[0])}
      disabled={uploading}
    />
  );
}
```

---

## 📡 API 接口说明

### 接口地址
```
POST /user/oss/presigned-url
GET  /user/oss/presigned-url
```

### 请求参数（POST 方式）

```json
{
  "fileName": "avatar.jpg",
  "userId": "123"
}
```

### 响应数据

```json
{
  "code": 200,
  "message": "预签名URL生成成功",
  "data": {
    "uploadUrl": "https://oss-1818ai-user-img-1302947942.cos.ap-guangzhou.myqcloud.com/user_img/xxx.jpg?sign=...",
    "fileUrl": "https://oss-1818ai-user-img-1302947942.cos.ap-guangzhou.myqcloud.com/user_img/xxx.jpg",
    "fileName": "avatar.jpg",
    "expiresIn": 3600
  }
}
```

### 字段说明

| 字段 | 说明 |
|------|------|
| `uploadUrl` | 预签名上传 URL（包含签名，有效期1小时） |
| `fileUrl` | 文件访问地址（永久有效） |
| `fileName` | 文件名 |
| `expiresIn` | 签名有效期（秒） |

---

## 🔧 上传流程

```
┌─────────┐      1. 请求预签名URL      ┌─────────┐
│         │ ────────────────────────> │         │
│  前端   │                            │  后端   │
│         │ <──────────────────────── │         │
└─────────┘      2. 返回预签名URL      └─────────┘
     │
     │ 3. PUT 文件到预签名URL
     ↓
┌─────────┐
│   COS   │
│  存储桶  │
└─────────┘
```

---

## ✅ 优势对比

| 特性 | 预签名 URL 直传 | POST 表单上传 |
|------|----------------|--------------|
| **前端代码** | ✅ 简单（一个 PUT 请求） | ❌ 复杂（多个表单字段） |
| **CORS 配置** | ✅ 不需要（签名在 URL 中） | ❌ 需要配置 |
| **字段匹配** | ✅ 无需关心字段名 | ❌ 必须匹配 COS 字段名 |
| **错误排查** | ✅ 简单 | ❌ 复杂（403 签名错误） |
| **安全性** | ✅ URL 有过期时间 | ✅ Policy 有过期时间 |

---

## ⚠️ 注意事项

1. **预签名 URL 有效期**
   - 默认 1 小时（3600 秒）
   - 过期后需要重新获取

2. **Content-Type**
   - PUT 请求时建议设置正确的 Content-Type
   - 例如：`image/jpeg`, `image/png`

3. **文件大小限制**
   - 默认最大 100MB
   - 可在后端配置中调整

4. **文件命名**
   - 后端会自动生成唯一文件名（UUID）
   - 避免文件名冲突

---

## 🧪 测试命令

```bash
# 1. 获取预签名 URL
curl -X POST http://localhost:8083/user/oss/presigned-url \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"fileName":"test.jpg","userId":"123"}'

# 2. 使用预签名 URL 上传文件
curl -X PUT "<返回的uploadUrl>" \
  -H "Content-Type: image/jpeg" \
  --data-binary "@/path/to/test.jpg"

# 3. 访问文件
curl "<返回的fileUrl>"
```

---

## 🎉 总结

使用预签名 URL 直传是最简单、最可靠的方式：
- ✅ 前端只需两步：获取 URL → PUT 文件
- ✅ 无需配置 CORS
- ✅ 无需关心复杂的表单字段
- ✅ 错误排查简单

**强烈推荐使用这种方式！**
