--- globs: backend/api/**/*.api alwaysApply: false --- # 后端 API 开发规则 ## 🔄 API 开发工作流 ### 1. 需求分析 - 明确API功能和参数 - 确定请求/响应格式 - 考虑错误处理场景 ### 2. API 定义 (.api文件) 在 `backend/api/desc/` 目录下定义: ```api // photo.api syntax = "v1" info( title: "Photography Photo API" desc: "照片管理相关API" author: "iriver" email: "iriver@example.com" version: "v1" ) import "common.api" type ( UploadPhotoRequest { Title string `form:"title"` Description string `form:"description,optional"` CategoryId string `form:"category_id,optional"` File string `form:"file"` } UploadPhotoResponse { Id string `json:"id"` Title string `json:"title"` Filename string `json:"filename"` Thumbnail string `json:"thumbnail"` CreatedAt string `json:"created_at"` } UpdatePhotoRequest { Id string `path:"id"` Title string `json:"title,optional"` Description string `json:"description,optional"` CategoryId string `json:"category_id,optional"` } ) @server( group: photo prefix: /api/v1 ) service photography-api { @doc "上传照片" @handler uploadPhoto post /photos (UploadPhotoRequest) returns (UploadPhotoResponse) @doc "更新照片信息" @handler updatePhoto put /photos/:id (UpdatePhotoRequest) returns (BaseResponse) @doc "删除照片" @handler deletePhoto delete /photos/:id (IdPathRequest) returns (BaseResponse) @doc "获取照片详情" @handler getPhoto get /photos/:id (IdPathRequest) returns (PhotoResponse) @doc "获取照片列表" @handler getPhotoList get /photos (PhotoListRequest) returns (PhotoListResponse) } ``` ### 3. 代码生成 ```bash cd backend make api ``` ### 4. Handler 实现模式 ```go func (h *UploadPhotoHandler) UploadPhoto(w http.ResponseWriter, r *http.Request) { var req types.UploadPhotoRequest if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } l := photo.NewUploadPhotoLogic(r.Context(), h.svcCtx) resp, err := l.UploadPhoto(&req, r) response.Response(w, resp, err) } ``` ### 5. Logic 实现模式 ```go func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest, r *http.Request) (resp *types.UploadPhotoResponse, err error) { // 1. 参数验证 if err = l.validateRequest(req); err != nil { return nil, err } // 2. 获取上传文件 fileHeader, err := l.getUploadFile(r) if err != nil { return nil, err } // 3. 文件处理 savedPath, thumbnail, err := l.processFile(fileHeader) if err != nil { return nil, err } // 4. 数据持久化 photo, err := l.savePhoto(req, savedPath, thumbnail) if err != nil { return nil, err } // 5. 构造响应 return l.buildResponse(photo), nil } ``` ## 📋 API 接口规范 ### 请求规范 ```go // 路径参数 type IdPathRequest { Id string `path:"id"` } // 查询参数 type PhotoListRequest { Page int `form:"page,default=1"` Limit int `form:"limit,default=10"` CategoryId string `form:"category_id,optional"` Keyword string `form:"keyword,optional"` } // JSON请求体 type UpdatePhotoRequest { Title string `json:"title,optional"` Description string `json:"description,optional"` CategoryId string `json:"category_id,optional"` } // 文件上传 (multipart/form-data) type UploadPhotoRequest { Title string `form:"title"` File string `form:"file"` } ``` ### 响应规范 ```go // 基础响应 type BaseResponse { Code int `json:"code"` Msg string `json:"msg"` Data any `json:"data"` } // 单个资源响应 type PhotoResponse { BaseResponse Data Photo `json:"data"` } // 列表响应 type PhotoListResponse { BaseResponse Data PhotoListData `json:"data"` } type PhotoListData { List []Photo `json:"list"` Total int `json:"total"` Page int `json:"page"` Limit int `json:"limit"` } ``` ## 🧪 API 测试策略 ### 1. 手动测试 (curl) ```bash # 健康检查 curl -X GET "http://localhost:8888/api/v1/health" # 用户登录 curl -X POST "http://localhost:8888/api/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 上传照片 curl -X POST "http://localhost:8888/api/v1/photos" \ -H "Authorization: Bearer $TOKEN" \ -F "title=测试照片" \ -F "description=这是一张测试照片" \ -F "file=@test.jpg" # 获取照片列表 curl -X GET "http://localhost:8888/api/v1/photos?page=1&limit=10" \ -H "Authorization: Bearer $TOKEN" # 更新照片 curl -X PUT "http://localhost:8888/api/v1/photos/123" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"title":"更新后的标题"}' # 删除照片 curl -X DELETE "http://localhost:8888/api/v1/photos/123" \ -H "Authorization: Bearer $TOKEN" ``` ### 2. HTTP文件测试 创建 `test_api.http` 文件: ```http ### 登录获取token POST http://localhost:8888/api/v1/auth/login Content-Type: application/json { "username": "admin", "password": "123456" } ### 设置变量 @token = {{login.response.body.data.token}} ### 上传照片 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="file"; filename="test.jpg" Content-Type: image/jpeg < ./test.jpg --boundary-- ### 获取照片列表 GET http://localhost:8888/api/v1/photos?page=1&limit=5 Authorization: Bearer {{token}} ``` ## 🛡️ 安全和验证 ### 认证验证 ```go // 获取当前用户ID func (l *UploadPhotoLogic) getCurrentUserID() (string, error) { userID := l.ctx.Value("userID") if userID == nil { return "", errors.New("用户未认证") } return userID.(string), nil } ``` ### 参数验证 ```go func (l *UploadPhotoLogic) validateRequest(req *types.UploadPhotoRequest) error { if req.Title == "" { return errorx.NewDefaultError("照片标题不能为空") } if len(req.Title) > 100 { return errorx.NewDefaultError("照片标题不能超过100个字符") } return nil } ``` ### 文件验证 ```go func (l *UploadPhotoLogic) validateFile(fileHeader *multipart.FileHeader) error { // 文件大小验证 if fileHeader.Size > file.MaxFileSize { return errorx.NewDefaultError("文件大小不能超过10MB") } // 文件类型验证 if !file.IsImageFile(fileHeader.Filename) { return errorx.NewDefaultError("只支持图片文件") } return nil } ``` ## 📊 错误处理 ### 标准错误响应 ```go // 参数错误 return nil, errorx.NewCodeError(400, "参数错误") // 认证错误 return nil, errorx.NewCodeError(401, "未认证") // 权限错误 return nil, errorx.NewCodeError(403, "权限不足") // 资源不存在 return nil, errorx.NewCodeError(404, "照片不存在") // 服务器错误 return nil, errorx.NewCodeError(500, "服务器内部错误") ``` ### 业务错误处理 ```go func (l *UploadPhotoLogic) handleBusinessError(err error) error { switch { case errors.Is(err, sql.ErrNoRows): return errorx.NewCodeError(404, "资源不存在") case strings.Contains(err.Error(), "duplicate"): return errorx.NewCodeError(409, "资源已存在") default: logx.Errorf("业务处理失败: %v", err) return errorx.NewCodeError(500, "处理失败") } } ``` ## 🔧 开发工具 ### Makefile 命令 ```makefile # 生成API代码 api: goctl api go -api api/desc/photography.api -dir ./ # 启动服务 run: go run cmd/api/main.go # 构建 build: go build -o bin/photography-api cmd/api/main.go # 测试 test: go test ./... ``` ### 调试技巧 ```go // 添加调试日志 logx.Infof("处理上传照片请求: %+v", req) logx.Errorf("文件保存失败: %v", err) // 检查请求context userID := l.ctx.Value("userID") logx.Infof("当前用户: %v", userID) ``` 当前API开发状态参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md)。 # 后端 API 开发规则 ## 🔄 API 开发工作流 ### 1. 需求分析 - 明确API功能和参数 - 确定请求/响应格式 - 考虑错误处理场景 ### 2. API 定义 (.api文件) 在 `backend/api/desc/` 目录下定义: ```api // photo.api syntax = "v1" info( title: "Photography Photo API" desc: "照片管理相关API" author: "iriver" email: "iriver@example.com" version: "v1" ) import "common.api" type ( UploadPhotoRequest { Title string `form:"title"` Description string `form:"description,optional"` CategoryId string `form:"category_id,optional"` File string `form:"file"` } UploadPhotoResponse { Id string `json:"id"` Title string `json:"title"` Filename string `json:"filename"` Thumbnail string `json:"thumbnail"` CreatedAt string `json:"created_at"` } UpdatePhotoRequest { Id string `path:"id"` Title string `json:"title,optional"` Description string `json:"description,optional"` CategoryId string `json:"category_id,optional"` } ) @server( group: photo prefix: /api/v1 ) service photography-api { @doc "上传照片" @handler uploadPhoto post /photos (UploadPhotoRequest) returns (UploadPhotoResponse) @doc "更新照片信息" @handler updatePhoto put /photos/:id (UpdatePhotoRequest) returns (BaseResponse) @doc "删除照片" @handler deletePhoto delete /photos/:id (IdPathRequest) returns (BaseResponse) @doc "获取照片详情" @handler getPhoto get /photos/:id (IdPathRequest) returns (PhotoResponse) @doc "获取照片列表" @handler getPhotoList get /photos (PhotoListRequest) returns (PhotoListResponse) } ``` ### 3. 代码生成 ```bash cd backend make api ``` ### 4. Handler 实现模式 ```go func (h *UploadPhotoHandler) UploadPhoto(w http.ResponseWriter, r *http.Request) { var req types.UploadPhotoRequest if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } l := photo.NewUploadPhotoLogic(r.Context(), h.svcCtx) resp, err := l.UploadPhoto(&req, r) response.Response(w, resp, err) } ``` ### 5. Logic 实现模式 ```go func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest, r *http.Request) (resp *types.UploadPhotoResponse, err error) { // 1. 参数验证 if err = l.validateRequest(req); err != nil { return nil, err } // 2. 获取上传文件 fileHeader, err := l.getUploadFile(r) if err != nil { return nil, err } // 3. 文件处理 savedPath, thumbnail, err := l.processFile(fileHeader) if err != nil { return nil, err } // 4. 数据持久化 photo, err := l.savePhoto(req, savedPath, thumbnail) if err != nil { return nil, err } // 5. 构造响应 return l.buildResponse(photo), nil } ``` ## 📋 API 接口规范 ### 请求规范 ```go // 路径参数 type IdPathRequest { Id string `path:"id"` } // 查询参数 type PhotoListRequest { Page int `form:"page,default=1"` Limit int `form:"limit,default=10"` CategoryId string `form:"category_id,optional"` Keyword string `form:"keyword,optional"` } // JSON请求体 type UpdatePhotoRequest { Title string `json:"title,optional"` Description string `json:"description,optional"` CategoryId string `json:"category_id,optional"` } // 文件上传 (multipart/form-data) type UploadPhotoRequest { Title string `form:"title"` File string `form:"file"` } ``` ### 响应规范 ```go // 基础响应 type BaseResponse { Code int `json:"code"` Msg string `json:"msg"` Data any `json:"data"` } // 单个资源响应 type PhotoResponse { BaseResponse Data Photo `json:"data"` } // 列表响应 type PhotoListResponse { BaseResponse Data PhotoListData `json:"data"` } type PhotoListData { List []Photo `json:"list"` Total int `json:"total"` Page int `json:"page"` Limit int `json:"limit"` } ``` ## 🧪 API 测试策略 ### 1. 手动测试 (curl) ```bash # 健康检查 curl -X GET "http://localhost:8888/api/v1/health" # 用户登录 curl -X POST "http://localhost:8888/api/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 上传照片 curl -X POST "http://localhost:8888/api/v1/photos" \ -H "Authorization: Bearer $TOKEN" \ -F "title=测试照片" \ -F "description=这是一张测试照片" \ -F "file=@test.jpg" # 获取照片列表 curl -X GET "http://localhost:8888/api/v1/photos?page=1&limit=10" \ -H "Authorization: Bearer $TOKEN" # 更新照片 curl -X PUT "http://localhost:8888/api/v1/photos/123" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"title":"更新后的标题"}' # 删除照片 curl -X DELETE "http://localhost:8888/api/v1/photos/123" \ -H "Authorization: Bearer $TOKEN" ``` ### 2. HTTP文件测试 创建 `test_api.http` 文件: ```http ### 登录获取token POST http://localhost:8888/api/v1/auth/login Content-Type: application/json { "username": "admin", "password": "123456" } ### 设置变量 @token = {{login.response.body.data.token}} ### 上传照片 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="file"; filename="test.jpg" Content-Type: image/jpeg < ./test.jpg --boundary-- ### 获取照片列表 GET http://localhost:8888/api/v1/photos?page=1&limit=5 Authorization: Bearer {{token}} ``` ## 🛡️ 安全和验证 ### 认证验证 ```go // 获取当前用户ID func (l *UploadPhotoLogic) getCurrentUserID() (string, error) { userID := l.ctx.Value("userID") if userID == nil { return "", errors.New("用户未认证") } return userID.(string), nil } ``` ### 参数验证 ```go func (l *UploadPhotoLogic) validateRequest(req *types.UploadPhotoRequest) error { if req.Title == "" { return errorx.NewDefaultError("照片标题不能为空") } if len(req.Title) > 100 { return errorx.NewDefaultError("照片标题不能超过100个字符") } return nil } ``` ### 文件验证 ```go func (l *UploadPhotoLogic) validateFile(fileHeader *multipart.FileHeader) error { // 文件大小验证 if fileHeader.Size > file.MaxFileSize { return errorx.NewDefaultError("文件大小不能超过10MB") } // 文件类型验证 if !file.IsImageFile(fileHeader.Filename) { return errorx.NewDefaultError("只支持图片文件") } return nil } ``` ## 📊 错误处理 ### 标准错误响应 ```go // 参数错误 return nil, errorx.NewCodeError(400, "参数错误") // 认证错误 return nil, errorx.NewCodeError(401, "未认证") // 权限错误 return nil, errorx.NewCodeError(403, "权限不足") // 资源不存在 return nil, errorx.NewCodeError(404, "照片不存在") // 服务器错误 return nil, errorx.NewCodeError(500, "服务器内部错误") ``` ### 业务错误处理 ```go func (l *UploadPhotoLogic) handleBusinessError(err error) error { switch { case errors.Is(err, sql.ErrNoRows): return errorx.NewCodeError(404, "资源不存在") case strings.Contains(err.Error(), "duplicate"): return errorx.NewCodeError(409, "资源已存在") default: logx.Errorf("业务处理失败: %v", err) return errorx.NewCodeError(500, "处理失败") } } ``` ## 🔧 开发工具 ### Makefile 命令 ```makefile # 生成API代码 api: goctl api go -api api/desc/photography.api -dir ./ # 启动服务 run: go run cmd/api/main.go # 构建 build: go build -o bin/photography-api cmd/api/main.go # 测试 test: go test ./... ``` ### 调试技巧 ```go // 添加调试日志 logx.Infof("处理上传照片请求: %+v", req) logx.Errorf("文件保存失败: %v", err) // 检查请求context userID := l.ctx.Value("userID") logx.Infof("当前用户: %v", userID) ``` 当前API开发状态参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md)。