diff --git a/TASK_PROGRESS.md b/TASK_PROGRESS.md new file mode 100644 index 0000000..c743b67 --- /dev/null +++ b/TASK_PROGRESS.md @@ -0,0 +1,241 @@ +# Photography Portfolio 项目任务进度 + +> 最后更新: 2025-01-10 +> 项目状态: 开发中 🚧 + +## 📊 总体进度概览 + +- **总任务数**: 26 +- **已完成**: 3 ✅ +- **进行中**: 0 🔄 +- **待开始**: 23 ⏳ +- **完成率**: 12% + +--- + +## 🔥 高优先级任务 (9/26) + +### ✅ 已完成 (3/9) + +#### 1. ✅ 完善照片上传功能 +**状态**: 已完成 ✅ +**完成时间**: 2025-01-10 +**完成内容**: +- 创建了完整的文件处理工具包 (`pkg/utils/file/file.go`) +- 实现了图片上传、缩略图生成、文件存储 +- 支持多种图片格式 (JPEG, PNG, GIF, WebP) +- 添加了文件大小和类型验证 +- 实现了自动缩略图生成 (300px宽度) +- 更新了上传处理器支持 multipart form +- 更新了业务逻辑层处理文件上传 +- 添加了静态文件服务 (`/uploads/*`) +- 集成了图片处理库 (`github.com/disintegration/imaging`) + +#### 2. ✅ 实现 JWT 认证中间件 +**状态**: 已完成 ✅ +**完成时间**: 2025-01-10 +**完成内容**: +- 创建了 JWT 认证中间件 (`internal/middleware/auth.go`) +- 实现了 Bearer Token 验证 +- 支持用户信息注入到请求上下文 +- 完善的错误处理和响应 +- 与现有 JWT 工具包集成 +- go-zero 框架已内置 JWT 支持,路由配置完整 + +#### 3. ✅ 完善照片更新和删除业务逻辑 +**状态**: 已完成 ✅ +**完成时间**: 2025-01-10 +**完成内容**: +- 实现了完整的照片更新逻辑 (`updatePhotoLogic.go`) + - 参数验证和权限检查 + - 支持部分字段更新 (title, description, category_id) + - 分类存在性验证 + - 用户权限验证 (只能更新自己的照片) +- 实现了完整的照片删除逻辑 (`deletePhotoLogic.go`) + - 权限验证 (只能删除自己的照片) + - 同时删除数据库记录和文件系统文件 + - 安全的文件删除处理 (即使文件删除失败也不回滚) +- 更新了 Handler 使用统一响应格式 +- 创建了完整的 API 测试用例 (`test_photo_crud.http`) +- 包含正常场景和错误场景的测试覆盖 + +### 🔄 进行中 (0/9) + +### ⏳ 待开始 (6/9) + +#### 4. 完善分类更新和删除业务逻辑 +**优先级**: 高 🔥 +**预估工作量**: 0.5天 +**依赖**: 无 + +#### 5. 前端与后端 API 集成测试 +**优先级**: 高 🔥 +**预估工作量**: 1天 +**依赖**: 前端项目 + +#### 6. 实现用户认证流程 (登录/注册界面) +**优先级**: 高 🔥 +**预估工作量**: 1天 +**依赖**: 前端项目 + +#### 7. 实现照片上传界面和进度显示 +**优先级**: 高 🔥 +**预估工作量**: 1天 +**依赖**: 前端项目 + +#### 8. 配置生产环境数据库 (PostgreSQL) +**优先级**: 高 🔥 +**预估工作量**: 0.5天 +**依赖**: 生产环境服务器 + +#### 9. 更新 CI/CD 流程支持后端部署 +**优先级**: 高 🔥 +**预估工作量**: 1天 +**依赖**: 部署环境 + +--- + +## 📋 中优先级任务 (11/26) + +### 后端功能 (5项) +- **完善用户管理 CRUD 操作** ⏳ +- **添加数据库迁移脚本和种子数据** ⏳ +- **实现 CORS 中间件和安全配置** ⏳ +- **添加 API 接口测试用例** ⏳ +- **实现日志中间件和错误处理** ⏳ + +### 前端功能 (3项) +- **完善照片管理界面 (编辑/删除)** ⏳ +- **实现分类管理界面** ⏳ +- **添加响应式设计优化** ⏳ + +### 部署和运维 (2项) +- **配置反向代理 (前后端统一域名)** ⏳ +- **配置文件存储服务 (图片上传)** ⏳ + +### 测试和文档 (2项) +- **编写 API 集成测试** ⏳ +- **完善 API 接口文档** ⏳ + +--- + +## 📌 低优先级任务 (6/26) + +### 后端扩展 (3项) +- **完善生产环境配置 (PostgreSQL)** ⏳ +- **添加 Docker 容器化配置** ⏳ +- **实现 API 文档生成 (Swagger)** ⏳ + +### 部署优化 (1项) +- **设置监控和日志收集** ⏳ + +### 测试和文档 (2项) +- **编写前端 E2E 测试** ⏳ +- **编写部署文档** ⏳ + +--- + +## 🎯 里程碑规划 + +### 第一阶段:核心功能完善 (本周) +- [x] 照片上传功能 +- [x] JWT 认证中间件 +- [ ] 照片和分类的完整 CRUD +- [ ] 前后端 API 集成 + +**目标**: 实现核心业务功能的完整闭环 + +### 第二阶段:功能完整性 (下周) +- [ ] 用户界面完善 +- [ ] 数据库配置 +- [ ] 基础测试用例 +- [ ] 安全性配置 + +**目标**: 功能完整性和用户体验 + +### 第三阶段:部署和优化 (后续) +- [ ] CI/CD 更新 +- [ ] 生产环境部署 +- [ ] 性能优化 +- [ ] 监控和文档 + +**目标**: 生产环境就绪 + +--- + +## 💻 技术成果 + +### ✅ 已实现的核心功能 +- **文件上传系统**: 完整的图片上传和缩略图生成 +- **JWT 认证体系**: 用户认证和权限管理 +- **静态文件服务**: 图片资源访问 +- **图片处理能力**: 自动缩放和格式支持 +- **安全文件验证**: 类型和大小检查 +- **照片CRUD完整**: 创建、读取、更新、删除全功能 +- **权限控制**: 用户只能操作自己的照片 +- **文件系统管理**: 删除照片时同步删除文件 + +### 📊 API 接口状态 +- ✅ `POST /api/v1/auth/login` - 用户登录 +- ✅ `POST /api/v1/auth/register` - 用户注册 +- ✅ `GET /api/v1/health` - 健康检查 +- ✅ `GET /api/v1/photos` - 照片列表 +- ✅ `POST /api/v1/photos` - 上传照片 (支持文件上传) +- ✅ `GET /api/v1/photos/:id` - 获取照片详情 +- ✅ `PUT /api/v1/photos/:id` - 更新照片 (支持权限验证) +- ✅ `DELETE /api/v1/photos/:id` - 删除照片 (同时删除文件) +- ✅ `GET /api/v1/categories` - 分类列表 +- ✅ `POST /api/v1/categories` - 创建分类 +- ✅ `GET /api/v1/users` - 用户列表 +- ✅ `GET /uploads/*` - 静态文件访问 +- ⏳ `PUT /api/v1/categories/:id` - 更新分类 +- ⏳ `DELETE /api/v1/categories/:id` - 删除分类 + +### 🛠️ 技术栈 +- **后端框架**: go-zero v1.8.0 +- **数据库**: SQLite (开发) / PostgreSQL (生产) +- **认证**: JWT Token +- **文件处理**: imaging + uuid +- **构建工具**: Go 1.23+ + Makefile + +--- + +## 📈 每日进度记录 + +### 2025-01-10 +- ✅ **照片上传功能完成**: 实现文件处理、缩略图生成、静态服务 +- ✅ **JWT 认证中间件完成**: Bearer Token 验证和用户上下文注入 +- ✅ **照片更新删除功能完成**: 实现权限验证、文件同步删除、完整CRUD +- 📝 **下一步**: 完善分类的更新删除功能 + +### 待补充... + +--- + +## 🔄 更新日志 + +### v0.2.0 - 2025-01-10 +- 新增完整的文件上传系统 +- 新增 JWT 认证中间件 +- 新增静态文件服务 +- 优化图片处理能力 +- 完善照片更新和删除功能 +- 实现用户权限控制 +- 添加文件系统同步管理 + +### v0.1.0 - 2025-01-09 +- 初始化 go-zero 项目架构 +- 实现基础 CRUD 接口 +- 配置开发环境 + +--- + +## 📞 联系信息 + +**项目负责人**: iriver +**项目仓库**: https://git.iriver.top/iriver/photography +**更新频率**: 每日更新 + +--- + +*本文档自动同步项目进度,如有疑问请查看具体模块的 CLAUDE.md 文件* \ No newline at end of file diff --git a/backend/internal/handler/photo/deletePhotoHandler.go b/backend/internal/handler/photo/deletePhotoHandler.go index 7028531..103f1d4 100644 --- a/backend/internal/handler/photo/deletePhotoHandler.go +++ b/backend/internal/handler/photo/deletePhotoHandler.go @@ -7,6 +7,7 @@ import ( "photography-backend/internal/logic/photo" "photography-backend/internal/svc" "photography-backend/internal/types" + "photography-backend/pkg/response" ) // 删除照片 @@ -20,10 +21,6 @@ func DeletePhotoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { l := photo.NewDeletePhotoLogic(r.Context(), svcCtx) resp, err := l.DeletePhoto(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + response.Response(w, resp, err) } } diff --git a/backend/internal/handler/photo/updatePhotoHandler.go b/backend/internal/handler/photo/updatePhotoHandler.go index 13325fd..aad8d91 100644 --- a/backend/internal/handler/photo/updatePhotoHandler.go +++ b/backend/internal/handler/photo/updatePhotoHandler.go @@ -7,6 +7,7 @@ import ( "photography-backend/internal/logic/photo" "photography-backend/internal/svc" "photography-backend/internal/types" + "photography-backend/pkg/response" ) // 更新照片 @@ -20,10 +21,6 @@ func UpdatePhotoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { l := photo.NewUpdatePhotoLogic(r.Context(), svcCtx) resp, err := l.UpdatePhoto(&req) - if err != nil { - httpx.ErrorCtx(r.Context(), w, err) - } else { - httpx.OkJsonCtx(r.Context(), w, resp) - } + response.Response(w, resp, err) } } diff --git a/backend/internal/logic/photo/deletePhotoLogic.go b/backend/internal/logic/photo/deletePhotoLogic.go index e6c01a1..dcc3c5a 100644 --- a/backend/internal/logic/photo/deletePhotoLogic.go +++ b/backend/internal/logic/photo/deletePhotoLogic.go @@ -3,8 +3,11 @@ package photo import ( "context" + "photography-backend/internal/model" "photography-backend/internal/svc" "photography-backend/internal/types" + "photography-backend/pkg/errorx" + fileUtil "photography-backend/pkg/utils/file" "github.com/zeromicro/go-zero/core/logx" ) @@ -25,7 +28,57 @@ func NewDeletePhotoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delet } func (l *DeletePhotoLogic) DeletePhoto(req *types.DeletePhotoRequest) (resp *types.DeletePhotoResponse, err error) { - // todo: add your logic here and delete this line + // 1. 获取当前用户ID (从JWT中间件获取) + userID := l.ctx.Value("userID") + if userID == nil { + return nil, errorx.NewWithCode(errorx.AuthError) + } + currentUserID := userID.(int64) - return + // 2. 查询照片是否存在 + photo, err := l.svcCtx.PhotoModel.FindOne(l.ctx, req.Id) + if err != nil { + if err == model.ErrNotFound { + return nil, errorx.NewWithCode(errorx.PhotoNotFound) + } + logx.Errorf("查询照片失败: %v", err) + return nil, errorx.NewWithCode(errorx.ServerError) + } + + // 3. 检查权限 - 只有照片所有者可以删除 + if photo.UserId != currentUserID { + return nil, errorx.NewWithCode(errorx.Forbidden) + } + + // 4. 删除数据库记录 + err = l.svcCtx.PhotoModel.Delete(l.ctx, req.Id) + if err != nil { + logx.Errorf("删除照片记录失败: %v", err) + return nil, errorx.NewWithCode(errorx.ServerError) + } + + // 5. 删除文件系统中的文件 + // 删除原图 + if photo.FilePath != "" { + if err := fileUtil.DeleteFile(photo.FilePath); err != nil { + logx.Errorf("删除原图文件失败: %v", err) + // 注意:即使文件删除失败,我们也不回滚数据库操作,因为文件可能已经被手动删除 + } + } + + // 删除缩略图 + if photo.ThumbnailPath != "" { + if err := fileUtil.DeleteFile(photo.ThumbnailPath); err != nil { + logx.Errorf("删除缩略图文件失败: %v", err) + // 注意:即使文件删除失败,我们也不回滚数据库操作 + } + } + + // 6. 返回成功响应 + return &types.DeletePhotoResponse{ + BaseResponse: types.BaseResponse{ + Code: errorx.Success, + Message: "照片删除成功", + }, + }, nil } diff --git a/backend/internal/logic/photo/updatePhotoLogic.go b/backend/internal/logic/photo/updatePhotoLogic.go index f5cbb46..5ac8962 100644 --- a/backend/internal/logic/photo/updatePhotoLogic.go +++ b/backend/internal/logic/photo/updatePhotoLogic.go @@ -2,9 +2,13 @@ package photo import ( "context" + "database/sql" + "time" + "photography-backend/internal/model" "photography-backend/internal/svc" "photography-backend/internal/types" + "photography-backend/pkg/errorx" "github.com/zeromicro/go-zero/core/logx" ) @@ -25,7 +29,75 @@ func NewUpdatePhotoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Updat } func (l *UpdatePhotoLogic) UpdatePhoto(req *types.UpdatePhotoRequest) (resp *types.UpdatePhotoResponse, err error) { - // todo: add your logic here and delete this line + // 1. 获取当前用户ID (从JWT中间件获取) + userID := l.ctx.Value("userID") + if userID == nil { + return nil, errorx.NewWithCode(errorx.AuthError) + } + currentUserID := userID.(int64) - return + // 2. 查询照片是否存在 + photo, err := l.svcCtx.PhotoModel.FindOne(l.ctx, req.Id) + if err != nil { + if err == model.ErrNotFound { + return nil, errorx.NewWithCode(errorx.PhotoNotFound) + } + logx.Errorf("查询照片失败: %v", err) + return nil, errorx.NewWithCode(errorx.ServerError) + } + + // 3. 检查权限 - 只有照片所有者可以更新 + if photo.UserId != currentUserID { + return nil, errorx.NewWithCode(errorx.Forbidden) + } + + // 4. 验证分类是否存在 (如果要更新分类) + if req.CategoryId > 0 { + _, err = l.svcCtx.CategoryModel.FindOne(l.ctx, req.CategoryId) + if err != nil { + if err == model.ErrNotFound { + return nil, errorx.NewWithCode(errorx.CategoryNotFound) + } + logx.Errorf("查询分类失败: %v", err) + return nil, errorx.NewWithCode(errorx.ServerError) + } + } + + // 5. 更新照片信息 + if req.Title != "" { + photo.Title = req.Title + } + if req.Description != "" { + photo.Description = sql.NullString{String: req.Description, Valid: true} + } + if req.CategoryId > 0 { + photo.CategoryId = req.CategoryId + } + photo.UpdatedAt = time.Now() + + // 6. 保存到数据库 + err = l.svcCtx.PhotoModel.Update(l.ctx, photo) + if err != nil { + logx.Errorf("更新照片失败: %v", err) + return nil, errorx.NewWithCode(errorx.ServerError) + } + + // 7. 构造响应 + return &types.UpdatePhotoResponse{ + BaseResponse: types.BaseResponse{ + Code: errorx.Success, + Message: "照片更新成功", + }, + Data: types.Photo{ + Id: photo.Id, + Title: photo.Title, + Description: photo.Description.String, + FilePath: photo.FilePath, + ThumbnailPath: photo.ThumbnailPath, + UserId: photo.UserId, + CategoryId: photo.CategoryId, + CreatedAt: photo.CreatedAt.Unix(), + UpdatedAt: photo.UpdatedAt.Unix(), + }, + }, nil } diff --git a/backend/test_photo_crud.http b/backend/test_photo_crud.http new file mode 100644 index 0000000..1914310 --- /dev/null +++ b/backend/test_photo_crud.http @@ -0,0 +1,94 @@ +### 照片CRUD测试 + +### 1. 用户登录获取token +POST http://localhost:8888/api/v1/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "123456" +} + +### 设置变量 +@token = {{login.response.body.data.token}} + +### 2. 获取照片列表 +GET http://localhost:8888/api/v1/photos?page=1&page_size=5 +Authorization: Bearer {{token}} + +### 3. 上传照片 (可选,用于创建测试数据) +POST http://localhost:8888/api/v1/photos +Authorization: Bearer {{token}} +Content-Type: multipart/form-data; boundary=boundary + +--boundary +Content-Disposition: form-data; name="title" + +测试照片更新删除 +--boundary +Content-Disposition: form-data; name="description" + +这是一张用于测试更新和删除功能的照片 +--boundary +Content-Disposition: form-data; name="category_id" + +1 +--boundary +Content-Disposition: form-data; name="file"; filename="test.jpg" +Content-Type: image/jpeg + +< ./test.jpg +--boundary-- + +### 4. 获取单个照片详情 +GET http://localhost:8888/api/v1/photos/1 +Authorization: Bearer {{token}} + +### 5. 更新照片信息 +PUT http://localhost:8888/api/v1/photos/1 +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "title": "更新后的照片标题", + "description": "更新后的照片描述信息", + "category_id": 1 +} + +### 6. 再次获取照片详情验证更新 +GET http://localhost:8888/api/v1/photos/1 +Authorization: Bearer {{token}} + +### 7. 删除照片 +DELETE http://localhost:8888/api/v1/photos/1 +Authorization: Bearer {{token}} + +### 8. 验证删除(应该返回404) +GET http://localhost:8888/api/v1/photos/1 +Authorization: Bearer {{token}} + +### 错误场景测试 + +### 9. 尝试更新不存在的照片 +PUT http://localhost:8888/api/v1/photos/999 +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "title": "不存在的照片" +} + +### 10. 尝试删除不存在的照片 +DELETE http://localhost:8888/api/v1/photos/999 +Authorization: Bearer {{token}} + +### 11. 无认证更新照片 +PUT http://localhost:8888/api/v1/photos/1 +Content-Type: application/json + +{ + "title": "无认证更新" +} + +### 12. 无认证删除照片 +DELETE http://localhost:8888/api/v1/photos/1 \ No newline at end of file