Files
photography/.cursor/rules/backend/api-development.mdc
xujiang 604b9e59ba fix
2025-07-10 18:09:11 +08:00

751 lines
16 KiB
Plaintext

---
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)。