fix
This commit is contained in:
750
.cursor/rules/backend/api-development.mdc
Normal file
750
.cursor/rules/backend/api-development.mdc
Normal file
@ -0,0 +1,750 @@
|
||||
---
|
||||
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)。
|
||||
492
.cursor/rules/backend/go-zero-framework.mdc
Normal file
492
.cursor/rules/backend/go-zero-framework.mdc
Normal file
@ -0,0 +1,492 @@
|
||||
---
|
||||
globs: backend/*,backend/**/*.go
|
||||
alwaysApply: false
|
||||
---
|
||||
# Go-Zero 框架开发规则
|
||||
|
||||
## 🚀 架构规范
|
||||
|
||||
### 核心概念
|
||||
- **Handler**: HTTP请求处理层,只做参数验证和调用Logic
|
||||
- **Logic**: 业务逻辑层,包含核心业务代码
|
||||
- **Model**: 数据访问层,处理数据库操作
|
||||
- **Types**: 请求/响应类型定义
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
backend/
|
||||
├── cmd/api/main.go # 服务启动入口
|
||||
├── etc/photography-api.yaml # 配置文件
|
||||
├── api/desc/ # API定义文件
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP处理器
|
||||
│ ├── logic/ # 业务逻辑
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── svc/ # 服务上下文
|
||||
│ └── types/ # 类型定义
|
||||
└── pkg/ # 工具包
|
||||
```
|
||||
|
||||
## 🎯 开发流程
|
||||
|
||||
### 1. API定义
|
||||
在 `api/desc/` 目录下定义接口:
|
||||
```api
|
||||
service photography-api {
|
||||
@handler uploadPhoto
|
||||
post /api/v1/photos (UploadPhotoRequest) returns (UploadPhotoResponse)
|
||||
}
|
||||
|
||||
type UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
Description string `form:"description,optional"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
|
||||
type UploadPhotoResponse {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Filename string `json:"filename"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 代码生成
|
||||
```bash
|
||||
cd backend
|
||||
make api # 生成handler和logic骨架
|
||||
```
|
||||
|
||||
### 3. 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 := logic.NewUploadPhotoLogic(r.Context(), h.svcCtx)
|
||||
resp, err := l.UploadPhoto(&req)
|
||||
response.Response(w, resp, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Logic实现
|
||||
```go
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (*types.UploadPhotoResponse, error) {
|
||||
// 1. 参数验证
|
||||
if req.Title == "" {
|
||||
return nil, errorx.NewDefaultError("照片标题不能为空")
|
||||
}
|
||||
|
||||
// 2. 业务逻辑
|
||||
photoID := uuid.New().String()
|
||||
|
||||
// 3. 数据持久化
|
||||
photo := &model.Photo{
|
||||
ID: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}
|
||||
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.UploadPhotoResponse{
|
||||
Id: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 工具包使用
|
||||
|
||||
### 文件处理
|
||||
使用 [pkg/utils/file/file.go](mdc:backend/pkg/utils/file/file.go):
|
||||
```go
|
||||
import "photography/pkg/utils/file"
|
||||
|
||||
// 验证图片文件
|
||||
if !file.IsImageFile(filename) {
|
||||
return errors.New("不支持的文件类型")
|
||||
}
|
||||
|
||||
// 保存文件并生成缩略图
|
||||
savedPath, thumbnail, err := file.SaveImage(fileData, filename)
|
||||
```
|
||||
|
||||
### JWT认证
|
||||
使用 [pkg/utils/jwt/jwt.go](mdc:backend/pkg/utils/jwt/jwt.go):
|
||||
```go
|
||||
import "photography/pkg/utils/jwt"
|
||||
|
||||
// 生成token
|
||||
token, err := jwt.GenerateToken(userID, username)
|
||||
|
||||
// 验证token
|
||||
userID, err := jwt.ParseToken(tokenString)
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
使用 [pkg/errorx/errorx.go](mdc:backend/pkg/errorx/errorx.go):
|
||||
```go
|
||||
import "photography/pkg/errorx"
|
||||
|
||||
// 业务错误
|
||||
return nil, errorx.NewDefaultError("用户不存在")
|
||||
|
||||
// 自定义错误码
|
||||
return nil, errorx.NewCodeError(40001, "参数错误")
|
||||
```
|
||||
|
||||
## 🛡️ 中间件
|
||||
|
||||
### JWT认证中间件
|
||||
在 `internal/middleware/auth.go` 中实现:
|
||||
```go
|
||||
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
httpx.Error(w, errors.New("未提供认证token"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := jwt.ParseToken(strings.TrimPrefix(token, "Bearer "))
|
||||
if err != nil {
|
||||
httpx.Error(w, errors.New("token无效"))
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户ID注入到context
|
||||
ctx := context.WithValue(r.Context(), "userID", userID)
|
||||
next(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 数据模型
|
||||
|
||||
### 模型定义示例
|
||||
```go
|
||||
type Photo struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Filename string `db:"filename" json:"filename"`
|
||||
Thumbnail string `db:"thumbnail" json:"thumbnail"`
|
||||
CategoryID string `db:"category_id" json:"category_id"`
|
||||
UserID string `db:"user_id" json:"user_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
```go
|
||||
// 插入
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
|
||||
// 查询
|
||||
photo, err := l.svcCtx.PhotoModel.FindOne(l.ctx, photoID)
|
||||
|
||||
// 更新
|
||||
err := l.svcCtx.PhotoModel.Update(l.ctx, photo)
|
||||
|
||||
// 删除
|
||||
err := l.svcCtx.PhotoModel.Delete(l.ctx, photoID)
|
||||
```
|
||||
|
||||
## 🔄 配置管理
|
||||
|
||||
### 配置文件: `etc/photography-api.yaml`
|
||||
```yaml
|
||||
Name: photography-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
|
||||
Auth:
|
||||
AccessSecret: your-secret-key
|
||||
AccessExpire: 86400
|
||||
|
||||
DataSource: photography.db
|
||||
|
||||
Log:
|
||||
ServiceName: photography-api
|
||||
Mode: file
|
||||
Path: logs
|
||||
Level: info
|
||||
```
|
||||
|
||||
## 🧪 测试规范
|
||||
|
||||
### 单元测试
|
||||
```go
|
||||
func TestUploadPhotoLogic(t *testing.T) {
|
||||
// 准备测试数据
|
||||
req := &types.UploadPhotoRequest{
|
||||
Title: "测试照片",
|
||||
File: "test.jpg",
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
logic := NewUploadPhotoLogic(context.Background(), svcCtx)
|
||||
resp, err := logic.UploadPhoto(req)
|
||||
|
||||
// 断言结果
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Id)
|
||||
assert.Equal(t, "测试照片", resp.Title)
|
||||
}
|
||||
```
|
||||
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解当前后端开发进度。
|
||||
# Go-Zero 框架开发规则
|
||||
|
||||
## 🚀 架构规范
|
||||
|
||||
### 核心概念
|
||||
- **Handler**: HTTP请求处理层,只做参数验证和调用Logic
|
||||
- **Logic**: 业务逻辑层,包含核心业务代码
|
||||
- **Model**: 数据访问层,处理数据库操作
|
||||
- **Types**: 请求/响应类型定义
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
backend/
|
||||
├── cmd/api/main.go # 服务启动入口
|
||||
├── etc/photography-api.yaml # 配置文件
|
||||
├── api/desc/ # API定义文件
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP处理器
|
||||
│ ├── logic/ # 业务逻辑
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── svc/ # 服务上下文
|
||||
│ └── types/ # 类型定义
|
||||
└── pkg/ # 工具包
|
||||
```
|
||||
|
||||
## 🎯 开发流程
|
||||
|
||||
### 1. API定义
|
||||
在 `api/desc/` 目录下定义接口:
|
||||
```api
|
||||
service photography-api {
|
||||
@handler uploadPhoto
|
||||
post /api/v1/photos (UploadPhotoRequest) returns (UploadPhotoResponse)
|
||||
}
|
||||
|
||||
type UploadPhotoRequest {
|
||||
Title string `form:"title"`
|
||||
Description string `form:"description,optional"`
|
||||
File string `form:"file"`
|
||||
}
|
||||
|
||||
type UploadPhotoResponse {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Filename string `json:"filename"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 代码生成
|
||||
```bash
|
||||
cd backend
|
||||
make api # 生成handler和logic骨架
|
||||
```
|
||||
|
||||
### 3. 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 := logic.NewUploadPhotoLogic(r.Context(), h.svcCtx)
|
||||
resp, err := l.UploadPhoto(&req)
|
||||
response.Response(w, resp, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Logic实现
|
||||
```go
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (*types.UploadPhotoResponse, error) {
|
||||
// 1. 参数验证
|
||||
if req.Title == "" {
|
||||
return nil, errorx.NewDefaultError("照片标题不能为空")
|
||||
}
|
||||
|
||||
// 2. 业务逻辑
|
||||
photoID := uuid.New().String()
|
||||
|
||||
// 3. 数据持久化
|
||||
photo := &model.Photo{
|
||||
ID: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}
|
||||
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.UploadPhotoResponse{
|
||||
Id: photoID,
|
||||
Title: req.Title,
|
||||
// ...
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 工具包使用
|
||||
|
||||
### 文件处理
|
||||
使用 [pkg/utils/file/file.go](mdc:backend/pkg/utils/file/file.go):
|
||||
```go
|
||||
import "photography/pkg/utils/file"
|
||||
|
||||
// 验证图片文件
|
||||
if !file.IsImageFile(filename) {
|
||||
return errors.New("不支持的文件类型")
|
||||
}
|
||||
|
||||
// 保存文件并生成缩略图
|
||||
savedPath, thumbnail, err := file.SaveImage(fileData, filename)
|
||||
```
|
||||
|
||||
### JWT认证
|
||||
使用 [pkg/utils/jwt/jwt.go](mdc:backend/pkg/utils/jwt/jwt.go):
|
||||
```go
|
||||
import "photography/pkg/utils/jwt"
|
||||
|
||||
// 生成token
|
||||
token, err := jwt.GenerateToken(userID, username)
|
||||
|
||||
// 验证token
|
||||
userID, err := jwt.ParseToken(tokenString)
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
使用 [pkg/errorx/errorx.go](mdc:backend/pkg/errorx/errorx.go):
|
||||
```go
|
||||
import "photography/pkg/errorx"
|
||||
|
||||
// 业务错误
|
||||
return nil, errorx.NewDefaultError("用户不存在")
|
||||
|
||||
// 自定义错误码
|
||||
return nil, errorx.NewCodeError(40001, "参数错误")
|
||||
```
|
||||
|
||||
## 🛡️ 中间件
|
||||
|
||||
### JWT认证中间件
|
||||
在 `internal/middleware/auth.go` 中实现:
|
||||
```go
|
||||
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
httpx.Error(w, errors.New("未提供认证token"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := jwt.ParseToken(strings.TrimPrefix(token, "Bearer "))
|
||||
if err != nil {
|
||||
httpx.Error(w, errors.New("token无效"))
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户ID注入到context
|
||||
ctx := context.WithValue(r.Context(), "userID", userID)
|
||||
next(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 数据模型
|
||||
|
||||
### 模型定义示例
|
||||
```go
|
||||
type Photo struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Filename string `db:"filename" json:"filename"`
|
||||
Thumbnail string `db:"thumbnail" json:"thumbnail"`
|
||||
CategoryID string `db:"category_id" json:"category_id"`
|
||||
UserID string `db:"user_id" json:"user_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
```go
|
||||
// 插入
|
||||
err := l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
|
||||
// 查询
|
||||
photo, err := l.svcCtx.PhotoModel.FindOne(l.ctx, photoID)
|
||||
|
||||
// 更新
|
||||
err := l.svcCtx.PhotoModel.Update(l.ctx, photo)
|
||||
|
||||
// 删除
|
||||
err := l.svcCtx.PhotoModel.Delete(l.ctx, photoID)
|
||||
```
|
||||
|
||||
## 🔄 配置管理
|
||||
|
||||
### 配置文件: `etc/photography-api.yaml`
|
||||
```yaml
|
||||
Name: photography-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
|
||||
Auth:
|
||||
AccessSecret: your-secret-key
|
||||
AccessExpire: 86400
|
||||
|
||||
DataSource: photography.db
|
||||
|
||||
Log:
|
||||
ServiceName: photography-api
|
||||
Mode: file
|
||||
Path: logs
|
||||
Level: info
|
||||
```
|
||||
|
||||
## 🧪 测试规范
|
||||
|
||||
### 单元测试
|
||||
```go
|
||||
func TestUploadPhotoLogic(t *testing.T) {
|
||||
// 准备测试数据
|
||||
req := &types.UploadPhotoRequest{
|
||||
Title: "测试照片",
|
||||
File: "test.jpg",
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
logic := NewUploadPhotoLogic(context.Background(), svcCtx)
|
||||
resp, err := logic.UploadPhoto(req)
|
||||
|
||||
// 断言结果
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Id)
|
||||
assert.Equal(t, "测试照片", resp.Title)
|
||||
}
|
||||
```
|
||||
|
||||
参考 [TASK_PROGRESS.md](mdc:TASK_PROGRESS.md) 了解当前后端开发进度。
|
||||
Reference in New Issue
Block a user