0.0.8.5
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
# 服务器文件系统存储实现
|
||||
|
||||
## ✅ 已完成
|
||||
|
||||
成功实现了将图片存储在服务器文件系统的功能。
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
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. **多服务器**:需要共享存储或同步
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
成功实现了服务器文件系统存储方案,适合:
|
||||
- 自托管部署
|
||||
- 中小型应用
|
||||
- 对数据隐私有要求的场景
|
||||
- 不想依赖第三方云服务
|
||||
|
||||
如果未来需要更好的扩展性,可以轻松迁移到云存储服务!
|
||||
Reference in New Issue
Block a user