0.0.8.5
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# 图片存储解决方案
|
||||
|
||||
## 🚨 当前问题
|
||||
|
||||
项目目前使用 **IndexedDB** 存储图片,这存在以下问题:
|
||||
|
||||
### 1. **本地存储限制**
|
||||
- IndexedDB 是浏览器本地存储
|
||||
- 数据只存在用户的浏览器中
|
||||
- 无法跨设备访问
|
||||
- 清除浏览器数据会丢失所有图片
|
||||
|
||||
### 2. **无法共享**
|
||||
- 其他用户看不到上传的图片
|
||||
- 协作者无法访问
|
||||
- 不适合多用户系统
|
||||
|
||||
### 3. **存储容量限制**
|
||||
- 浏览器对 IndexedDB 有容量限制
|
||||
- 大量图片会占用用户设备空间
|
||||
- 可能导致性能问题
|
||||
|
||||
## ✅ 推荐解决方案
|
||||
|
||||
### 方案一:云存储服务(推荐)
|
||||
|
||||
使用专业的云存储服务,如:
|
||||
|
||||
#### 国内服务
|
||||
- **阿里云 OSS**
|
||||
- **腾讯云 COS**
|
||||
- **七牛云**
|
||||
- **又拍云**
|
||||
|
||||
#### 国际服务
|
||||
- **AWS S3**
|
||||
- **Google Cloud Storage**
|
||||
- **Azure Blob Storage**
|
||||
- **Cloudflare R2**
|
||||
|
||||
#### 实现步骤
|
||||
|
||||
1. **注册云存储服务**
|
||||
2. **创建存储桶(Bucket)**
|
||||
3. **获取 API 密钥**
|
||||
4. **创建上传 API**
|
||||
|
||||
```typescript
|
||||
// app/api/upload/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
|
||||
|
||||
const s3Client = new S3Client({
|
||||
region: process.env.AWS_REGION!,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: '未授权' }, { status: 401 })
|
||||
}
|
||||
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: '没有文件' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
const fileName = `avatars/${Date.now()}-${file.name}`
|
||||
|
||||
// 上传到 S3
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
await s3Client.send(new PutObjectCommand({
|
||||
Bucket: process.env.AWS_BUCKET_NAME!,
|
||||
Key: fileName,
|
||||
Body: buffer,
|
||||
ContentType: file.type,
|
||||
}))
|
||||
|
||||
// 返回 URL
|
||||
const url = `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${fileName}`
|
||||
|
||||
return NextResponse.json({ url })
|
||||
}
|
||||
```
|
||||
|
||||
5. **前端调用**
|
||||
|
||||
```typescript
|
||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
setIsUploadingAvatar(true)
|
||||
|
||||
// 上传到云存储
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const { url } = await response.json()
|
||||
|
||||
// 更新成员头像 URL
|
||||
await updateMember(member.id, {
|
||||
avatarUrl: url,
|
||||
})
|
||||
|
||||
setAvatarBlobUrl(url)
|
||||
} catch (error) {
|
||||
console.error('上传头像失败:', error)
|
||||
} finally {
|
||||
setIsUploadingAvatar(false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案二:服务器文件系统
|
||||
|
||||
将图片存储在服务器的文件系统中。
|
||||
|
||||
#### 优点
|
||||
- 不需要额外的云服务
|
||||
- 成本较低
|
||||
|
||||
#### 缺点
|
||||
- 需要管理服务器存储空间
|
||||
- 扩展性较差
|
||||
- 需要处理备份
|
||||
|
||||
#### 实现步骤
|
||||
|
||||
```typescript
|
||||
// app/api/upload/route.ts
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File
|
||||
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
// 保存到 public/uploads 目录
|
||||
const fileName = `${Date.now()}-${file.name}`
|
||||
const path = join(process.cwd(), 'public', 'uploads', fileName)
|
||||
await writeFile(path, buffer)
|
||||
|
||||
// 返回相对 URL
|
||||
const url = `/uploads/${fileName}`
|
||||
return NextResponse.json({ url })
|
||||
}
|
||||
```
|
||||
|
||||
### 方案三:Base64 编码(不推荐)
|
||||
|
||||
将图片转换为 Base64 字符串存储在数据库中。
|
||||
|
||||
#### 缺点
|
||||
- 数据库体积急剧增大
|
||||
- 查询性能下降
|
||||
- 不适合大图片
|
||||
- 传输效率低
|
||||
|
||||
**不推荐用于生产环境!**
|
||||
|
||||
## 📊 方案对比
|
||||
|
||||
| 方案 | 成本 | 性能 | 可靠性 | 扩展性 | 推荐度 |
|
||||
|------|------|------|--------|--------|--------|
|
||||
| 云存储 | 中 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| 服务器文件系统 | 低 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
|
||||
| IndexedDB | 无 | ⭐⭐ | ⭐ | ⭐ | ⭐ |
|
||||
| Base64 | 无 | ⭐ | ⭐⭐ | ⭐ | ❌ |
|
||||
|
||||
## 🔧 迁移步骤
|
||||
|
||||
### 1. 选择云存储服务
|
||||
推荐:阿里云 OSS(国内)或 AWS S3(国际)
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
```env
|
||||
# .env.local
|
||||
AWS_REGION=ap-southeast-1
|
||||
AWS_ACCESS_KEY_ID=your_access_key
|
||||
AWS_SECRET_ACCESS_KEY=your_secret_key
|
||||
AWS_BUCKET_NAME=your_bucket_name
|
||||
```
|
||||
|
||||
### 3. 安装依赖
|
||||
|
||||
```bash
|
||||
npm install @aws-sdk/client-s3
|
||||
# 或
|
||||
npm install ali-oss # 阿里云
|
||||
```
|
||||
|
||||
### 4. 创建上传 API
|
||||
参考上面的代码示例
|
||||
|
||||
### 5. 修改前端代码
|
||||
- 移除 IndexedDB 相关代码
|
||||
- 改为调用上传 API
|
||||
- 使用返回的 URL
|
||||
|
||||
### 6. 数据迁移
|
||||
- 将现有的 IndexedDB 图片迁移到云存储
|
||||
- 更新数据库中的 URL
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. 图片压缩
|
||||
上传前压缩图片以节省存储和带宽:
|
||||
|
||||
```typescript
|
||||
import imageCompression from 'browser-image-compression'
|
||||
|
||||
const compressedFile = await imageCompression(file, {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 1920,
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 图片格式
|
||||
- 头像:建议 JPEG/WebP,尺寸 500x500
|
||||
- 照片墙:建议 JPEG/WebP,尺寸 1920x1080
|
||||
|
||||
### 3. CDN 加速
|
||||
使用 CDN 加速图片访问:
|
||||
- 阿里云 OSS + CDN
|
||||
- AWS S3 + CloudFront
|
||||
- Cloudflare R2(自带 CDN)
|
||||
|
||||
### 4. 安全性
|
||||
- 使用签名 URL 控制访问
|
||||
- 设置合理的 CORS 策略
|
||||
- 限制上传文件大小和类型
|
||||
|
||||
### 5. 成本优化
|
||||
- 定期清理未使用的图片
|
||||
- 使用生命周期策略自动归档
|
||||
- 选择合适的存储类型
|
||||
|
||||
## 📝 当前状态
|
||||
|
||||
- ✅ 头像上传功能已实现(使用 IndexedDB)
|
||||
- ⚠️ 需要迁移到云存储
|
||||
- 📋 照片墙功能也使用 IndexedDB
|
||||
- 📋 需要统一迁移所有图片存储
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
1. 选择云存储服务
|
||||
2. 创建上传 API
|
||||
3. 修改前端上传逻辑
|
||||
4. 迁移现有数据
|
||||
5. 测试验证
|
||||
6. 部署上线
|
||||
Reference in New Issue
Block a user