2025-07-02 16:17:59 +08:00
|
|
|
|
# TYAPI Server API 开发规范
|
|
|
|
|
|
|
|
|
|
|
|
## 🏗️ 项目架构概览
|
|
|
|
|
|
|
|
|
|
|
|
本项目采用 **DDD(领域驱动设计)** + **Clean Architecture** + **事件驱动架构**,基于 Gin 框架构建的企业级后端 API 服务。
|
|
|
|
|
|
|
|
|
|
|
|
## 📋 目录结构规范
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
internal/
|
|
|
|
|
|
├── domains/ # 领域层
|
|
|
|
|
|
│ └── user/ # 用户领域
|
|
|
|
|
|
│ ├── dto/ # 数据传输对象
|
|
|
|
|
|
│ ├── entities/ # 实体
|
|
|
|
|
|
│ ├── events/ # 领域事件
|
|
|
|
|
|
│ ├── handlers/ # HTTP处理器
|
|
|
|
|
|
│ ├── repositories/ # 仓储接口实现
|
|
|
|
|
|
│ ├── routes/ # 路由配置
|
|
|
|
|
|
│ ├── services/ # 领域服务
|
|
|
|
|
|
│ └── validators/ # 验证器
|
|
|
|
|
|
├── shared/ # 共享基础设施
|
|
|
|
|
|
│ ├── interfaces/ # 接口定义
|
|
|
|
|
|
│ ├── middleware/ # 中间件
|
|
|
|
|
|
│ ├── http/ # HTTP基础组件
|
|
|
|
|
|
│ └── ...
|
|
|
|
|
|
└── config/ # 配置管理
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🎯 业务分层架构
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 控制器层 (Handlers)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/domains/user/handlers/user_handler.go
|
|
|
|
|
|
type UserHandler struct {
|
|
|
|
|
|
userService *services.UserService // 注入领域服务
|
|
|
|
|
|
response interfaces.ResponseBuilder // 统一响应构建器
|
|
|
|
|
|
validator interfaces.RequestValidator // 请求验证器
|
|
|
|
|
|
logger *zap.Logger // 结构化日志
|
|
|
|
|
|
jwtAuth *middleware.JWTAuthMiddleware // JWT认证
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标准CRUD处理器方法
|
|
|
|
|
|
func (h *UserHandler) Create(c *gin.Context) {
|
|
|
|
|
|
var req dto.CreateUserRequest
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 请求验证
|
|
|
|
|
|
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
|
|
|
|
return // 验证器已处理响应
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 调用领域服务
|
|
|
|
|
|
user, err := h.userService.Create(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("Failed to create user", zap.Error(err))
|
|
|
|
|
|
h.response.BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 统一响应格式
|
|
|
|
|
|
response := dto.FromEntity(user)
|
|
|
|
|
|
h.response.Created(c, response, "User created successfully")
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 服务层 (Services)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/domains/user/services/user_service.go
|
|
|
|
|
|
type UserService struct {
|
|
|
|
|
|
repo *repositories.UserRepository // 数据访问
|
|
|
|
|
|
eventBus interfaces.EventBus // 事件总线
|
|
|
|
|
|
logger *zap.Logger // 日志
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *UserService) Create(ctx context.Context, req *dto.CreateUserRequest) (*entities.User, error) {
|
|
|
|
|
|
// 1. 业务规则验证
|
|
|
|
|
|
if err := s.validateCreateUser(req); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 实体创建
|
|
|
|
|
|
user := entities.NewUser(req.Username, req.Email, req.Password)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 数据持久化
|
|
|
|
|
|
if err := s.repo.Create(ctx, user); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 发布领域事件
|
|
|
|
|
|
event := events.NewUserCreatedEvent(user.ID, user.Username, user.Email)
|
|
|
|
|
|
s.eventBus.PublishAsync(ctx, event)
|
|
|
|
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 仓储层 (Repositories)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/domains/user/repositories/user_repository.go
|
|
|
|
|
|
type UserRepository struct {
|
|
|
|
|
|
db *gorm.DB // 数据库连接
|
|
|
|
|
|
cache interfaces.CacheService // 缓存服务
|
|
|
|
|
|
logger *zap.Logger // 日志
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *UserRepository) Create(ctx context.Context, user *entities.User) error {
|
|
|
|
|
|
// 使用事务确保数据一致性
|
|
|
|
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
|
if err := tx.Create(user).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除相关缓存
|
|
|
|
|
|
r.cache.Delete(ctx, fmt.Sprintf("user:count"))
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. DTO 层 (数据传输对象)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/domains/user/dto/user_dto.go
|
|
|
|
|
|
type CreateUserRequest struct {
|
|
|
|
|
|
Username string `json:"username" binding:"required,min=3,max=50" validate:"username"`
|
|
|
|
|
|
Email string `json:"email" binding:"required,email" validate:"email"`
|
|
|
|
|
|
Password string `json:"password" binding:"required,min=8" validate:"password"`
|
|
|
|
|
|
DisplayName string `json:"display_name" binding:"max=100"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type UserResponse struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
|
DisplayName string `json:"display_name"`
|
|
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 实体转换函数
|
|
|
|
|
|
func FromEntity(user *entities.User) *UserResponse {
|
|
|
|
|
|
return &UserResponse{
|
|
|
|
|
|
ID: user.ID,
|
|
|
|
|
|
Username: user.Username,
|
|
|
|
|
|
Email: user.Email,
|
|
|
|
|
|
DisplayName: user.DisplayName,
|
|
|
|
|
|
CreatedAt: user.CreatedAt,
|
|
|
|
|
|
UpdatedAt: user.UpdatedAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🛣️ 路由配置规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. DDD 领域路由设计模式
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/domains/user/routes/user_routes.go
|
|
|
|
|
|
type UserRoutes struct {
|
|
|
|
|
|
handler *handlers.UserHandler
|
|
|
|
|
|
jwtAuth *middleware.JWTAuthMiddleware
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *UserRoutes) RegisterRoutes(router *gin.Engine) {
|
|
|
|
|
|
// API版本组
|
|
|
|
|
|
v1 := router.Group("/api/v1")
|
|
|
|
|
|
|
|
|
|
|
|
// 🏢 用户域路由组 - 按域名组织
|
|
|
|
|
|
users := v1.Group("/users")
|
|
|
|
|
|
{
|
|
|
|
|
|
// 🌍 公开路由(不需要认证)
|
|
|
|
|
|
users.POST("/send-code", r.handler.SendCode) // 发送验证码
|
|
|
|
|
|
users.POST("/register", r.handler.Register) // 用户注册
|
|
|
|
|
|
users.POST("/login", r.handler.Login) // 用户登录
|
|
|
|
|
|
|
|
|
|
|
|
// 🔐 需要认证的路由
|
|
|
|
|
|
authenticated := users.Group("")
|
|
|
|
|
|
authenticated.Use(r.jwtAuth.Handle())
|
|
|
|
|
|
{
|
|
|
|
|
|
authenticated.GET("/me", r.handler.GetProfile) // 获取当前用户信息
|
|
|
|
|
|
authenticated.PUT("/me/password", r.handler.ChangePassword) // 修改密码
|
|
|
|
|
|
// 未来扩展示例:
|
|
|
|
|
|
// authenticated.PUT("/me", r.handler.UpdateProfile) // 更新用户信息
|
|
|
|
|
|
// authenticated.DELETE("/me", r.handler.DeleteAccount) // 删除账户
|
|
|
|
|
|
// authenticated.GET("/me/sessions", r.handler.GetSessions) // 获取登录会话
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 📱 SMS验证码域路由组(如果需要单独管理SMS)
|
|
|
|
|
|
sms := v1.Group("/sms")
|
|
|
|
|
|
{
|
|
|
|
|
|
sms.POST("/send", r.handler.SendCode) // 发送验证码
|
|
|
|
|
|
// 未来可以添加:
|
|
|
|
|
|
// sms.POST("/verify", r.handler.VerifyCode) // 验证验证码
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. DDD 多域路由架构
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 按域组织路由,支持横向扩展
|
|
|
|
|
|
func (r *UserRoutes) RegisterRoutes(router *gin.Engine) {
|
|
|
|
|
|
v1 := router.Group("/api/v1")
|
|
|
|
|
|
|
|
|
|
|
|
// 👥 用户域
|
|
|
|
|
|
users := v1.Group("/users")
|
|
|
|
|
|
// 📦 订单域
|
|
|
|
|
|
orders := v1.Group("/orders")
|
|
|
|
|
|
// 🛍️ 商品域
|
|
|
|
|
|
products := v1.Group("/products")
|
|
|
|
|
|
// 💰 支付域
|
|
|
|
|
|
payments := v1.Group("/payments")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 多级权限路由分层
|
|
|
|
|
|
users := v1.Group("/users")
|
|
|
|
|
|
{
|
|
|
|
|
|
// Level 1: 公开路由
|
|
|
|
|
|
users.POST("/register", r.handler.Register)
|
|
|
|
|
|
users.POST("/login", r.handler.Login)
|
|
|
|
|
|
|
|
|
|
|
|
// Level 2: 用户认证路由
|
|
|
|
|
|
authenticated := users.Group("")
|
|
|
|
|
|
authenticated.Use(r.jwtAuth.Handle())
|
|
|
|
|
|
{
|
|
|
|
|
|
authenticated.GET("/me", r.handler.GetProfile)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Level 3: 管理员路由
|
|
|
|
|
|
admin := users.Group("/admin")
|
|
|
|
|
|
admin.Use(r.jwtAuth.Handle(), r.adminAuth.Handle())
|
|
|
|
|
|
{
|
|
|
|
|
|
admin.GET("", r.handler.AdminList)
|
|
|
|
|
|
admin.DELETE("/:id", r.handler.AdminDelete)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. DDD 路由命名最佳实践
|
|
|
|
|
|
|
|
|
|
|
|
#### ✅ **推荐做法** - 领域导向设计:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 🏢 按业务域划分路由
|
|
|
|
|
|
/api/v1/users/* # 用户域的所有操作
|
|
|
|
|
|
/api/v1/orders/* # 订单域的所有操作
|
|
|
|
|
|
/api/v1/products/* # 商品域的所有操作
|
|
|
|
|
|
/api/v1/payments/* # 支付域的所有操作
|
|
|
|
|
|
|
|
|
|
|
|
// 📋 资源操作使用名词复数
|
|
|
|
|
|
POST /api/v1/users/register # 用户注册
|
|
|
|
|
|
POST /api/v1/users/login # 用户登录
|
|
|
|
|
|
GET /api/v1/users/me # 获取当前用户
|
|
|
|
|
|
PUT /api/v1/users/me/password # 修改当前用户密码
|
|
|
|
|
|
|
|
|
|
|
|
// 🔗 体现资源关系的嵌套路径
|
|
|
|
|
|
GET /api/v1/users/me/orders # 获取当前用户的订单
|
|
|
|
|
|
GET /api/v1/orders/123/items # 获取订单的商品项目
|
|
|
|
|
|
POST /api/v1/products/456/reviews # 为商品添加评论
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### ❌ **避免的做法** - 技术导向设计:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// ❌ 技术导向路径
|
|
|
|
|
|
/api/v1/auth/* # 混合了多个域的认证操作
|
|
|
|
|
|
/api/v1/service/* # 不明确的服务路径
|
|
|
|
|
|
/api/v1/api/* # 冗余的api前缀
|
|
|
|
|
|
|
|
|
|
|
|
// ❌ 动词路径
|
|
|
|
|
|
/api/v1/getUserInfo # 应该用 GET /users/me
|
|
|
|
|
|
/api/v1/changeUserPassword # 应该用 PUT /users/me/password
|
|
|
|
|
|
/api/v1/deleteUserAccount # 应该用 DELETE /users/me
|
|
|
|
|
|
|
|
|
|
|
|
// ❌ 混合域概念
|
|
|
|
|
|
/api/v1/userorders # 应该分离为 /users/me/orders
|
|
|
|
|
|
/api/v1/authprofile # 应该分离为 /users/me
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🔐 权限控制体系
|
|
|
|
|
|
|
|
|
|
|
|
### 1. JWT 认证中间件
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 强制认证中间件
|
|
|
|
|
|
type JWTAuthMiddleware struct {
|
|
|
|
|
|
config *config.Config
|
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 可选认证中间件(支持游客访问)
|
|
|
|
|
|
type OptionalAuthMiddleware struct {
|
|
|
|
|
|
jwtAuth *JWTAuthMiddleware
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用方式
|
|
|
|
|
|
protected.Use(r.jwtAuth.Handle()) // 强制认证
|
|
|
|
|
|
public.Use(r.optionalAuth.Handle()) // 可选认证
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 权限验证模式
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 在Handler中获取当前用户
|
|
|
|
|
|
func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
|
|
|
|
|
userID, exists := c.Get("user_id")
|
|
|
|
|
|
if !exists {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return userID.(string)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 权限检查示例
|
|
|
|
|
|
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
|
|
|
|
|
userID := h.getCurrentUserID(c)
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
h.response.Unauthorized(c, "User not authenticated")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 业务逻辑...
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 权限级别定义
|
|
|
|
|
|
|
|
|
|
|
|
- **Public**: 公开接口,无需认证
|
|
|
|
|
|
- **User**: 需要用户登录
|
|
|
|
|
|
- **Admin**: 需要管理员权限
|
|
|
|
|
|
- **Owner**: 需要资源所有者权限
|
|
|
|
|
|
|
|
|
|
|
|
## 📝 API 响应规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 统一响应格式 (APIResponse 结构)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 标准API响应结构
|
|
|
|
|
|
type APIResponse struct {
|
|
|
|
|
|
Success bool `json:"success"` // 操作是否成功
|
|
|
|
|
|
Message string `json:"message"` // 响应消息(中文)
|
|
|
|
|
|
Data interface{} `json:"data,omitempty"` // 响应数据
|
|
|
|
|
|
Errors interface{} `json:"errors,omitempty"` // 错误详情
|
|
|
|
|
|
Pagination *PaginationMeta `json:"pagination,omitempty"` // 分页信息
|
|
|
|
|
|
Meta map[string]interface{} `json:"meta,omitempty"` // 元数据
|
|
|
|
|
|
RequestID string `json:"request_id"` // 请求追踪ID
|
|
|
|
|
|
Timestamp int64 `json:"timestamp"` // Unix时间戳
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分页元数据结构
|
|
|
|
|
|
type PaginationMeta struct {
|
|
|
|
|
|
Page int `json:"page"` // 当前页码
|
|
|
|
|
|
PageSize int `json:"page_size"` // 每页大小
|
|
|
|
|
|
Total int64 `json:"total"` // 总记录数
|
|
|
|
|
|
TotalPages int `json:"total_pages"` // 总页数
|
|
|
|
|
|
HasNext bool `json:"has_next"` // 是否有下一页
|
|
|
|
|
|
HasPrev bool `json:"has_prev"` // 是否有上一页
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 成功响应格式示例
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
// 查询成功响应 (200 OK)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "获取成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建成功响应 (201 Created)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "用户注册成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 登录成功响应 (200 OK)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "登录成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"user": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
|
|
|
|
"token_type": "Bearer",
|
|
|
|
|
|
"expires_in": 86400,
|
|
|
|
|
|
"login_method": "password"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分页响应 (200 OK)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "获取成功",
|
|
|
|
|
|
"data": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
"pagination": {
|
|
|
|
|
|
"page": 1,
|
|
|
|
|
|
"page_size": 10,
|
|
|
|
|
|
"total": 100,
|
|
|
|
|
|
"total_pages": 10,
|
|
|
|
|
|
"has_next": true,
|
|
|
|
|
|
"has_prev": false
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 错误响应格式示例
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
// 参数验证错误 (400 Bad Request)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "请求参数错误",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"phone": ["手机号必须为11位数字"],
|
|
|
|
|
|
"password": ["密码长度至少6位"],
|
|
|
|
|
|
"confirm_password": ["确认密码必须与密码一致"]
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证码错误 (422 Unprocessable Entity)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "验证失败",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"phone": ["手机号必须为11位数字"],
|
|
|
|
|
|
"code": ["验证码必须为6位数字"]
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 业务逻辑错误 (400 Bad Request)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "手机号已存在",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 认证错误 (401 Unauthorized)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "用户未登录或token已过期",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 权限错误 (403 Forbidden)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "权限不足,无法访问此资源",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 资源不存在 (404 Not Found)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "请求的资源不存在",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 资源冲突 (409 Conflict)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "手机号已被注册",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 限流错误 (429 Too Many Requests)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "请求过于频繁,请稍后再试",
|
|
|
|
|
|
"meta": {
|
|
|
|
|
|
"retry_after": "60s"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 服务器错误 (500 Internal Server Error)
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "服务器内部错误",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 🚦 限流中间件和 TooManyRequests 详解
|
|
|
|
|
|
|
|
|
|
|
|
#### 限流配置
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
# config.yaml 限流配置
|
|
|
|
|
|
ratelimit:
|
|
|
|
|
|
requests: 1000 # 每个时间窗口允许的请求数
|
|
|
|
|
|
window: 60s # 时间窗口大小
|
|
|
|
|
|
burst: 200 # 突发请求允许数
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 限流中间件实现
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// RateLimitMiddleware 限流中间件(修复后的版本)
|
|
|
|
|
|
type RateLimitMiddleware struct {
|
|
|
|
|
|
config *config.Config
|
|
|
|
|
|
response interfaces.ResponseBuilder // ✅ 使用统一响应格式
|
|
|
|
|
|
limiters map[string]*rate.Limiter
|
|
|
|
|
|
mutex sync.RWMutex
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle 限流处理逻辑
|
|
|
|
|
|
func (m *RateLimitMiddleware) Handle() gin.HandlerFunc {
|
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
|
clientID := m.getClientID(c) // 获取客户端ID(通常是IP地址)
|
|
|
|
|
|
limiter := m.getLimiter(clientID)
|
|
|
|
|
|
|
|
|
|
|
|
if !limiter.Allow() {
|
|
|
|
|
|
// 添加限流头部信息
|
|
|
|
|
|
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", m.config.RateLimit.Requests))
|
|
|
|
|
|
c.Header("X-RateLimit-Window", m.config.RateLimit.Window.String())
|
|
|
|
|
|
c.Header("Retry-After", "60")
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 使用统一的TooManyRequests响应格式(修复前是c.JSON)
|
|
|
|
|
|
m.response.TooManyRequests(c, "请求过于频繁,请稍后再试")
|
|
|
|
|
|
c.Abort()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.Next()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 多层限流保护
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 🔹 1. 全局IP限流(中间件层)
|
|
|
|
|
|
// 通过RateLimitMiddleware自动处理,返回429状态码
|
|
|
|
|
|
|
|
|
|
|
|
// 🔹 2. 短信发送限流(业务层)
|
|
|
|
|
|
func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error {
|
|
|
|
|
|
// 最小发送间隔检查
|
|
|
|
|
|
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
|
|
|
|
|
|
if lastSent exists && now.Sub(lastSent) < s.config.RateLimit.MinInterval {
|
|
|
|
|
|
return fmt.Errorf("请等待 %v 后再试", s.config.RateLimit.MinInterval)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 每小时发送限制
|
|
|
|
|
|
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
|
|
|
|
|
if hourlyCount >= s.config.RateLimit.HourlyLimit {
|
|
|
|
|
|
return fmt.Errorf("每小时最多发送 %d 条短信", s.config.RateLimit.HourlyLimit)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔹 3. Handler层限流错误处理
|
|
|
|
|
|
func (h *UserHandler) SendSMSCode(c *gin.Context) {
|
|
|
|
|
|
err := h.smsCodeService.SendCode(ctx, &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 检查是否是限流错误
|
|
|
|
|
|
if strings.Contains(err.Error(), "请等待") ||
|
|
|
|
|
|
strings.Contains(err.Error(), "最多发送") {
|
|
|
|
|
|
// ✅ 使用TooManyRequests响应
|
|
|
|
|
|
h.response.TooManyRequests(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.response.BadRequest(c, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.response.Success(c, nil, "验证码发送成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 限流响应示例
|
|
|
|
|
|
|
|
|
|
|
|
**中间件层限流**(全局 IP 限流):
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "请求过于频繁,请稍后再试",
|
|
|
|
|
|
"meta": {
|
|
|
|
|
|
"retry_after": "60s"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**业务层限流**(短信发送限流):
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "请等待 60 秒后再试",
|
|
|
|
|
|
"meta": {
|
|
|
|
|
|
"retry_after": "60s"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### TooManyRequests 使用场景
|
|
|
|
|
|
|
|
|
|
|
|
- 🚫 **全局限流**: IP 请求频率限制
|
|
|
|
|
|
- 📱 **短信限流**: 验证码发送频率限制
|
|
|
|
|
|
- 🔐 **登录限流**: 防止暴力破解
|
|
|
|
|
|
- 📧 **邮件限流**: 邮件发送频率限制
|
|
|
|
|
|
- <20><> **搜索限流**: 防止恶意搜索
|
|
|
|
|
|
|
|
|
|
|
|
### 4. ResponseBuilder 响应构建器使用
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 成功响应
|
|
|
|
|
|
h.response.Success(c, data, "获取成功")
|
|
|
|
|
|
h.response.Created(c, data, "创建成功")
|
|
|
|
|
|
|
|
|
|
|
|
// 客户端错误响应
|
|
|
|
|
|
h.response.BadRequest(c, "请求参数错误", validationErrors)
|
|
|
|
|
|
h.response.Unauthorized(c, "用户未登录或token已过期")
|
|
|
|
|
|
h.response.Forbidden(c, "权限不足,无法访问此资源")
|
|
|
|
|
|
h.response.NotFound(c, "请求的资源不存在")
|
|
|
|
|
|
h.response.Conflict(c, "手机号已被注册")
|
|
|
|
|
|
h.response.ValidationError(c, validationErrors)
|
|
|
|
|
|
h.response.TooManyRequests(c, "请求过于频繁,请稍后再试")
|
|
|
|
|
|
|
|
|
|
|
|
// 服务器错误响应
|
|
|
|
|
|
h.response.InternalError(c, "服务器内部错误")
|
|
|
|
|
|
|
|
|
|
|
|
// 分页响应
|
|
|
|
|
|
h.response.Paginated(c, data, pagination)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义响应
|
|
|
|
|
|
h.response.CustomResponse(c, statusCode, data)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 错误处理分层架构
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 1. Handler层 - HTTP错误处理
|
|
|
|
|
|
func (h *UserHandler) Register(c *gin.Context) {
|
|
|
|
|
|
var req dto.RegisterRequest
|
|
|
|
|
|
|
|
|
|
|
|
// 验证请求参数
|
|
|
|
|
|
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
|
|
|
|
return // 验证器已处理响应,直接返回
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用业务服务
|
|
|
|
|
|
user, err := h.userService.Register(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("用户注册失败", zap.Error(err))
|
|
|
|
|
|
|
|
|
|
|
|
// 根据错误类型返回相应响应
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case strings.Contains(err.Error(), "手机号已存在"):
|
|
|
|
|
|
h.response.Conflict(c, "手机号已被注册")
|
|
|
|
|
|
case strings.Contains(err.Error(), "验证码错误"):
|
|
|
|
|
|
h.response.BadRequest(c, "验证码错误或已过期")
|
|
|
|
|
|
default:
|
|
|
|
|
|
h.response.InternalError(c, "注册失败,请稍后重试")
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 成功响应
|
|
|
|
|
|
response := dto.FromEntity(user)
|
|
|
|
|
|
h.response.Created(c, response, "用户注册成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 验证器层 - 参数验证错误
|
|
|
|
|
|
func (v *RequestValidator) BindAndValidate(c *gin.Context, dto interface{}) error {
|
|
|
|
|
|
// 绑定请求体
|
|
|
|
|
|
if err := c.ShouldBindJSON(dto); err != nil {
|
|
|
|
|
|
v.response.BadRequest(c, "请求体格式错误", err.Error())
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证数据
|
|
|
|
|
|
if err := v.validator.Struct(dto); err != nil {
|
|
|
|
|
|
validationErrors := v.formatValidationErrors(err)
|
|
|
|
|
|
v.response.ValidationError(c, validationErrors)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 业务服务层 - 业务逻辑错误
|
|
|
|
|
|
func (s *UserService) Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error) {
|
|
|
|
|
|
// 验证手机号格式
|
|
|
|
|
|
if !s.isValidPhone(req.Phone) {
|
|
|
|
|
|
return nil, fmt.Errorf("手机号格式不正确")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查手机号是否已存在
|
|
|
|
|
|
if err := s.checkPhoneDuplicate(ctx, req.Phone); err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("手机号已存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证验证码
|
|
|
|
|
|
if err := s.smsCodeService.VerifyCode(ctx, req.Phone, req.Code, entities.SMSSceneRegister); err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("验证码错误或已过期")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建用户...
|
|
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🔄 RESTful API 设计规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. DDD 架构下的 URL 设计规范
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 🏢 领域驱动的资源设计
|
|
|
|
|
|
GET /api/v1/users/me # 获取当前用户信息
|
|
|
|
|
|
PUT /api/v1/users/me # 更新当前用户信息
|
|
|
|
|
|
DELETE /api/v1/users/me # 删除当前用户账户
|
|
|
|
|
|
|
|
|
|
|
|
# 🔐 认证相关操作(仍在用户域内)
|
|
|
|
|
|
POST /api/v1/users/register # 用户注册
|
|
|
|
|
|
POST /api/v1/users/login # 用户登录
|
|
|
|
|
|
POST /api/v1/users/logout # 用户登出
|
|
|
|
|
|
POST /api/v1/users/send-code # 发送验证码
|
|
|
|
|
|
|
|
|
|
|
|
# 📱 SMS验证码域操作
|
|
|
|
|
|
POST /api/v1/sms/send # 发送验证码
|
|
|
|
|
|
POST /api/v1/sms/verify # 验证验证码
|
|
|
|
|
|
|
|
|
|
|
|
# 🔗 子资源嵌套(当前用户的资源)
|
|
|
|
|
|
GET /api/v1/users/me/orders # 获取当前用户的订单
|
|
|
|
|
|
GET /api/v1/users/me/favorites # 获取当前用户的收藏
|
|
|
|
|
|
POST /api/v1/users/me/favorites # 添加收藏
|
|
|
|
|
|
DELETE /api/v1/users/me/favorites/:id # 删除收藏
|
|
|
|
|
|
|
|
|
|
|
|
# 🛍️ 跨域资源关系
|
|
|
|
|
|
GET /api/v1/orders/123 # 获取订单详情
|
|
|
|
|
|
GET /api/v1/orders/123/items # 获取订单商品
|
|
|
|
|
|
POST /api/v1/products/456/reviews # 为商品添加评论
|
|
|
|
|
|
|
|
|
|
|
|
# 🎯 特殊操作使用动词(在对应域内)
|
|
|
|
|
|
PUT /api/v1/users/me/password # 修改密码
|
|
|
|
|
|
POST /api/v1/orders/123/cancel # 取消订单
|
|
|
|
|
|
POST /api/v1/payments/123/refund # 退款操作
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. DDD 多域 API 路径设计示例
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 👥 用户域 (User Domain)
|
|
|
|
|
|
POST /api/v1/users/register # 用户注册
|
|
|
|
|
|
POST /api/v1/users/login # 用户登录
|
|
|
|
|
|
GET /api/v1/users/me # 获取当前用户
|
|
|
|
|
|
PUT /api/v1/users/me # 更新用户信息
|
|
|
|
|
|
PUT /api/v1/users/me/password # 修改密码
|
|
|
|
|
|
GET /api/v1/users/me/sessions # 获取登录会话
|
|
|
|
|
|
|
|
|
|
|
|
# 📦 订单域 (Order Domain)
|
|
|
|
|
|
GET /api/v1/orders # 获取订单列表
|
|
|
|
|
|
POST /api/v1/orders # 创建订单
|
|
|
|
|
|
GET /api/v1/orders/:id # 获取订单详情
|
|
|
|
|
|
PUT /api/v1/orders/:id # 更新订单
|
|
|
|
|
|
POST /api/v1/orders/:id/cancel # 取消订单
|
|
|
|
|
|
GET /api/v1/orders/:id/items # 获取订单商品
|
|
|
|
|
|
|
|
|
|
|
|
# 🛍️ 商品域 (Product Domain)
|
|
|
|
|
|
GET /api/v1/products # 获取商品列表
|
|
|
|
|
|
POST /api/v1/products # 创建商品
|
|
|
|
|
|
GET /api/v1/products/:id # 获取商品详情
|
|
|
|
|
|
PUT /api/v1/products/:id # 更新商品
|
|
|
|
|
|
GET /api/v1/products/:id/reviews # 获取商品评论
|
|
|
|
|
|
POST /api/v1/products/:id/reviews # 添加商品评论
|
|
|
|
|
|
|
|
|
|
|
|
# 💰 支付域 (Payment Domain)
|
|
|
|
|
|
POST /api/v1/payments # 创建支付
|
|
|
|
|
|
GET /api/v1/payments/:id # 获取支付状态
|
|
|
|
|
|
POST /api/v1/payments/:id/refund # 申请退款
|
|
|
|
|
|
|
|
|
|
|
|
# 📱 通知域 (Notification Domain)
|
|
|
|
|
|
GET /api/v1/notifications # 获取通知列表
|
|
|
|
|
|
PUT /api/v1/notifications/:id/read # 标记通知为已读
|
|
|
|
|
|
POST /api/v1/sms/send # 发送短信验证码
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. HTTP 状态码规范
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# ✅ 成功响应 (2xx)
|
|
|
|
|
|
200 OK # 查询成功 (GET /api/v1/users/me)
|
|
|
|
|
|
201 Created # 创建成功 (POST /api/v1/users/register)
|
|
|
|
|
|
204 No Content # 删除成功 (DELETE /api/v1/users/me)
|
|
|
|
|
|
|
|
|
|
|
|
# ❌ 客户端错误 (4xx)
|
|
|
|
|
|
400 Bad Request # 请求参数错误
|
|
|
|
|
|
401 Unauthorized # 未认证 (需要登录)
|
|
|
|
|
|
403 Forbidden # 无权限 (登录但权限不足)
|
|
|
|
|
|
404 Not Found # 资源不存在
|
|
|
|
|
|
422 Unprocessable Entity # 业务验证失败
|
|
|
|
|
|
429 Too Many Requests # 请求频率限制
|
|
|
|
|
|
|
|
|
|
|
|
# ⚠️ 服务器错误 (5xx)
|
|
|
|
|
|
500 Internal Server Error # 服务器内部错误
|
|
|
|
|
|
502 Bad Gateway # 网关错误
|
|
|
|
|
|
503 Service Unavailable # 服务不可用
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 状态码在 DDD 架构中的应用
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 用户域状态码示例
|
|
|
|
|
|
func (h *UserHandler) Login(c *gin.Context) {
|
|
|
|
|
|
// 参数验证失败
|
|
|
|
|
|
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
|
|
|
|
// 422 Unprocessable Entity
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
user, err := h.userService.Login(ctx, &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case errors.Is(err, domain.ErrUserNotFound):
|
|
|
|
|
|
h.response.NotFound(c, "用户不存在") // 404
|
|
|
|
|
|
case errors.Is(err, domain.ErrInvalidPassword):
|
|
|
|
|
|
h.response.Unauthorized(c, "密码错误") // 401
|
|
|
|
|
|
case errors.Is(err, domain.ErrUserBlocked):
|
|
|
|
|
|
h.response.Forbidden(c, "账户已被禁用") // 403
|
|
|
|
|
|
default:
|
|
|
|
|
|
h.response.InternalError(c, "登录失败") // 500
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.response.Success(c, user, "登录成功") // 200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *UserHandler) Register(c *gin.Context) {
|
|
|
|
|
|
user, err := h.userService.Register(ctx, &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case errors.Is(err, domain.ErrPhoneExists):
|
|
|
|
|
|
h.response.Conflict(c, "手机号已存在") // 409
|
|
|
|
|
|
case errors.Is(err, domain.ErrInvalidCode):
|
|
|
|
|
|
h.response.BadRequest(c, "验证码错误") // 400
|
|
|
|
|
|
default:
|
|
|
|
|
|
h.response.InternalError(c, "注册失败") // 500
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.response.Created(c, user, "注册成功") // 201
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## ✅ 数据验证规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 结构体标签验证 (中文提示)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 用户注册请求验证
|
|
|
|
|
|
type RegisterRequest struct {
|
|
|
|
|
|
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
|
|
|
|
|
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
|
|
|
|
|
|
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password" example:"password123"`
|
|
|
|
|
|
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户登录请求验证
|
|
|
|
|
|
type LoginWithPasswordRequest struct {
|
|
|
|
|
|
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
|
|
|
|
|
Password string `json:"password" binding:"required" example:"password123"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 修改密码请求验证
|
|
|
|
|
|
type ChangePasswordRequest struct {
|
|
|
|
|
|
OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"`
|
|
|
|
|
|
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
|
|
|
|
|
|
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"newpassword123"`
|
|
|
|
|
|
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 官方中文翻译包集成
|
|
|
|
|
|
|
|
|
|
|
|
项目集成了 `github.com/go-playground/validator/v10/translations/zh` 官方中文翻译包,自动提供专业的中文验证错误消息。
|
|
|
|
|
|
|
|
|
|
|
|
**集成优势:**
|
|
|
|
|
|
|
|
|
|
|
|
- ✅ **官方支持**: 使用 validator 官方维护的中文翻译
|
|
|
|
|
|
- ✅ **专业翻译**: 所有标准验证规则都有准确的中文翻译
|
|
|
|
|
|
- ✅ **自动更新**: 跟随 validator 版本自动获得新功能的中文支持
|
|
|
|
|
|
- ✅ **智能结合**: 官方翻译 + 自定义字段名映射,提供最佳用户体验
|
|
|
|
|
|
- ✅ **兼容性好**: 保持与现有 API 接口的完全兼容
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 创建支持中文翻译的验证器
|
|
|
|
|
|
func NewRequestValidatorZh(response interfaces.ResponseBuilder) interfaces.RequestValidator {
|
|
|
|
|
|
// 创建验证器实例
|
|
|
|
|
|
validate := validator.New()
|
|
|
|
|
|
|
|
|
|
|
|
// 创建中文locale
|
|
|
|
|
|
zhLocale := zh.New()
|
|
|
|
|
|
uni := ut.New(zhLocale, zhLocale)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取中文翻译器
|
|
|
|
|
|
trans, _ := uni.GetTranslator("zh")
|
|
|
|
|
|
|
|
|
|
|
|
// 注册官方中文翻译
|
|
|
|
|
|
zh_translations.RegisterDefaultTranslations(validate, trans)
|
|
|
|
|
|
|
|
|
|
|
|
// 注册自定义验证器和翻译
|
|
|
|
|
|
registerCustomValidatorsZh(validate, trans)
|
|
|
|
|
|
|
|
|
|
|
|
return &RequestValidatorZh{
|
|
|
|
|
|
validator: validate,
|
|
|
|
|
|
translator: trans,
|
|
|
|
|
|
response: response,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 手机号验证器
|
|
|
|
|
|
func validatePhone(fl validator.FieldLevel) bool {
|
|
|
|
|
|
phone := fl.Field().String()
|
|
|
|
|
|
if phone == "" {
|
|
|
|
|
|
return true // 空值由required标签处理
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 中国手机号验证:11位,以1开头
|
|
|
|
|
|
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
|
|
|
|
|
|
return matched
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户名验证器
|
|
|
|
|
|
func validateUsername(fl validator.FieldLevel) bool {
|
|
|
|
|
|
username := fl.Field().String()
|
|
|
|
|
|
if username == "" {
|
|
|
|
|
|
return true // 空值由required标签处理
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户名规则:3-30字符,字母数字下划线,不能数字开头
|
|
|
|
|
|
if len(username) < 3 || len(username) > 30 {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
matched, _ := regexp.MatchString(`^[a-zA-Z][a-zA-Z0-9_]*$`, username)
|
|
|
|
|
|
return matched
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 强密码验证器
|
|
|
|
|
|
func validateStrongPassword(fl validator.FieldLevel) bool {
|
|
|
|
|
|
password := fl.Field().String()
|
|
|
|
|
|
if password == "" {
|
|
|
|
|
|
return true // 空值由required标签处理
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 密码强度:至少8位,包含大小写字母和数字
|
|
|
|
|
|
if len(password) < 8 {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
|
|
|
|
|
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
|
|
|
|
|
|
hasDigit := regexp.MustCompile(`\d`).MatchString(password)
|
|
|
|
|
|
|
|
|
|
|
|
return hasUpper && hasLower && hasDigit
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 自定义验证器和翻译注册
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 注册自定义验证器和中文翻译
|
|
|
|
|
|
func registerCustomValidatorsZh(v *validator.Validate, trans ut.Translator) {
|
|
|
|
|
|
// 注册手机号验证器
|
|
|
|
|
|
v.RegisterValidation("phone", validatePhoneZh)
|
|
|
|
|
|
v.RegisterTranslation("phone", trans, func(ut ut.Translator) error {
|
|
|
|
|
|
return ut.Add("phone", "{0}必须是有效的手机号", true)
|
|
|
|
|
|
}, func(ut ut.Translator, fe validator.FieldError) string {
|
|
|
|
|
|
t, _ := ut.T("phone", fe.Field())
|
|
|
|
|
|
return t
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 注册用户名验证器
|
|
|
|
|
|
v.RegisterValidation("username", validateUsernameZh)
|
|
|
|
|
|
v.RegisterTranslation("username", trans, func(ut ut.Translator) error {
|
|
|
|
|
|
return ut.Add("username", "{0}格式不正确,只能包含字母、数字、下划线,且不能以数字开头", true)
|
|
|
|
|
|
}, func(ut ut.Translator, fe validator.FieldError) string {
|
|
|
|
|
|
t, _ := ut.T("username", fe.Field())
|
|
|
|
|
|
return t
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 注册密码强度验证器
|
|
|
|
|
|
v.RegisterValidation("strong_password", validateStrongPasswordZh)
|
|
|
|
|
|
v.RegisterTranslation("strong_password", trans, func(ut ut.Translator) error {
|
|
|
|
|
|
return ut.Add("strong_password", "{0}强度不足,必须包含大小写字母和数字,且不少于8位", true)
|
|
|
|
|
|
}, func(ut ut.Translator, fe validator.FieldError) string {
|
|
|
|
|
|
t, _ := ut.T("strong_password", fe.Field())
|
|
|
|
|
|
return t
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 智能错误格式化(官方翻译 + 自定义字段名)
|
|
|
|
|
|
func (v *RequestValidatorZh) formatValidationErrorsZh(err error) map[string][]string {
|
|
|
|
|
|
errors := make(map[string][]string)
|
|
|
|
|
|
|
|
|
|
|
|
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
|
|
|
|
|
for _, fieldError := range validationErrors {
|
|
|
|
|
|
fieldName := v.getFieldNameZh(fieldError)
|
|
|
|
|
|
|
|
|
|
|
|
// 使用官方翻译器获取中文错误消息
|
|
|
|
|
|
errorMessage := fieldError.Translate(v.translator)
|
|
|
|
|
|
|
|
|
|
|
|
// 替换字段名为中文显示名称
|
|
|
|
|
|
fieldDisplayName := v.getFieldDisplayName(fieldError.Field())
|
|
|
|
|
|
if fieldDisplayName != fieldError.Field() {
|
|
|
|
|
|
errorMessage = strings.ReplaceAll(errorMessage, fieldError.Field(), fieldDisplayName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if _, exists := errors[fieldName]; !exists {
|
|
|
|
|
|
errors[fieldName] = []string{}
|
|
|
|
|
|
}
|
|
|
|
|
|
errors[fieldName] = append(errors[fieldName], errorMessage)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 中文翻译效果对比
|
|
|
|
|
|
|
|
|
|
|
|
**标准验证规则** (官方翻译)
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "验证失败",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"phone": ["手机号必须是有效的手机号"],
|
|
|
|
|
|
"email": ["email必须是一个有效的邮箱"],
|
|
|
|
|
|
"password": ["password长度必须至少为8个字符"],
|
|
|
|
|
|
"confirm_password": ["ConfirmPassword必须等于Password"],
|
|
|
|
|
|
"age": ["age必须大于或等于18"]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**自定义验证规则** (自定义翻译)
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "验证失败",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"username": [
|
|
|
|
|
|
"用户名格式不正确,只能包含字母、数字、下划线,且不能以数字开头"
|
|
|
|
|
|
],
|
|
|
|
|
|
"password": ["密码强度不足,必须包含大小写字母和数字,且不少于8位"]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**优化后的用户体验**
|
|
|
|
|
|
|
|
|
|
|
|
通过字段名映射,最终用户看到的是:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "验证失败",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"phone": ["手机号必须是有效的手机号"],
|
|
|
|
|
|
"email": ["邮箱必须是一个有效的邮箱"],
|
|
|
|
|
|
"password": ["密码长度必须至少为8个字符"],
|
|
|
|
|
|
"confirm_password": ["确认密码必须等于密码"],
|
|
|
|
|
|
"age": ["年龄必须大于或等于18"]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 验证器使用示例
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func (h *UserHandler) Register(c *gin.Context) {
|
|
|
|
|
|
var req dto.RegisterRequest
|
|
|
|
|
|
|
|
|
|
|
|
// 验证器会自动处理错误响应,返回中文错误信息
|
|
|
|
|
|
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
|
|
|
|
return // 验证失败,已返回带中文提示的错误响应
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 继续业务逻辑...
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证失败时的响应示例
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "验证失败",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"phone": ["手机号 长度必须为 11 位"],
|
|
|
|
|
|
"password": ["密码 长度不能少于 6 位"],
|
|
|
|
|
|
"confirm_password": ["确认密码 必须与 密码 一致"],
|
|
|
|
|
|
"code": ["验证码 长度必须为 6 位"]
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📊 分页和查询规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 分页参数
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type UserListRequest struct {
|
|
|
|
|
|
Page int `form:"page" binding:"min=1"`
|
|
|
|
|
|
PageSize int `form:"page_size" binding:"min=1,max=100"`
|
|
|
|
|
|
Sort string `form:"sort"` // 排序字段
|
|
|
|
|
|
Order string `form:"order"` // asc/desc
|
|
|
|
|
|
Search string `form:"search"` // 搜索关键词
|
|
|
|
|
|
Filters map[string]interface{} `form:"filters"` // 过滤条件
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 查询接口设计
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
GET /api/v1/users?page=1&page_size=20&sort=created_at&order=desc&search=john
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🔧 中间件使用规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 全局中间件(按优先级)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/container/container.go - RegisterMiddlewares
|
|
|
|
|
|
router.RegisterMiddleware(requestID) // 95 - 请求ID
|
|
|
|
|
|
router.RegisterMiddleware(security) // 85 - 安全头部
|
|
|
|
|
|
router.RegisterMiddleware(responseTime) // 75 - 响应时间
|
|
|
|
|
|
router.RegisterMiddleware(cors) // 70 - CORS
|
|
|
|
|
|
router.RegisterMiddleware(rateLimit) // 65 - 限流
|
|
|
|
|
|
router.RegisterMiddleware(requestLogger) // 80 - 请求日志
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 路由级中间件
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 认证中间件
|
|
|
|
|
|
protected.Use(r.jwtAuth.Handle())
|
|
|
|
|
|
|
|
|
|
|
|
// 可选认证中间件
|
|
|
|
|
|
public.Use(r.optionalAuth.Handle())
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义中间件
|
|
|
|
|
|
adminRoutes.Use(r.adminAuth.Handle())
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🎯 错误处理规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 业务错误分类 (中文错误码和消息)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 业务错误结构
|
|
|
|
|
|
type BusinessError struct {
|
|
|
|
|
|
Code string `json:"code"` // 错误码
|
|
|
|
|
|
Message string `json:"message"` // 中文错误消息
|
|
|
|
|
|
Details interface{} `json:"details,omitempty"` // 错误详情
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户域错误码定义
|
|
|
|
|
|
const (
|
|
|
|
|
|
// 用户相关错误
|
|
|
|
|
|
ErrUserNotFound = "USER_NOT_FOUND" // 用户不存在
|
|
|
|
|
|
ErrUserExists = "USER_EXISTS" // 用户已存在
|
|
|
|
|
|
ErrPhoneExists = "PHONE_EXISTS" // 手机号已存在
|
|
|
|
|
|
ErrInvalidCredentials = "INVALID_CREDENTIALS" // 登录凭据无效
|
|
|
|
|
|
ErrInvalidPassword = "INVALID_PASSWORD" // 密码错误
|
|
|
|
|
|
ErrUserBlocked = "USER_BLOCKED" // 用户被禁用
|
|
|
|
|
|
|
|
|
|
|
|
// 验证码相关错误
|
|
|
|
|
|
ErrInvalidCode = "INVALID_CODE" // 验证码错误
|
|
|
|
|
|
ErrCodeExpired = "CODE_EXPIRED" // 验证码已过期
|
|
|
|
|
|
ErrCodeUsed = "CODE_USED" // 验证码已使用
|
|
|
|
|
|
ErrCodeSendTooFrequent = "CODE_SEND_TOO_FREQUENT" // 验证码发送过于频繁
|
|
|
|
|
|
|
|
|
|
|
|
// 请求相关错误
|
|
|
|
|
|
ErrValidationFailed = "VALIDATION_FAILED" // 参数验证失败
|
|
|
|
|
|
ErrInvalidRequest = "INVALID_REQUEST" // 请求格式错误
|
|
|
|
|
|
ErrMissingParam = "MISSING_PARAM" // 缺少必需参数
|
|
|
|
|
|
|
|
|
|
|
|
// 权限相关错误
|
|
|
|
|
|
ErrUnauthorized = "UNAUTHORIZED" // 未认证
|
|
|
|
|
|
ErrForbidden = "FORBIDDEN" // 权限不足
|
|
|
|
|
|
ErrTokenExpired = "TOKEN_EXPIRED" // Token已过期
|
|
|
|
|
|
ErrTokenInvalid = "TOKEN_INVALID" // Token无效
|
|
|
|
|
|
|
|
|
|
|
|
// 系统相关错误
|
|
|
|
|
|
ErrInternalServer = "INTERNAL_SERVER_ERROR" // 服务器内部错误
|
|
|
|
|
|
ErrServiceUnavailable = "SERVICE_UNAVAILABLE" // 服务不可用
|
|
|
|
|
|
ErrRateLimitExceeded = "RATE_LIMIT_EXCEEDED" // 请求频率超限
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 错误消息映射(中文)
|
|
|
|
|
|
var ErrorMessages = map[string]string{
|
|
|
|
|
|
// 用户相关
|
|
|
|
|
|
ErrUserNotFound: "用户不存在",
|
|
|
|
|
|
ErrUserExists: "用户已存在",
|
|
|
|
|
|
ErrPhoneExists: "手机号已被注册",
|
|
|
|
|
|
ErrInvalidCredentials: "用户名或密码错误",
|
|
|
|
|
|
ErrInvalidPassword: "密码错误",
|
|
|
|
|
|
ErrUserBlocked: "账户已被禁用,请联系客服",
|
|
|
|
|
|
|
|
|
|
|
|
// 验证码相关
|
|
|
|
|
|
ErrInvalidCode: "验证码错误",
|
|
|
|
|
|
ErrCodeExpired: "验证码已过期,请重新获取",
|
|
|
|
|
|
ErrCodeUsed: "验证码已使用,请重新获取",
|
|
|
|
|
|
ErrCodeSendTooFrequent: "验证码发送过于频繁,请稍后再试",
|
|
|
|
|
|
|
|
|
|
|
|
// 请求相关
|
|
|
|
|
|
ErrValidationFailed: "请求参数验证失败",
|
|
|
|
|
|
ErrInvalidRequest: "请求格式错误",
|
|
|
|
|
|
ErrMissingParam: "缺少必需参数",
|
|
|
|
|
|
|
|
|
|
|
|
// 权限相关
|
|
|
|
|
|
ErrUnauthorized: "用户未登录或登录已过期",
|
|
|
|
|
|
ErrForbidden: "权限不足,无法访问此资源",
|
|
|
|
|
|
ErrTokenExpired: "登录已过期,请重新登录",
|
|
|
|
|
|
ErrTokenInvalid: "登录信息无效,请重新登录",
|
|
|
|
|
|
|
|
|
|
|
|
// 系统相关
|
|
|
|
|
|
ErrInternalServer: "服务器内部错误,请稍后重试",
|
|
|
|
|
|
ErrServiceUnavailable: "服务暂时不可用,请稍后重试",
|
|
|
|
|
|
ErrRateLimitExceeded: "请求过于频繁,请稍后再试",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建业务错误
|
|
|
|
|
|
func NewBusinessError(code string, details ...interface{}) *BusinessError {
|
|
|
|
|
|
message := ErrorMessages[code]
|
|
|
|
|
|
if message == "" {
|
|
|
|
|
|
message = "未知错误"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := &BusinessError{
|
|
|
|
|
|
Code: code,
|
|
|
|
|
|
Message: message,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(details) > 0 {
|
|
|
|
|
|
err.Details = details[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 实现error接口
|
|
|
|
|
|
func (e *BusinessError) Error() string {
|
|
|
|
|
|
return e.Message
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 错误处理模式示例
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 服务层错误处理
|
|
|
|
|
|
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
|
|
|
|
|
user, err := s.repo.GetByID(ctx, id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return nil, NewBusinessError(ErrUserNotFound)
|
|
|
|
|
|
}
|
|
|
|
|
|
s.logger.Error("获取用户失败", zap.Error(err), zap.String("user_id", id))
|
|
|
|
|
|
return nil, NewBusinessError(ErrInternalServer)
|
|
|
|
|
|
}
|
|
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *UserService) Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error) {
|
|
|
|
|
|
// 检查手机号是否已存在
|
|
|
|
|
|
existingUser, err := s.repo.FindByPhone(ctx, req.Phone)
|
|
|
|
|
|
if err == nil && existingUser != nil {
|
|
|
|
|
|
return nil, NewBusinessError(ErrPhoneExists)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证验证码
|
|
|
|
|
|
if err := s.smsCodeService.VerifyCode(ctx, req.Phone, req.Code, entities.SMSSceneRegister); err != nil {
|
|
|
|
|
|
if strings.Contains(err.Error(), "expired") {
|
|
|
|
|
|
return nil, NewBusinessError(ErrCodeExpired)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, NewBusinessError(ErrInvalidCode)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建用户...
|
|
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handler层错误处理
|
|
|
|
|
|
func (h *UserHandler) GetProfile(c *gin.Context) {
|
|
|
|
|
|
userID := h.getCurrentUserID(c)
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
h.response.Unauthorized(c, ErrorMessages[ErrUnauthorized])
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
user, err := h.userService.GetByID(c.Request.Context(), userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if bizErr, ok := err.(*BusinessError); ok {
|
|
|
|
|
|
switch bizErr.Code {
|
|
|
|
|
|
case ErrUserNotFound:
|
|
|
|
|
|
h.response.NotFound(c, bizErr.Message)
|
|
|
|
|
|
case ErrUnauthorized:
|
|
|
|
|
|
h.response.Unauthorized(c, bizErr.Message)
|
|
|
|
|
|
case ErrForbidden:
|
|
|
|
|
|
h.response.Forbidden(c, bizErr.Message)
|
|
|
|
|
|
default:
|
|
|
|
|
|
h.response.InternalError(c, bizErr.Message)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
h.logger.Error("获取用户信息失败", zap.Error(err))
|
|
|
|
|
|
h.response.InternalError(c, ErrorMessages[ErrInternalServer])
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response := dto.FromEntity(user)
|
|
|
|
|
|
h.response.Success(c, response, "获取用户信息成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 登录错误处理示例
|
|
|
|
|
|
func (h *UserHandler) LoginWithPassword(c *gin.Context) {
|
|
|
|
|
|
var req dto.LoginWithPasswordRequest
|
|
|
|
|
|
|
|
|
|
|
|
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
|
|
|
|
return // 验证器已处理响应
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
user, err := h.userService.LoginWithPassword(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
h.logger.Error("用户登录失败", zap.Error(err), zap.String("phone", req.Phone))
|
|
|
|
|
|
|
|
|
|
|
|
if bizErr, ok := err.(*BusinessError); ok {
|
|
|
|
|
|
switch bizErr.Code {
|
|
|
|
|
|
case ErrUserNotFound:
|
|
|
|
|
|
h.response.NotFound(c, "手机号未注册")
|
|
|
|
|
|
case ErrInvalidPassword:
|
|
|
|
|
|
h.response.Unauthorized(c, "密码错误")
|
|
|
|
|
|
case ErrUserBlocked:
|
|
|
|
|
|
h.response.Forbidden(c, bizErr.Message)
|
|
|
|
|
|
default:
|
|
|
|
|
|
h.response.BadRequest(c, bizErr.Message)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
h.response.InternalError(c, "登录失败,请稍后重试")
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成JWT token...
|
|
|
|
|
|
h.response.Success(c, loginResponse, "登录成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 统一错误响应格式
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 错误响应中间件
|
|
|
|
|
|
func ErrorHandlerMiddleware() gin.HandlerFunc {
|
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
|
c.Next()
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有未处理的错误
|
|
|
|
|
|
if len(c.Errors) > 0 {
|
|
|
|
|
|
err := c.Errors.Last().Err
|
|
|
|
|
|
|
|
|
|
|
|
if bizErr, ok := err.(*BusinessError); ok {
|
|
|
|
|
|
// 业务错误
|
|
|
|
|
|
c.JSON(getHTTPStatus(bizErr.Code), gin.H{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": bizErr.Message,
|
|
|
|
|
|
"error_code": bizErr.Code,
|
|
|
|
|
|
"details": bizErr.Details,
|
|
|
|
|
|
"request_id": c.GetString("request_id"),
|
|
|
|
|
|
"timestamp": time.Now().Unix(),
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 系统错误
|
|
|
|
|
|
c.JSON(500, gin.H{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": ErrorMessages[ErrInternalServer],
|
|
|
|
|
|
"error_code": ErrInternalServer,
|
|
|
|
|
|
"request_id": c.GetString("request_id"),
|
|
|
|
|
|
"timestamp": time.Now().Unix(),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据错误码获取HTTP状态码
|
|
|
|
|
|
func getHTTPStatus(errorCode string) int {
|
|
|
|
|
|
statusMap := map[string]int{
|
|
|
|
|
|
ErrValidationFailed: 400, // Bad Request
|
|
|
|
|
|
ErrInvalidRequest: 400,
|
|
|
|
|
|
ErrMissingParam: 400,
|
|
|
|
|
|
ErrInvalidCode: 400,
|
|
|
|
|
|
ErrPhoneExists: 409, // Conflict
|
|
|
|
|
|
ErrUserExists: 409,
|
|
|
|
|
|
ErrUnauthorized: 401, // Unauthorized
|
|
|
|
|
|
ErrTokenExpired: 401,
|
|
|
|
|
|
ErrTokenInvalid: 401,
|
|
|
|
|
|
ErrInvalidCredentials: 401,
|
|
|
|
|
|
ErrForbidden: 403, // Forbidden
|
|
|
|
|
|
ErrUserBlocked: 403,
|
|
|
|
|
|
ErrUserNotFound: 404, // Not Found
|
|
|
|
|
|
ErrCodeSendTooFrequent: 429, // Too Many Requests
|
|
|
|
|
|
ErrRateLimitExceeded: 429,
|
|
|
|
|
|
ErrInternalServer: 500, // Internal Server Error
|
|
|
|
|
|
ErrServiceUnavailable: 503, // Service Unavailable
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if status, exists := statusMap[errorCode]; exists {
|
|
|
|
|
|
return status
|
|
|
|
|
|
}
|
|
|
|
|
|
return 500 // 默认服务器错误
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📈 日志记录规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 结构化日志 (中文日志消息)
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 成功日志
|
|
|
|
|
|
h.logger.Info("用户注册成功",
|
|
|
|
|
|
zap.String("user_id", user.ID),
|
|
|
|
|
|
zap.String("phone", user.Phone),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
h.logger.Info("用户登录成功",
|
|
|
|
|
|
zap.String("user_id", user.ID),
|
|
|
|
|
|
zap.String("phone", user.Phone),
|
|
|
|
|
|
zap.String("login_method", "password"),
|
|
|
|
|
|
zap.String("ip_address", c.ClientIP()),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
h.logger.Info("验证码发送成功",
|
|
|
|
|
|
zap.String("phone", req.Phone),
|
|
|
|
|
|
zap.String("scene", string(req.Scene)),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
// 错误日志
|
|
|
|
|
|
h.logger.Error("用户注册失败",
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
zap.String("phone", req.Phone),
|
|
|
|
|
|
zap.String("error_type", "business_logic"),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
h.logger.Error("数据库操作失败",
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
zap.String("operation", "create_user"),
|
|
|
|
|
|
zap.String("table", "users"),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
h.logger.Error("外部服务调用失败",
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
zap.String("service", "sms_service"),
|
|
|
|
|
|
zap.String("action", "send_code"),
|
|
|
|
|
|
zap.String("phone", req.Phone),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
// 警告日志
|
|
|
|
|
|
h.logger.Warn("验证码重复发送",
|
|
|
|
|
|
zap.String("phone", req.Phone),
|
|
|
|
|
|
zap.String("scene", string(req.Scene)),
|
|
|
|
|
|
zap.Int("retry_count", retryCount),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
h.logger.Warn("异常登录尝试",
|
|
|
|
|
|
zap.String("phone", req.Phone),
|
|
|
|
|
|
zap.String("ip_address", c.ClientIP()),
|
|
|
|
|
|
zap.String("user_agent", c.GetHeader("User-Agent")),
|
|
|
|
|
|
zap.Int("attempt_count", attemptCount),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
|
|
|
|
|
|
// 调试日志
|
|
|
|
|
|
h.logger.Debug("开始处理用户注册请求",
|
|
|
|
|
|
zap.String("phone", req.Phone),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 日志级别使用规范
|
|
|
|
|
|
|
|
|
|
|
|
- **Debug**: 详细的调试信息(开发环境)
|
|
|
|
|
|
- 请求参数详情
|
|
|
|
|
|
- 中间步骤状态
|
|
|
|
|
|
- 性能指标数据
|
|
|
|
|
|
- **Info**: 重要的业务信息(生产环境)
|
|
|
|
|
|
- 用户操作成功记录
|
|
|
|
|
|
- 系统状态变更
|
|
|
|
|
|
- 业务流程关键节点
|
|
|
|
|
|
- **Warn**: 需要关注但不影响主功能的问题
|
|
|
|
|
|
- 重试操作
|
|
|
|
|
|
- 降级处理
|
|
|
|
|
|
- 资源使用超预期
|
|
|
|
|
|
- **Error**: 影响功能的错误信息
|
|
|
|
|
|
- 业务逻辑错误
|
|
|
|
|
|
- 数据库操作失败
|
|
|
|
|
|
- 外部服务调用失败
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 日志上下文信息规范
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 必需字段
|
|
|
|
|
|
- request_id: 请求追踪ID
|
|
|
|
|
|
- user_id: 用户ID(如果已认证)
|
|
|
|
|
|
- action: 操作类型
|
|
|
|
|
|
- timestamp: 时间戳(自动添加)
|
|
|
|
|
|
|
|
|
|
|
|
// 可选字段
|
|
|
|
|
|
- phone: 手机号(敏感信息需脱敏)
|
|
|
|
|
|
- ip_address: 客户端IP
|
|
|
|
|
|
- user_agent: 用户代理
|
|
|
|
|
|
- error_type: 错误类型分类
|
|
|
|
|
|
- duration: 操作耗时
|
|
|
|
|
|
- service: 服务名称
|
|
|
|
|
|
- method: 请求方法
|
|
|
|
|
|
- path: 请求路径
|
|
|
|
|
|
|
|
|
|
|
|
// 脱敏处理示例
|
|
|
|
|
|
func maskPhone(phone string) string {
|
|
|
|
|
|
if len(phone) != 11 {
|
|
|
|
|
|
return phone
|
|
|
|
|
|
}
|
|
|
|
|
|
return phone[:3] + "****" + phone[7:]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.logger.Info("用户登录成功",
|
|
|
|
|
|
zap.String("phone", maskPhone(user.Phone)), // 138****8000
|
|
|
|
|
|
zap.String("user_id", user.ID),
|
|
|
|
|
|
zap.String("request_id", c.GetString("request_id")))
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🧪 测试规范
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 单元测试
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func TestUserService_Create(t *testing.T) {
|
|
|
|
|
|
// 使用testify进行测试
|
|
|
|
|
|
assert := assert.New(t)
|
|
|
|
|
|
|
|
|
|
|
|
// Mock依赖
|
|
|
|
|
|
mockRepo := &mocks.UserRepository{}
|
|
|
|
|
|
mockEventBus := &mocks.EventBus{}
|
|
|
|
|
|
|
|
|
|
|
|
service := services.NewUserService(mockRepo, mockEventBus, logger)
|
|
|
|
|
|
|
|
|
|
|
|
// 测试用例...
|
|
|
|
|
|
user, err := service.Create(ctx, req)
|
|
|
|
|
|
assert.NoError(err)
|
|
|
|
|
|
assert.NotNil(user)
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 集成测试
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func TestUserHandler_Create(t *testing.T) {
|
|
|
|
|
|
// 设置测试环境
|
|
|
|
|
|
router := setupTestRouter()
|
|
|
|
|
|
|
|
|
|
|
|
// 发送测试请求
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
req, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData))
|
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证响应
|
|
|
|
|
|
assert.Equal(t, 201, w.Code)
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🚀 新增业务领域开发指南
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 创建新领域
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 1. 创建领域目录结构
|
|
|
|
|
|
mkdir -p internal/domains/product/{dto,entities,events,handlers,repositories,routes,services,validators}
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 复制用户领域作为模板
|
|
|
|
|
|
cp -r internal/domains/user/* internal/domains/product/
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 修改包名和结构体名称
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 注册到依赖注入容器
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/container/container.go
|
|
|
|
|
|
fx.Provide(
|
|
|
|
|
|
// Product domain
|
|
|
|
|
|
NewProductRepository,
|
|
|
|
|
|
NewProductService,
|
|
|
|
|
|
NewProductHandler,
|
|
|
|
|
|
NewProductRoutes,
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
fx.Invoke(
|
|
|
|
|
|
RegisterProductRoutes,
|
|
|
|
|
|
),
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 添加路由注册
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func RegisterProductRoutes(
|
|
|
|
|
|
router *http.GinRouter,
|
|
|
|
|
|
productRoutes *routes.ProductRoutes,
|
|
|
|
|
|
) {
|
|
|
|
|
|
productRoutes.RegisterRoutes(router.GetEngine())
|
|
|
|
|
|
productRoutes.RegisterPublicRoutes(router.GetEngine())
|
|
|
|
|
|
productRoutes.RegisterAdminRoutes(router.GetEngine())
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🚀 DDD 新域开发指南
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 创建新业务域
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 1. 创建领域目录结构(以订单域为例)
|
|
|
|
|
|
mkdir -p internal/domains/order/{dto,entities,events,handlers,repositories,routes,services}
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 复制用户域作为模板
|
|
|
|
|
|
cp -r internal/domains/user/* internal/domains/order/
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 批量替换包名和结构体名称
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 定义领域实体和 DTO
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/domains/order/entities/order.go
|
|
|
|
|
|
type Order struct {
|
|
|
|
|
|
ID string `json:"id" gorm:"primaryKey"`
|
|
|
|
|
|
UserID string `json:"user_id" gorm:"not null"`
|
|
|
|
|
|
TotalAmount float64 `json:"total_amount"`
|
|
|
|
|
|
Status Status `json:"status"`
|
|
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// internal/domains/order/dto/order_dto.go
|
|
|
|
|
|
type CreateOrderRequest struct {
|
|
|
|
|
|
Items []OrderItem `json:"items" binding:"required,dive"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type OrderResponse struct {
|
|
|
|
|
|
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
|
|
|
|
|
UserID string `json:"user_id" example:"user-123"`
|
|
|
|
|
|
TotalAmount float64 `json:"total_amount" example:"99.99"`
|
|
|
|
|
|
Status string `json:"status" example:"pending"`
|
|
|
|
|
|
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 配置领域路由
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/domains/order/routes/order_routes.go
|
|
|
|
|
|
func (r *OrderRoutes) RegisterRoutes(router *gin.Engine) {
|
|
|
|
|
|
v1 := router.Group("/api/v1")
|
|
|
|
|
|
|
|
|
|
|
|
// 📦 订单域路由组
|
|
|
|
|
|
orders := v1.Group("/orders")
|
|
|
|
|
|
{
|
|
|
|
|
|
// 公开查询(可选认证)
|
|
|
|
|
|
orders.GET("/:id/public", r.handler.GetPublicOrder)
|
|
|
|
|
|
|
|
|
|
|
|
// 需要认证的路由
|
|
|
|
|
|
authenticated := orders.Group("")
|
|
|
|
|
|
authenticated.Use(r.jwtAuth.Handle())
|
|
|
|
|
|
{
|
|
|
|
|
|
authenticated.GET("", r.handler.List) // GET /api/v1/orders
|
|
|
|
|
|
authenticated.POST("", r.handler.Create) // POST /api/v1/orders
|
|
|
|
|
|
authenticated.GET("/:id", r.handler.GetByID) // GET /api/v1/orders/:id
|
|
|
|
|
|
authenticated.PUT("/:id", r.handler.Update) // PUT /api/v1/orders/:id
|
|
|
|
|
|
authenticated.POST("/:id/cancel", r.handler.Cancel) // POST /api/v1/orders/:id/cancel
|
|
|
|
|
|
authenticated.GET("/:id/items", r.handler.GetItems) // GET /api/v1/orders/:id/items
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 注册到依赖注入容器
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// internal/container/container.go
|
|
|
|
|
|
fx.Provide(
|
|
|
|
|
|
// User domain
|
|
|
|
|
|
repositories.NewUserRepository,
|
|
|
|
|
|
services.NewUserService,
|
|
|
|
|
|
handlers.NewUserHandler,
|
|
|
|
|
|
routes.NewUserRoutes,
|
|
|
|
|
|
|
|
|
|
|
|
// Order domain - 新增
|
|
|
|
|
|
order_repositories.NewOrderRepository,
|
|
|
|
|
|
order_services.NewOrderService,
|
|
|
|
|
|
order_handlers.NewOrderHandler,
|
|
|
|
|
|
order_routes.NewOrderRoutes,
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
fx.Invoke(
|
|
|
|
|
|
RegisterUserRoutes,
|
|
|
|
|
|
RegisterOrderRoutes, // 新增
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// 添加路由注册函数
|
|
|
|
|
|
func RegisterOrderRoutes(
|
|
|
|
|
|
router *http.GinRouter,
|
|
|
|
|
|
orderRoutes *order_routes.OrderRoutes,
|
|
|
|
|
|
) {
|
|
|
|
|
|
orderRoutes.RegisterRoutes(router.GetEngine())
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 跨域关系处理
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 用户订单关系 - 在用户域添加
|
|
|
|
|
|
func (r *UserRoutes) RegisterRoutes(router *gin.Engine) {
|
|
|
|
|
|
users := v1.Group("/users")
|
|
|
|
|
|
authenticated := users.Group("")
|
|
|
|
|
|
authenticated.Use(r.jwtAuth.Handle())
|
|
|
|
|
|
{
|
|
|
|
|
|
authenticated.GET("/me", r.handler.GetProfile)
|
|
|
|
|
|
// 添加用户相关的订单操作
|
|
|
|
|
|
authenticated.GET("/me/orders", r.handler.GetUserOrders) // 获取用户订单
|
|
|
|
|
|
authenticated.GET("/me/orders/stats", r.handler.GetOrderStats) // 订单统计
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 或者在订单域处理用户关系
|
|
|
|
|
|
func (h *OrderHandler) List(c *gin.Context) {
|
|
|
|
|
|
userID := h.getCurrentUserID(c) // 从JWT中获取用户ID
|
|
|
|
|
|
orders, err := h.orderService.GetUserOrders(ctx, userID)
|
|
|
|
|
|
// ... 业务逻辑
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📖 Swagger/OpenAPI 文档集成指南
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 新增接口 Swagger 文档支持
|
|
|
|
|
|
|
|
|
|
|
|
#### 为 Handler 方法添加 Swagger 注释
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// @Summary 接口简短描述(必需)
|
|
|
|
|
|
// @Description 接口详细描述(可选)
|
|
|
|
|
|
// @Tags 标签分组(推荐)
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer # 如果需要JWT认证
|
|
|
|
|
|
// @Param request body dto.RequestStruct true "请求参数描述"
|
|
|
|
|
|
// @Param id path string true "路径参数描述"
|
|
|
|
|
|
// @Param page query int false "查询参数描述"
|
|
|
|
|
|
// @Success 200 {object} dto.ResponseStruct "成功响应描述"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "错误响应描述"
|
|
|
|
|
|
// @Router /api/v1/your-endpoint [post]
|
|
|
|
|
|
func (h *YourHandler) YourMethod(c *gin.Context) {
|
|
|
|
|
|
// Handler实现
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### Swagger 注释语法详解
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 基础注释
|
|
|
|
|
|
// @Summary 接口摘要(在文档列表中显示)
|
|
|
|
|
|
// @Description 详细描述(支持多行)
|
|
|
|
|
|
// @Tags 标签分组(用于在UI中分组显示)
|
|
|
|
|
|
|
|
|
|
|
|
// 请求/响应格式
|
|
|
|
|
|
// @Accept 接受的内容类型:json, xml, plain, html, mpfd, x-www-form-urlencoded
|
|
|
|
|
|
// @Produce 响应的内容类型:json, xml, plain, html
|
|
|
|
|
|
|
|
|
|
|
|
// 安全认证
|
|
|
|
|
|
// @Security Bearer # JWT认证
|
|
|
|
|
|
// @Security ApiKeyAuth # API Key认证
|
|
|
|
|
|
// @Security BasicAuth # 基础认证
|
|
|
|
|
|
|
|
|
|
|
|
// 参数定义
|
|
|
|
|
|
// @Param name location type required "description" Enums(A,B,C) default(A)
|
|
|
|
|
|
// location: query, path, header, body, formData
|
|
|
|
|
|
// type: string, number, integer, boolean, array, object
|
|
|
|
|
|
// required: true, false
|
|
|
|
|
|
|
|
|
|
|
|
// 响应定义
|
|
|
|
|
|
// @Success code {type} model "description"
|
|
|
|
|
|
// @Failure code {type} model "description"
|
|
|
|
|
|
// code: HTTP状态码
|
|
|
|
|
|
// type: object, array, string, number, boolean
|
|
|
|
|
|
// model: 响应模型(如dto.UserResponse)
|
|
|
|
|
|
|
|
|
|
|
|
// 路由定义
|
|
|
|
|
|
// @Router path [method]
|
|
|
|
|
|
// method: get, post, put, delete, patch, head, options
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 完整示例:订单域接口文档
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// CreateOrder 创建订单
|
|
|
|
|
|
// @Summary 创建新订单
|
|
|
|
|
|
// @Description 根据购物车内容创建新的订单,支持多商品下单
|
|
|
|
|
|
// @Tags 订单管理
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
// @Param request body dto.CreateOrderRequest true "创建订单请求"
|
|
|
|
|
|
// @Success 201 {object} dto.OrderResponse "订单创建成功"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
|
|
|
|
|
// @Failure 422 {object} map[string]interface{} "业务验证失败"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/orders [post]
|
|
|
|
|
|
func (h *OrderHandler) CreateOrder(c *gin.Context) {
|
|
|
|
|
|
// 实现代码
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetOrderList 获取订单列表
|
|
|
|
|
|
// @Summary 获取当前用户的订单列表
|
|
|
|
|
|
// @Description 分页获取当前用户的订单列表,支持按状态筛选和关键词搜索
|
|
|
|
|
|
// @Tags 订单管理
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
// @Param page query int false "页码" default(1) minimum(1)
|
|
|
|
|
|
// @Param page_size query int false "每页数量" default(20) minimum(1) maximum(100)
|
|
|
|
|
|
// @Param status query string false "订单状态" Enums(pending,paid,shipped,delivered,cancelled)
|
|
|
|
|
|
// @Param search query string false "搜索关键词"
|
|
|
|
|
|
// @Success 200 {object} dto.OrderListResponse "订单列表"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/orders [get]
|
|
|
|
|
|
func (h *OrderHandler) GetOrderList(c *gin.Context) {
|
|
|
|
|
|
// 实现代码
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateOrder 更新订单
|
|
|
|
|
|
// @Summary 更新订单信息
|
|
|
|
|
|
// @Description 更新指定订单的部分信息,如收货地址、备注等
|
|
|
|
|
|
// @Tags 订单管理
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
// @Param id path string true "订单ID" Format(uuid)
|
|
|
|
|
|
// @Param request body dto.UpdateOrderRequest true "更新订单请求"
|
|
|
|
|
|
// @Success 200 {object} dto.OrderResponse "订单更新成功"
|
|
|
|
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|
|
|
|
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
|
|
|
|
|
// @Failure 403 {object} map[string]interface{} "无权限操作此订单"
|
|
|
|
|
|
// @Failure 404 {object} map[string]interface{} "订单不存在"
|
|
|
|
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|
|
|
|
|
// @Router /api/v1/orders/{id} [put]
|
|
|
|
|
|
func (h *OrderHandler) UpdateOrder(c *gin.Context) {
|
|
|
|
|
|
// 实现代码
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. DTO 结构体文档化
|
|
|
|
|
|
|
|
|
|
|
|
#### 为请求/响应结构体添加文档标签
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// CreateOrderRequest 创建订单请求
|
|
|
|
|
|
type CreateOrderRequest struct {
|
|
|
|
|
|
Items []OrderItem `json:"items" binding:"required,dive" example:"[{\"product_id\":\"123\",\"quantity\":2}]"`
|
|
|
|
|
|
DeliveryAddress string `json:"delivery_address" binding:"required,max=200" example:"北京市朝阳区xxx街道xxx号"`
|
|
|
|
|
|
PaymentMethod string `json:"payment_method" binding:"required,oneof=alipay wechat" example:"alipay"`
|
|
|
|
|
|
Remark string `json:"remark" binding:"max=500" example:"请尽快发货"`
|
|
|
|
|
|
} // @name CreateOrderRequest
|
|
|
|
|
|
|
|
|
|
|
|
// OrderResponse 订单响应
|
|
|
|
|
|
type OrderResponse struct {
|
|
|
|
|
|
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
|
|
|
|
|
UserID string `json:"user_id" example:"user-123"`
|
|
|
|
|
|
OrderNo string `json:"order_no" example:"ORD20240101001"`
|
|
|
|
|
|
Status OrderStatus `json:"status" example:"pending"`
|
|
|
|
|
|
TotalAmount float64 `json:"total_amount" example:"299.99"`
|
|
|
|
|
|
PaymentMethod string `json:"payment_method" example:"alipay"`
|
|
|
|
|
|
DeliveryAddress string `json:"delivery_address" example:"北京市朝阳区xxx街道xxx号"`
|
|
|
|
|
|
Items []OrderItem `json:"items"`
|
|
|
|
|
|
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
|
|
|
|
|
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
|
|
|
|
|
} // @name OrderResponse
|
|
|
|
|
|
|
|
|
|
|
|
// OrderItem 订单商品项
|
|
|
|
|
|
type OrderItem struct {
|
|
|
|
|
|
ProductID string `json:"product_id" example:"prod-123"`
|
|
|
|
|
|
ProductName string `json:"product_name" example:"iPhone 15 Pro"`
|
|
|
|
|
|
Quantity int `json:"quantity" example:"1"`
|
|
|
|
|
|
Price float64 `json:"price" example:"999.99"`
|
|
|
|
|
|
Subtotal float64 `json:"subtotal" example:"999.99"`
|
|
|
|
|
|
} // @name OrderItem
|
|
|
|
|
|
|
|
|
|
|
|
// OrderListResponse 订单列表响应
|
|
|
|
|
|
type OrderListResponse struct {
|
|
|
|
|
|
Orders []OrderResponse `json:"orders"`
|
|
|
|
|
|
Pagination Pagination `json:"pagination"`
|
|
|
|
|
|
} // @name OrderListResponse
|
|
|
|
|
|
|
|
|
|
|
|
// Pagination 分页信息
|
|
|
|
|
|
type Pagination struct {
|
|
|
|
|
|
Page int `json:"page" example:"1"`
|
|
|
|
|
|
PageSize int `json:"page_size" example:"20"`
|
|
|
|
|
|
Total int `json:"total" example:"150"`
|
|
|
|
|
|
TotalPages int `json:"total_pages" example:"8"`
|
|
|
|
|
|
} // @name Pagination
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 枚举类型文档化
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// OrderStatus 订单状态
|
|
|
|
|
|
type OrderStatus string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
OrderStatusPending OrderStatus = "pending" // 待支付
|
|
|
|
|
|
OrderStatusPaid OrderStatus = "paid" // 已支付
|
|
|
|
|
|
OrderStatusShipped OrderStatus = "shipped" // 已发货
|
|
|
|
|
|
OrderStatusDelivered OrderStatus = "delivered" // 已送达
|
|
|
|
|
|
OrderStatusCancelled OrderStatus = "cancelled" // 已取消
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 为枚举添加Swagger文档
|
|
|
|
|
|
// @Description 订单状态
|
|
|
|
|
|
// @Enum pending,paid,shipped,delivered,cancelled
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 文档生成和更新流程
|
|
|
|
|
|
|
|
|
|
|
|
#### 标准工作流程
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 1. 编写/修改Handler方法,添加Swagger注释
|
|
|
|
|
|
vim internal/domains/order/handlers/order_handler.go
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 编写/修改DTO结构体,添加example标签
|
|
|
|
|
|
vim internal/domains/order/dto/order_dto.go
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 重新生成Swagger文档
|
|
|
|
|
|
make docs
|
|
|
|
|
|
# 或直接使用命令
|
|
|
|
|
|
swag init -g cmd/api/main.go -o docs/swagger
|
|
|
|
|
|
|
|
|
|
|
|
# 4. 重启项目
|
|
|
|
|
|
go run cmd/api/main.go
|
|
|
|
|
|
|
|
|
|
|
|
# 5. 访问文档查看效果
|
|
|
|
|
|
open http://localhost:8080/swagger/index.html
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 快速开发脚本
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 创建docs脚本:scripts/update-docs.sh
|
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
echo "🔄 Updating Swagger documentation..."
|
|
|
|
|
|
|
|
|
|
|
|
# 生成文档
|
|
|
|
|
|
make docs
|
|
|
|
|
|
|
|
|
|
|
|
if [ $? -eq 0 ]; then
|
|
|
|
|
|
echo "✅ Swagger documentation updated successfully!"
|
|
|
|
|
|
echo "📖 View at: http://localhost:8080/swagger/index.html"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "❌ Failed to update documentation"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# 重启开发服务器(可选)
|
|
|
|
|
|
if [ "$1" = "--restart" ]; then
|
|
|
|
|
|
echo "🔄 Restarting development server..."
|
|
|
|
|
|
pkill -f "go run cmd/api/main.go"
|
|
|
|
|
|
nohup go run cmd/api/main.go > /dev/null 2>&1 &
|
|
|
|
|
|
echo "🚀 Development server restarted!"
|
|
|
|
|
|
fi
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 文档质量检查清单
|
|
|
|
|
|
|
|
|
|
|
|
#### 必需元素检查
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] **@Summary**: 简洁明了的接口描述
|
|
|
|
|
|
- [ ] **@Description**: 详细的功能说明
|
|
|
|
|
|
- [ ] **@Tags**: 正确的分组标签
|
|
|
|
|
|
- [ ] **@Router**: 正确的路径和 HTTP 方法
|
|
|
|
|
|
- [ ] **@Accept/@Produce**: 正确的内容类型
|
|
|
|
|
|
- [ ] **@Security**: 认证要求(如需要)
|
|
|
|
|
|
|
|
|
|
|
|
#### 参数文档检查
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] **路径参数**: 所有{id}等路径参数都有@Param
|
|
|
|
|
|
- [ ] **查询参数**: 分页、筛选等参数都有@Param
|
|
|
|
|
|
- [ ] **请求体**: 复杂请求有@Param body 定义
|
|
|
|
|
|
- [ ] **示例值**: 所有参数都有 realistic 的 example
|
|
|
|
|
|
|
|
|
|
|
|
#### 响应文档检查
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] **成功响应**: @Success 定义了正确的状态码和模型
|
|
|
|
|
|
- [ ] **错误响应**: @Failure 覆盖了主要的错误场景
|
|
|
|
|
|
- [ ] **响应模型**: DTO 结构体有完整的 json 标签和 example
|
|
|
|
|
|
- [ ] **状态码**: 符合 RESTful 规范
|
|
|
|
|
|
|
|
|
|
|
|
### 6. 高级文档特性
|
|
|
|
|
|
|
|
|
|
|
|
#### 自定义响应模型
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 为复杂响应创建专门的文档模型
|
|
|
|
|
|
type APIResponse struct {
|
|
|
|
|
|
Success bool `json:"success" example:"true"`
|
|
|
|
|
|
Data interface{} `json:"data"`
|
|
|
|
|
|
Message string `json:"message" example:"操作成功"`
|
|
|
|
|
|
RequestID string `json:"request_id" example:"req-123"`
|
|
|
|
|
|
Timestamp int64 `json:"timestamp" example:"1640995200"`
|
|
|
|
|
|
} // @name APIResponse
|
|
|
|
|
|
|
|
|
|
|
|
// 在Handler中使用
|
|
|
|
|
|
// @Success 200 {object} APIResponse{data=dto.OrderResponse} "成功响应"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 分组和版本管理
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 使用一致的标签分组
|
|
|
|
|
|
// @Tags 用户认证 # 认证相关接口
|
|
|
|
|
|
// @Tags 用户管理 # 用户CRUD接口
|
|
|
|
|
|
// @Tags 订单管理 # 订单相关接口
|
|
|
|
|
|
// @Tags 商品管理 # 商品相关接口
|
|
|
|
|
|
// @Tags 系统管理 # 系统功能接口
|
|
|
|
|
|
|
|
|
|
|
|
// 版本控制
|
|
|
|
|
|
// @Router /api/v1/users [post] # V1版本
|
|
|
|
|
|
// @Router /api/v2/users [post] # V2版本(向后兼容)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 7. 常见问题和解决方案
|
|
|
|
|
|
|
|
|
|
|
|
#### 问题 1:文档生成失败
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 检查Swagger注释语法
|
|
|
|
|
|
swag init -g cmd/api/main.go -o docs/swagger --parseDependency
|
|
|
|
|
|
|
|
|
|
|
|
# 常见错误:
|
|
|
|
|
|
# - 缺少@Router注释
|
|
|
|
|
|
# - HTTP方法写错(必须小写)
|
|
|
|
|
|
# - 路径格式不正确
|
|
|
|
|
|
# - 缺少必需的包导入
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 问题 2:模型没有正确显示
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 确保结构体有正确的标签
|
|
|
|
|
|
type UserRequest struct {
|
|
|
|
|
|
Name string `json:"name" example:"张三"` # json标签必需
|
|
|
|
|
|
} // @name UserRequest # 显式命名(可选)
|
|
|
|
|
|
|
|
|
|
|
|
# 确保包被正确解析
|
|
|
|
|
|
swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 问题 3:认证测试失败
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 确保安全定义正确
|
|
|
|
|
|
// @securityDefinitions.apikey Bearer
|
|
|
|
|
|
// @in header
|
|
|
|
|
|
// @name Authorization
|
|
|
|
|
|
// @description Type "Bearer" followed by a space and JWT token.
|
|
|
|
|
|
|
|
|
|
|
|
// 在接口中正确使用
|
|
|
|
|
|
// @Security Bearer
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 8. 持续集成中的文档检查
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# CI脚本示例:.github/workflows/docs.yml
|
|
|
|
|
|
name: API Documentation Check
|
|
|
|
|
|
|
|
|
|
|
|
on: [push, pull_request]
|
|
|
|
|
|
|
|
|
|
|
|
jobs:
|
|
|
|
|
|
docs-check:
|
|
|
|
|
|
runs-on: ubuntu-latest
|
|
|
|
|
|
steps:
|
|
|
|
|
|
- uses: actions/checkout@v2
|
|
|
|
|
|
|
|
|
|
|
|
- name: Setup Go
|
|
|
|
|
|
uses: actions/setup-go@v2
|
|
|
|
|
|
with:
|
|
|
|
|
|
go-version: 1.23
|
|
|
|
|
|
|
|
|
|
|
|
- name: Install swag
|
|
|
|
|
|
run: go install github.com/swaggo/swag/cmd/swag@latest
|
|
|
|
|
|
|
|
|
|
|
|
- name: Generate docs
|
|
|
|
|
|
run: make docs
|
|
|
|
|
|
|
|
|
|
|
|
- name: Check docs are up to date
|
|
|
|
|
|
run: |
|
|
|
|
|
|
if [[ `git status --porcelain docs/` ]]; then
|
|
|
|
|
|
echo "Documentation is out of date. Please run 'make docs'"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📚 最佳实践总结
|
|
|
|
|
|
|
|
|
|
|
|
### 🏗️ 架构设计原则
|
|
|
|
|
|
|
|
|
|
|
|
1. **领域驱动设计**: 按业务域组织代码和 API 路径,避免技术导向设计
|
|
|
|
|
|
2. **单一职责原则**: 每个层只负责自己的职责,保持清晰的边界分离
|
|
|
|
|
|
3. **依赖注入管理**: 使用 Uber FX 进行依赖管理,支持模块化扩展
|
|
|
|
|
|
4. **接口隔离原则**: 定义清晰的接口边界,便于测试和扩展
|
|
|
|
|
|
|
|
|
|
|
|
### 📋 API 设计规范
|
|
|
|
|
|
|
|
|
|
|
|
5. **统一响应格式**: 标准化的 API 响应结构和中文错误提示
|
|
|
|
|
|
6. **RESTful 路径设计**: 语义化路径清晰表达业务意图
|
|
|
|
|
|
7. **多层数据验证**: 从 DTO 到业务规则的完整验证链
|
|
|
|
|
|
8. **中文化用户体验**: 所有面向用户的消息都使用中文
|
|
|
|
|
|
|
|
|
|
|
|
### 🔧 技术实现规范
|
|
|
|
|
|
|
|
|
|
|
|
9. **结构化日志记录**: 使用 Zap 记录中文结构化日志,便于监控和调试
|
|
|
|
|
|
10. **智能缓存策略**: 合理使用 Redis 缓存提升系统性能
|
|
|
|
|
|
11. **事件驱动架构**: 使用领域事件解耦业务逻辑,支持异步处理
|
|
|
|
|
|
12. **错误处理分层**: 统一的业务错误码和 HTTP 状态码映射
|
|
|
|
|
|
|
|
|
|
|
|
### 📖 开发协作规范
|
|
|
|
|
|
|
|
|
|
|
|
13. **文档优先开发**: 编写接口时同步维护 Swagger 文档,确保文档和代码一致性
|
|
|
|
|
|
14. **完整测试覆盖**: 单元测试、集成测试和端到端测试
|
|
|
|
|
|
15. **代码审查机制**: 确保代码质量和规范一致性
|
|
|
|
|
|
16. **持续集成部署**: 自动化构建、测试和部署流程
|
|
|
|
|
|
|
|
|
|
|
|
### 🚀 性能和扩展性
|
|
|
|
|
|
|
|
|
|
|
|
17. **数据库事务管理**: 合理使用数据库事务确保数据一致性
|
|
|
|
|
|
18. **请求限流保护**: 防止恶意请求和系统过载
|
|
|
|
|
|
19. **监控和告警**: 完整的应用性能监控和业务指标收集
|
|
|
|
|
|
20. **水平扩展支持**: 微服务架构支持横向扩展
|
|
|
|
|
|
|
|
|
|
|
|
## 🔄 配置管理
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 环境配置
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
# config.yaml (开发环境)
|
|
|
|
|
|
server:
|
|
|
|
|
|
port: "8080"
|
|
|
|
|
|
mode: "debug"
|
|
|
|
|
|
|
|
|
|
|
|
# config.prod.yaml (生产环境)
|
|
|
|
|
|
server:
|
|
|
|
|
|
port: "8080"
|
|
|
|
|
|
mode: "release"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 环境变量覆盖
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 优先级: 环境变量 > 配置文件 > 默认值
|
|
|
|
|
|
export ENV=production
|
|
|
|
|
|
export DB_HOST=prod-database
|
|
|
|
|
|
export JWT_SECRET=secure-jwt-secret
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📋 当前项目 API 接口清单
|
|
|
|
|
|
|
|
|
|
|
|
### 👥 用户域 (User Domain)
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 🌍 公开接口(无需认证)
|
|
|
|
|
|
POST /api/v1/users/send-code # 发送验证码
|
|
|
|
|
|
POST /api/v1/users/register # 用户注册
|
|
|
|
|
|
POST /api/v1/users/login # 用户登录
|
|
|
|
|
|
|
|
|
|
|
|
# 🔐 认证接口(需要JWT Token)
|
|
|
|
|
|
GET /api/v1/users/me # 获取当前用户信息
|
|
|
|
|
|
PUT /api/v1/users/me/password # 修改密码
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 📱 SMS 验证码域
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 🌍 公开接口
|
|
|
|
|
|
POST /api/v1/sms/send # 发送验证码(与users/send-code相同)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 🔧 系统接口
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 🌍 健康检查
|
|
|
|
|
|
GET /health # 系统健康状态
|
|
|
|
|
|
GET /health/detailed # 详细健康状态
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 📊 请求示例
|
|
|
|
|
|
|
|
|
|
|
|
#### 发送验证码
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X POST http://localhost:8080/api/v1/users/send-code \
|
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
|
-d '{
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"scene": "register"
|
|
|
|
|
|
}'
|
|
|
|
|
|
|
|
|
|
|
|
# 响应示例
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "验证码发送成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"message": "验证码已发送到您的手机",
|
|
|
|
|
|
"expires_at": "2024-01-01T00:05:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 用户注册
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X POST http://localhost:8080/api/v1/users/register \
|
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
|
-d '{
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"password": "password123",
|
|
|
|
|
|
"confirm_password": "password123",
|
|
|
|
|
|
"code": "123456"
|
|
|
|
|
|
}'
|
|
|
|
|
|
|
|
|
|
|
|
# 响应示例
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "用户注册成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 密码登录
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X POST http://localhost:8080/api/v1/users/login-password \
|
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
|
-d '{
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"password": "password123"
|
|
|
|
|
|
}'
|
|
|
|
|
|
|
|
|
|
|
|
# 响应示例
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "登录成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"user": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
|
|
|
|
"token_type": "Bearer",
|
|
|
|
|
|
"expires_in": 86400,
|
|
|
|
|
|
"login_method": "password"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 短信验证码登录
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X POST http://localhost:8080/api/v1/users/login-sms \
|
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
|
-d '{
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"code": "123456"
|
|
|
|
|
|
}'
|
|
|
|
|
|
|
|
|
|
|
|
# 响应示例同密码登录,login_method为"sms"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 获取当前用户信息
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X GET http://localhost:8080/api/v1/users/me \
|
|
|
|
|
|
-H "Authorization: Bearer <your-jwt-token>"
|
|
|
|
|
|
|
|
|
|
|
|
# 响应示例
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "获取用户信息成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 修改密码
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X PUT http://localhost:8080/api/v1/users/me/password \
|
|
|
|
|
|
-H "Authorization: Bearer <your-jwt-token>" \
|
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
|
-d '{
|
|
|
|
|
|
"old_password": "oldpassword123",
|
|
|
|
|
|
"new_password": "newpassword123",
|
|
|
|
|
|
"confirm_new_password": "newpassword123",
|
|
|
|
|
|
"code": "123456"
|
|
|
|
|
|
}'
|
|
|
|
|
|
|
|
|
|
|
|
# 响应示例
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "密码修改成功",
|
|
|
|
|
|
"data": null,
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 错误响应示例
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 参数验证失败
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "请求参数验证失败",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"phone": ["手机号 长度必须为 11 位"],
|
|
|
|
|
|
"password": ["密码 长度不能少于 6 位"]
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 业务逻辑错误
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "手机号已被注册",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 认证失败
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "用户未登录或登录已过期",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 🔄 响应格式示例
|
|
|
|
|
|
|
|
|
|
|
|
#### 成功响应
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
// 用户注册成功
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "用户注册成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户登录成功
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "登录成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"user": {
|
|
|
|
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
|
|
|
|
"phone": "13800138000",
|
|
|
|
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
|
|
|
|
"updated_at": "2024-01-01T00:00:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
|
|
|
|
"token_type": "Bearer",
|
|
|
|
|
|
"expires_in": 86400,
|
|
|
|
|
|
"login_method": "password"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送验证码成功
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "验证码发送成功",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"message": "验证码已发送",
|
|
|
|
|
|
"expires_at": "2024-01-01T00:05:00Z"
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 错误响应
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
// 参数验证失败
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "请求参数验证失败",
|
|
|
|
|
|
"errors": {
|
|
|
|
|
|
"phone": ["手机号 长度必须为 11 位"],
|
|
|
|
|
|
"password": ["密码 长度不能少于 6 位"],
|
|
|
|
|
|
"code": ["验证码 长度必须为 6 位"]
|
|
|
|
|
|
},
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 业务逻辑错误
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "手机号已被注册",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 认证失败
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "用户未登录或登录已过期",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证码错误
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "验证码错误或已过期",
|
|
|
|
|
|
"request_id": "req_123456789",
|
|
|
|
|
|
"timestamp": 1704067200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
遵循以上规范,可以确保 API 开发的一致性、可维护性和扩展性。
|
2025-07-11 21:05:58 +08:00
|
|
|
|
|
|
|
|
|
|
# TYAPI Server 企业级高级特性完整集成指南
|
|
|
|
|
|
|
|
|
|
|
|
## 🚀 **高级特性完整解决方案实施完成**
|
|
|
|
|
|
|
|
|
|
|
|
本项目现已成功集成所有企业级高级特性,提供完整的可观测性、弹性恢复和分布式事务能力。所有组件均已通过编译验证和容器集成。
|
|
|
|
|
|
|
|
|
|
|
|
## 📊 **已完整集成的高级特性**
|
|
|
|
|
|
|
|
|
|
|
|
### 1. **🔍 分布式链路追踪 (Distributed Tracing)**
|
|
|
|
|
|
|
|
|
|
|
|
**技术栈**: OpenTelemetry + OTLP 导出器
|
|
|
|
|
|
**支持后端**: Jaeger、Zipkin、Tempo、任何 OTLP 兼容系统
|
|
|
|
|
|
**状态**: ✅ **完全集成**
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
# 配置示例 (config.yaml)
|
|
|
|
|
|
monitoring:
|
|
|
|
|
|
tracing_enabled: true
|
|
|
|
|
|
tracing_endpoint: "http://localhost:4317" # OTLP gRPC endpoint
|
|
|
|
|
|
sample_rate: 0.1
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**核心特性**:
|
|
|
|
|
|
|
|
|
|
|
|
- ✅ HTTP 请求自动追踪中间件
|
|
|
|
|
|
- ✅ 数据库操作追踪
|
|
|
|
|
|
- ✅ 缓存操作追踪
|
|
|
|
|
|
- ✅ 自定义业务操作追踪
|
|
|
|
|
|
- ✅ TraceID/SpanID 自动传播
|
|
|
|
|
|
- ✅ 生产级批处理导出
|
|
|
|
|
|
- ✅ 容器生命周期管理
|
|
|
|
|
|
|
|
|
|
|
|
**使用示例**:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 自动HTTP追踪(已在所有路由启用)
|
|
|
|
|
|
// 每个HTTP请求都会创建完整的追踪链路
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义业务操作追踪
|
|
|
|
|
|
ctx, span := tracer.StartSpan(ctx, "business.user_registration")
|
|
|
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
|
|
|
|
// 数据库操作追踪
|
|
|
|
|
|
ctx, span := tracer.StartDBSpan(ctx, "SELECT", "users", "WHERE phone = ?")
|
|
|
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
|
|
|
|
// 缓存操作追踪
|
|
|
|
|
|
ctx, span := tracer.StartCacheSpan(ctx, "GET", "user:cache:123")
|
|
|
|
|
|
defer span.End()
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. **📈 指标监控 (Metrics Collection)**
|
|
|
|
|
|
|
|
|
|
|
|
**技术栈**: Prometheus + 自定义业务指标
|
|
|
|
|
|
**导出端点**: `/metrics` (Prometheus 格式)
|
|
|
|
|
|
**状态**: ✅ **完全集成**
|
|
|
|
|
|
|
|
|
|
|
|
**自动收集指标**:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
# HTTP请求指标
|
|
|
|
|
|
http_requests_total{method="GET",path="/api/v1/users",status="200"} 1523
|
|
|
|
|
|
http_request_duration_seconds{method="GET",path="/api/v1/users"} 0.045
|
|
|
|
|
|
|
|
|
|
|
|
# 业务指标
|
|
|
|
|
|
business_user_created_total{source="register"} 245
|
|
|
|
|
|
business_user_login_total{platform="web",status="success"} 1892
|
|
|
|
|
|
business_sms_sent_total{type="verification",provider="aliyun"} 456
|
|
|
|
|
|
|
|
|
|
|
|
# 系统指标
|
|
|
|
|
|
active_users_total 1024
|
|
|
|
|
|
database_connections_active 12
|
|
|
|
|
|
cache_operations_total{operation="get",result="hit"} 8745
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**自定义指标注册**:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 注册自定义计数器
|
|
|
|
|
|
metrics.RegisterCounter("custom_events_total", "Custom events counter", []string{"event_type", "source"})
|
|
|
|
|
|
|
|
|
|
|
|
// 记录指标
|
|
|
|
|
|
metrics.IncrementCounter("custom_events_total", map[string]string{
|
|
|
|
|
|
"event_type": "user_action",
|
|
|
|
|
|
"source": "web",
|
|
|
|
|
|
})
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. **🛡️ 弹性恢复 (Resilience)**
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.1 **熔断器 (Circuit Breaker)**
|
|
|
|
|
|
|
|
|
|
|
|
**状态**: ✅ **完全集成**
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 使用熔断器保护服务调用
|
|
|
|
|
|
err := circuitBreaker.Execute("user-service", func() error {
|
|
|
|
|
|
return userService.GetUserByID(ctx, userID)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 批量执行保护
|
|
|
|
|
|
err := circuitBreaker.ExecuteBatch("batch-operation", []func() error{
|
|
|
|
|
|
func() error { return service1.Call() },
|
|
|
|
|
|
func() error { return service2.Call() },
|
|
|
|
|
|
})
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**特性**:
|
|
|
|
|
|
|
|
|
|
|
|
- ✅ 故障阈值自动检测
|
|
|
|
|
|
- ✅ 半开状态自动恢复
|
|
|
|
|
|
- ✅ 实时状态监控
|
|
|
|
|
|
- ✅ 多种失败策略
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.2 **重试机制 (Retry)**
|
|
|
|
|
|
|
|
|
|
|
|
**状态**: ✅ **完全集成**
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 快速重试(适用于网络抖动)
|
|
|
|
|
|
err := retryer.ExecuteWithQuickRetry(ctx, "api-call", func() error {
|
|
|
|
|
|
return httpClient.Call()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 标准重试(适用于业务操作)
|
|
|
|
|
|
err := retryer.ExecuteWithStandardRetry(ctx, "db-operation", func() error {
|
|
|
|
|
|
return db.Save(data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 耐心重试(适用于最终一致性)
|
|
|
|
|
|
err := retryer.ExecuteWithPatientRetry(ctx, "sync-operation", func() error {
|
|
|
|
|
|
return syncService.Sync()
|
|
|
|
|
|
})
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. **🔄 分布式事务 (Saga Pattern)**
|
|
|
|
|
|
|
|
|
|
|
|
**状态**: ✅ **完全集成**
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 创建分布式事务
|
|
|
|
|
|
saga := sagaManager.CreateSaga("user-registration-001", "用户注册流程")
|
|
|
|
|
|
|
|
|
|
|
|
// 添加事务步骤
|
|
|
|
|
|
saga.AddStep("create-user",
|
|
|
|
|
|
// 正向操作
|
|
|
|
|
|
func(ctx context.Context, data interface{}) error {
|
|
|
|
|
|
return userService.CreateUser(ctx, data)
|
|
|
|
|
|
},
|
|
|
|
|
|
// 补偿操作
|
|
|
|
|
|
func(ctx context.Context, data interface{}) error {
|
|
|
|
|
|
return userService.DeleteUser(ctx, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
saga.AddStep("send-welcome-email",
|
|
|
|
|
|
func(ctx context.Context, data interface{}) error {
|
|
|
|
|
|
return emailService.SendWelcome(ctx, data)
|
|
|
|
|
|
},
|
|
|
|
|
|
func(ctx context.Context, data interface{}) error {
|
|
|
|
|
|
return emailService.SendCancellation(ctx, data)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 执行事务
|
|
|
|
|
|
err := saga.Execute(ctx, userData)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**支持特性**:
|
|
|
|
|
|
|
|
|
|
|
|
- ✅ 自动补偿机制
|
|
|
|
|
|
- ✅ 步骤重试策略
|
|
|
|
|
|
- ✅ 事务状态跟踪
|
|
|
|
|
|
- ✅ 并发控制
|
|
|
|
|
|
|
|
|
|
|
|
### 5. **🪝 事件钩子系统 (Hook System)**
|
|
|
|
|
|
|
|
|
|
|
|
**状态**: ✅ **完全集成**
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 注册业务事件钩子
|
|
|
|
|
|
hookSystem.OnUserCreated("metrics-collector", hooks.PriorityHigh, func(ctx context.Context, user interface{}) error {
|
|
|
|
|
|
return businessMetrics.RecordUserCreated("register")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
hookSystem.OnUserCreated("welcome-email", hooks.PriorityNormal, func(ctx context.Context, user interface{}) error {
|
|
|
|
|
|
return emailService.SendWelcome(ctx, user)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 触发事件(在业务代码中)
|
|
|
|
|
|
results, err := hookSystem.TriggerUserCreated(ctx, newUser)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**钩子类型**:
|
|
|
|
|
|
|
|
|
|
|
|
- ✅ 同步钩子(阻塞执行)
|
|
|
|
|
|
- ✅ 异步钩子(后台执行)
|
|
|
|
|
|
- ✅ 优先级控制
|
|
|
|
|
|
- ✅ 超时保护
|
|
|
|
|
|
- ✅ 错误策略(继续/停止/收集)
|
|
|
|
|
|
|
|
|
|
|
|
## 🏗️ **架构集成图**
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ HTTP 请求层 │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 追踪中间件 → 指标中间件 → 限流中间件 → 认证中间件 │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 业务处理层 │
|
|
|
|
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
|
|
|
|
│ │ Handler │ │ Service │ │ Repository │ │
|
|
|
|
|
|
│ │ + 钩子 │ │ + 重试 │ │ + 熔断器 │ │
|
|
|
|
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
|
|
|
|
├─────────────────────────────────────────────────────────────┤
|
|
|
|
|
|
│ 基础设施层 │
|
|
|
|
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
|
|
|
|
│ │ 链路追踪 │ │ 指标收集 │ │ 分布式事务 │ │
|
|
|
|
|
|
│ │ (OpenTel) │ │(Prometheus) │ │ (Saga) │ │
|
|
|
|
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🛠️ **使用指南**
|
|
|
|
|
|
|
|
|
|
|
|
### **启动验证**
|
|
|
|
|
|
|
|
|
|
|
|
1. **编译验证**:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
go build ./cmd/api
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
2. **启动应用**:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
./api
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
3. **检查指标端点**:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl http://localhost:8080/metrics
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
4. **检查健康状态**:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl http://localhost:8080/health
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### **配置示例**
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
# config.yaml 完整高级特性配置
|
|
|
|
|
|
app:
|
|
|
|
|
|
name: "tyapi-server"
|
|
|
|
|
|
version: "1.0.0"
|
|
|
|
|
|
env: "production"
|
|
|
|
|
|
|
|
|
|
|
|
monitoring:
|
|
|
|
|
|
# 链路追踪配置
|
|
|
|
|
|
tracing_enabled: true
|
|
|
|
|
|
tracing_endpoint: "http://jaeger:4317"
|
|
|
|
|
|
sample_rate: 0.1
|
|
|
|
|
|
|
|
|
|
|
|
# 指标收集配置
|
|
|
|
|
|
metrics_enabled: true
|
|
|
|
|
|
metrics_endpoint: "/metrics"
|
|
|
|
|
|
|
|
|
|
|
|
resilience:
|
|
|
|
|
|
# 熔断器配置
|
|
|
|
|
|
circuit_breaker_enabled: true
|
|
|
|
|
|
failure_threshold: 5
|
|
|
|
|
|
timeout: 30s
|
|
|
|
|
|
|
|
|
|
|
|
# 重试配置
|
|
|
|
|
|
retry_enabled: true
|
|
|
|
|
|
max_retries: 3
|
|
|
|
|
|
retry_delay: 100ms
|
|
|
|
|
|
|
|
|
|
|
|
saga:
|
|
|
|
|
|
# 分布式事务配置
|
|
|
|
|
|
default_timeout: 30s
|
|
|
|
|
|
max_retries: 3
|
|
|
|
|
|
enable_persistence: false
|
|
|
|
|
|
|
|
|
|
|
|
hooks:
|
|
|
|
|
|
# 钩子系统配置
|
|
|
|
|
|
default_timeout: 30s
|
|
|
|
|
|
track_duration: true
|
|
|
|
|
|
error_strategy: "continue"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📋 **监控仪表板**
|
|
|
|
|
|
|
|
|
|
|
|
### **推荐监控栈**
|
|
|
|
|
|
|
|
|
|
|
|
1. **链路追踪**: Jaeger UI
|
|
|
|
|
|
|
|
|
|
|
|
- 地址: `http://localhost:16686`
|
|
|
|
|
|
- 查看完整请求链路
|
|
|
|
|
|
|
|
|
|
|
|
2. **指标监控**: Prometheus + Grafana
|
|
|
|
|
|
|
|
|
|
|
|
- Prometheus: `http://localhost:9090`
|
|
|
|
|
|
- Grafana: `http://localhost:3000`
|
|
|
|
|
|
|
|
|
|
|
|
3. **应用指标**: 内置指标端点
|
|
|
|
|
|
- 地址: `http://localhost:8080/metrics`
|
|
|
|
|
|
|
|
|
|
|
|
### **关键监控指标**
|
|
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
|
# 告警规则建议
|
|
|
|
|
|
groups:
|
|
|
|
|
|
- name: tyapi-server
|
|
|
|
|
|
rules:
|
|
|
|
|
|
- alert: HighErrorRate
|
|
|
|
|
|
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
|
|
|
|
|
|
|
|
|
|
|
|
- alert: CircuitBreakerOpen
|
|
|
|
|
|
expr: circuit_breaker_state{state="open"} > 0
|
|
|
|
|
|
|
|
|
|
|
|
- alert: SagaFailure
|
|
|
|
|
|
expr: rate(saga_failed_total[5m]) > 0.05
|
|
|
|
|
|
|
|
|
|
|
|
- alert: HighLatency
|
|
|
|
|
|
expr: histogram_quantile(0.95, http_request_duration_seconds) > 1
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🔧 **性能优化建议**
|
|
|
|
|
|
|
|
|
|
|
|
### **生产环境配置**
|
|
|
|
|
|
|
|
|
|
|
|
1. **追踪采样率**: 建议设置为 0.01-0.1 (1%-10%)
|
|
|
|
|
|
2. **指标收集**: 启用所有核心指标,按需启用业务指标
|
|
|
|
|
|
3. **熔断器阈值**: 根据服务 SLA 调整失败阈值
|
|
|
|
|
|
4. **钩子超时**: 设置合理的钩子执行超时时间
|
|
|
|
|
|
|
|
|
|
|
|
### **扩展性考虑**
|
|
|
|
|
|
|
|
|
|
|
|
1. **水平扩展**: 所有组件都支持多实例部署
|
|
|
|
|
|
2. **状态无关**: 追踪和指标数据通过外部系统存储
|
|
|
|
|
|
3. **配置热更新**: 支持运行时配置调整
|
|
|
|
|
|
|
|
|
|
|
|
## 🎯 **最佳实践**
|
|
|
|
|
|
|
|
|
|
|
|
### **链路追踪**
|
|
|
|
|
|
|
|
|
|
|
|
- 在关键业务操作中主动创建 Span
|
|
|
|
|
|
- 使用有意义的操作名称
|
|
|
|
|
|
- 添加重要的标签和属性
|
|
|
|
|
|
|
|
|
|
|
|
### **指标收集**
|
|
|
|
|
|
|
|
|
|
|
|
- 合理设置指标标签,避免高基数
|
|
|
|
|
|
- 定期清理不再使用的指标
|
|
|
|
|
|
- 使用直方图记录耗时分布
|
|
|
|
|
|
|
|
|
|
|
|
### **弹性设计**
|
|
|
|
|
|
|
|
|
|
|
|
- 在外部服务调用时使用熔断器
|
|
|
|
|
|
- 对瞬时失败使用重试机制
|
|
|
|
|
|
- 设计优雅降级策略
|
|
|
|
|
|
|
|
|
|
|
|
### **事件钩子**
|
|
|
|
|
|
|
|
|
|
|
|
- 保持钩子函数简单快速
|
|
|
|
|
|
- 使用异步钩子处理耗时操作
|
|
|
|
|
|
- 合理设置钩子优先级
|
|
|
|
|
|
|
|
|
|
|
|
## 🔍 **故障排查**
|
|
|
|
|
|
|
|
|
|
|
|
### **常见问题**
|
|
|
|
|
|
|
|
|
|
|
|
1. **追踪数据丢失**
|
|
|
|
|
|
|
|
|
|
|
|
- 检查 OTLP 端点连接性
|
|
|
|
|
|
- 确认采样率配置
|
|
|
|
|
|
- 查看应用日志中的追踪错误
|
|
|
|
|
|
|
|
|
|
|
|
2. **指标不更新**
|
|
|
|
|
|
|
|
|
|
|
|
- 验证 Prometheus 抓取配置
|
|
|
|
|
|
- 检查指标端点可访问性
|
|
|
|
|
|
- 确认指标注册成功
|
|
|
|
|
|
|
|
|
|
|
|
3. **熔断器异常触发**
|
|
|
|
|
|
- 检查失败阈值设置
|
|
|
|
|
|
- 分析下游服务健康状态
|
|
|
|
|
|
- 调整超时时间
|
|
|
|
|
|
|
|
|
|
|
|
## 🏆 **集成完成状态**
|
|
|
|
|
|
|
|
|
|
|
|
| 特性模块 | 实现状态 | 容器集成 | 中间件 | 配置支持 | 文档完整度 |
|
|
|
|
|
|
| ---------- | -------- | -------- | ------------- | -------- | ---------- |
|
|
|
|
|
|
| 链路追踪 | ✅ 100% | ✅ 完成 | ✅ 已集成 | ✅ 完整 | ✅ 完整 |
|
|
|
|
|
|
| 指标监控 | ✅ 100% | ✅ 完成 | ✅ 已集成 | ✅ 完整 | ✅ 完整 |
|
|
|
|
|
|
| 熔断器 | ✅ 100% | ✅ 完成 | ⚠️ 手动集成 | ✅ 完整 | ✅ 完整 |
|
|
|
|
|
|
| 重试机制 | ✅ 100% | ✅ 完成 | ⚠️ 手动集成 | ✅ 完整 | ✅ 完整 |
|
|
|
|
|
|
| 分布式事务 | ✅ 100% | ✅ 完成 | ⚠️ 手动集成 | ✅ 完整 | ✅ 完整 |
|
|
|
|
|
|
| 钩子系统 | ✅ 100% | ✅ 完成 | ⚠️ 应用级集成 | ✅ 完整 | ✅ 完整 |
|
|
|
|
|
|
|
|
|
|
|
|
## 🎉 **总结**
|
|
|
|
|
|
|
|
|
|
|
|
TYAPI Server 现已完成所有企业级高级特性的完整集成:
|
|
|
|
|
|
|
|
|
|
|
|
✅ **已完成的核心能力**:
|
|
|
|
|
|
|
|
|
|
|
|
- 分布式链路追踪 (OpenTelemetry + OTLP)
|
|
|
|
|
|
- 全方位指标监控 (Prometheus + 业务指标)
|
|
|
|
|
|
- 多层次弹性恢复 (熔断器 + 重试机制)
|
|
|
|
|
|
- 分布式事务管理 (Saga 模式)
|
|
|
|
|
|
- 灵活事件钩子系统
|
|
|
|
|
|
|
|
|
|
|
|
✅ **生产就绪特性**:
|
|
|
|
|
|
|
|
|
|
|
|
- 完整的容器依赖注入
|
|
|
|
|
|
- 自动化中间件集成
|
|
|
|
|
|
- 优雅的生命周期管理
|
|
|
|
|
|
- 完善的配置系统
|
|
|
|
|
|
- 详细的监控指标
|
|
|
|
|
|
|
|
|
|
|
|
✅ **开发体验**:
|
|
|
|
|
|
|
|
|
|
|
|
- 编译零错误
|
|
|
|
|
|
- 热插拔组件设计
|
|
|
|
|
|
- 丰富的使用示例
|
|
|
|
|
|
- 完整的故障排查指南
|
|
|
|
|
|
|
|
|
|
|
|
现在您的 TYAPI Server 已经具备了企业级产品的所有核心监控和弹性能力!🚀
|