303 lines
5.9 KiB
Markdown
303 lines
5.9 KiB
Markdown
# 服务器文件系统存储实现
|
||
|
||
## ✅ 已完成
|
||
|
||
成功实现了将图片存储在服务器文件系统的功能。
|
||
|
||
## 📁 文件结构
|
||
|
||
```
|
||
chinese-family-tree/
|
||
├── app/
|
||
│ └── api/
|
||
│ └── upload/
|
||
│ └── route.ts # 上传 API
|
||
├── public/
|
||
│ └── uploads/ # 上传文件存储目录
|
||
│ └── .gitkeep # 保留目录结构
|
||
└── .gitignore # 忽略上传的文件
|
||
```
|
||
|
||
## 🔧 实现细节
|
||
|
||
### 1. 上传 API (`app/api/upload/route.ts`)
|
||
|
||
**功能:**
|
||
- 接收文件上传请求
|
||
- 验证用户登录状态
|
||
- 验证文件类型(只允许图片)
|
||
- 验证文件大小(限制 5MB)
|
||
- 生成唯一文件名
|
||
- 保存到 `public/uploads` 目录
|
||
- 返回可访问的 URL
|
||
|
||
**安全措施:**
|
||
- ✅ 用户身份验证
|
||
- ✅ 文件类型验证
|
||
- ✅ 文件大小限制
|
||
- ✅ 唯一文件名(防止覆盖)
|
||
|
||
**文件命名规则:**
|
||
```
|
||
{timestamp}-{random}.{ext}
|
||
例如:1732435200000-a3b5c7.jpg
|
||
```
|
||
|
||
### 2. 前端上传逻辑
|
||
|
||
**修改文件:** `app/members/[id]/page.tsx`
|
||
|
||
**流程:**
|
||
1. 用户选择图片
|
||
2. 创建 FormData
|
||
3. 调用 `/api/upload` API
|
||
4. 获取返回的 URL
|
||
5. 更新成员的 `avatarUrl` 字段
|
||
6. 显示新头像
|
||
|
||
**兼容性:**
|
||
- 优先使用 `avatarUrl`(新方案)
|
||
- 兼容 `avatarImageId`(旧的 IndexedDB 方案)
|
||
|
||
### 3. Git 配置
|
||
|
||
**.gitignore 规则:**
|
||
```gitignore
|
||
# Uploads
|
||
public/uploads/*
|
||
!public/uploads/.gitkeep
|
||
```
|
||
|
||
**说明:**
|
||
- 忽略所有上传的文件
|
||
- 保留 `.gitkeep` 文件以维持目录结构
|
||
- 避免将用户上传的图片提交到 Git
|
||
|
||
## 📊 数据库字段
|
||
|
||
### FamilyMember 表
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `avatarUrl` | String? | 头像 URL(新方案) |
|
||
| `avatarImageId` | String? | IndexedDB ID(旧方案,兼容) |
|
||
|
||
**推荐:** 使用 `avatarUrl` 字段
|
||
|
||
## 🎯 使用方式
|
||
|
||
### 上传头像
|
||
|
||
1. 访问成员详情页
|
||
2. 将鼠标悬停在头像上
|
||
3. 点击出现的相机图标
|
||
4. 选择图片文件
|
||
5. 自动上传并更新
|
||
|
||
### API 调用示例
|
||
|
||
```typescript
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
|
||
const response = await fetch('/api/upload', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
|
||
const { url } = await response.json()
|
||
// url: "/uploads/1732435200000-a3b5c7.jpg"
|
||
```
|
||
|
||
## 🔒 安全性
|
||
|
||
### 已实现
|
||
|
||
1. **身份验证**:需要登录才能上传
|
||
2. **文件类型验证**:只允许图片格式
|
||
3. **文件大小限制**:最大 5MB
|
||
4. **唯一文件名**:防止文件覆盖
|
||
|
||
### 建议增强
|
||
|
||
1. **图片压缩**:上传前压缩以节省空间
|
||
2. **病毒扫描**:扫描上传的文件
|
||
3. **访问控制**:限制只有家族成员可访问
|
||
4. **定期清理**:删除未使用的图片
|
||
|
||
## 📈 性能优化
|
||
|
||
### 当前实现
|
||
|
||
- ✅ 直接存储在 `public` 目录
|
||
- ✅ Next.js 自动处理静态文件服务
|
||
- ✅ 支持浏览器缓存
|
||
|
||
### 可选优化
|
||
|
||
1. **图片压缩**
|
||
```typescript
|
||
import imageCompression from 'browser-image-compression'
|
||
|
||
const compressed = await imageCompression(file, {
|
||
maxSizeMB: 1,
|
||
maxWidthOrHeight: 1920,
|
||
})
|
||
```
|
||
|
||
2. **CDN 加速**
|
||
- 使用 Nginx 反向代理
|
||
- 配置 CDN 服务
|
||
- 启用 Gzip 压缩
|
||
|
||
3. **懒加载**
|
||
```tsx
|
||
<img
|
||
src={avatarUrl}
|
||
loading="lazy"
|
||
alt="头像"
|
||
/>
|
||
```
|
||
|
||
## 🚀 部署注意事项
|
||
|
||
### 1. 文件权限
|
||
|
||
确保 `public/uploads` 目录有写入权限:
|
||
|
||
```bash
|
||
chmod 755 public/uploads
|
||
```
|
||
|
||
### 2. 磁盘空间
|
||
|
||
监控磁盘使用情况:
|
||
|
||
```bash
|
||
du -sh public/uploads
|
||
```
|
||
|
||
### 3. 备份策略
|
||
|
||
定期备份上传的文件:
|
||
|
||
```bash
|
||
# 备份到其他位置
|
||
rsync -av public/uploads/ /backup/uploads/
|
||
|
||
# 或使用 tar 打包
|
||
tar -czf uploads-backup-$(date +%Y%m%d).tar.gz public/uploads/
|
||
```
|
||
|
||
### 4. Nginx 配置(可选)
|
||
|
||
如果使用 Nginx 作为反向代理:
|
||
|
||
```nginx
|
||
location /uploads/ {
|
||
alias /path/to/app/public/uploads/;
|
||
expires 30d;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
```
|
||
|
||
## 🔄 迁移指南
|
||
|
||
### 从 IndexedDB 迁移
|
||
|
||
如果有旧数据使用 IndexedDB 存储:
|
||
|
||
1. **导出旧数据**
|
||
```typescript
|
||
const images = await db.images.toArray()
|
||
for (const image of images) {
|
||
const formData = new FormData()
|
||
formData.append('file', new File([image.blob], 'image.jpg'))
|
||
|
||
const response = await fetch('/api/upload', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
|
||
const { url } = await response.json()
|
||
|
||
// 更新成员记录
|
||
await updateMember(memberId, { avatarUrl: url })
|
||
}
|
||
```
|
||
|
||
2. **清理旧数据**
|
||
```typescript
|
||
await db.images.clear()
|
||
```
|
||
|
||
## 💡 最佳实践
|
||
|
||
### 1. 文件命名
|
||
|
||
- ✅ 使用时间戳 + 随机字符串
|
||
- ✅ 保留原始文件扩展名
|
||
- ❌ 不要使用用户提供的文件名
|
||
|
||
### 2. 存储位置
|
||
|
||
- ✅ `public/uploads/` - 可直接访问
|
||
- ❌ `private/uploads/` - 需要额外处理
|
||
|
||
### 3. 错误处理
|
||
|
||
- ✅ 提供友好的错误提示
|
||
- ✅ 记录错误日志
|
||
- ✅ 失败时清理临时文件
|
||
|
||
### 4. 用户体验
|
||
|
||
- ✅ 显示上传进度
|
||
- ✅ 支持拖拽上传
|
||
- ✅ 预览上传的图片
|
||
|
||
## 📝 后续改进
|
||
|
||
### 短期
|
||
|
||
- [ ] 添加图片压缩
|
||
- [ ] 支持拖拽上传
|
||
- [ ] 显示上传进度
|
||
|
||
### 中期
|
||
|
||
- [ ] 图片裁剪功能
|
||
- [ ] 支持多图上传
|
||
- [ ] 图片管理界面
|
||
|
||
### 长期
|
||
|
||
- [ ] 迁移到云存储
|
||
- [ ] CDN 加速
|
||
- [ ] 图片处理服务
|
||
|
||
## ✨ 优势
|
||
|
||
1. **简单易用**:不需要额外的云服务
|
||
2. **成本低**:只需服务器存储空间
|
||
3. **完全控制**:数据完全在自己手中
|
||
4. **易于备份**:直接备份文件夹即可
|
||
5. **快速部署**:无需配置云服务
|
||
|
||
## ⚠️ 限制
|
||
|
||
1. **扩展性**:单服务器存储容量有限
|
||
2. **性能**:大量访问时可能需要 CDN
|
||
3. **备份**:需要自己管理备份策略
|
||
4. **多服务器**:需要共享存储或同步
|
||
|
||
## 🎉 总结
|
||
|
||
成功实现了服务器文件系统存储方案,适合:
|
||
- 自托管部署
|
||
- 中小型应用
|
||
- 对数据隐私有要求的场景
|
||
- 不想依赖第三方云服务
|
||
|
||
如果未来需要更好的扩展性,可以轻松迁移到云存储服务!
|